Last active
February 27, 2025 21:20
-
-
Save abiiranathan/5c0fe0281021ea79cf8903495ca8feef to your computer and use it in GitHub Desktop.
A utility for managing semantic versioning with Git tags
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# | |
# git-semver: A utility for managing semantic versioning with Git tags | |
# | |
# Installation: | |
# 1. Save this file as 'git-semver' in a directory in your PATH (e.g., /usr/local/bin/) | |
# 2. Make it executable: chmod +x /path/to/git-semver | |
# 3. Use it as a Git subcommand: git semver <command> | |
# | |
set -e | |
VERSION="1.0.0" | |
SCRIPT_NAME="git-semver" | |
# Print usage information | |
usage() { | |
cat <<EOF | |
$SCRIPT_NAME v$VERSION - Semantic versioning utility for Git repositories | |
USAGE: | |
git semver [COMMAND] [OPTIONS] | |
COMMANDS: | |
push [TAG] Create and push a new tag (auto-increments patch version if no tag provided) | |
major Increment the major version (X.0.0) and create a new tag | |
minor Increment the minor version (x.Y.0) and create a new tag | |
patch Increment the patch version (x.y.Z) and create a new tag | |
current Display the current version tag | |
next [PART] Show the next version without creating a tag (PART: major, minor, patch) | |
help Display this help message | |
EXAMPLES: | |
git semver push # Auto-increment patch version | |
git semver push v2.1.0 # Create and push specific tag v2.1.0 | |
git semver major # Increment major version and push | |
git semver next minor # Show the next minor version without creating a tag | |
EOF | |
} | |
# Get the latest tag or use v0.0.0 if none exists | |
get_latest_tag() { | |
git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0" | |
} | |
# Parse a semantic version tag into its components | |
# Sets the global variables: major, minor, patch, suffix | |
parse_semver() { | |
local tag=$1 | |
if [[ $tag =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z0-9.-]+)?$ ]]; then | |
major="${BASH_REMATCH[1]}" | |
minor="${BASH_REMATCH[2]}" | |
patch="${BASH_REMATCH[3]}" | |
suffix="${BASH_REMATCH[4]:-}" | |
return 0 | |
else | |
echo "Error: '$tag' does not follow semantic versioning format (vX.Y.Z)" | |
return 1 | |
fi | |
} | |
# Create and push a Git tag | |
create_and_push_tag() { | |
local tag_name=$1 | |
local force=$2 | |
# Sanity check - validate the tag format | |
if ! [[ $tag_name =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then | |
echo "Error: '$tag_name' does not follow semantic versioning format (vX.Y.Z)" | |
return 1 | |
fi | |
# Check if the tag already exists | |
if git rev-parse "$tag_name" >/dev/null 2>&1; then | |
if [ "$force" = "true" ]; then | |
echo "Warning: Tag '$tag_name' already exists, force option enabled. Overwriting..." | |
git tag -d "$tag_name" >/dev/null | |
else | |
echo "Error: Tag '$tag_name' already exists. Use --force to overwrite." | |
return 1 | |
fi | |
fi | |
echo "Creating tag: $tag_name" | |
git tag -a "$tag_name" -m "Tag: $tag_name" | |
echo "Pushing tag to origin..." | |
git push origin "$tag_name" | |
echo "Successfully created and pushed tag: $tag_name" | |
} | |
# Command: push - Create and push a new tag | |
cmd_push() { | |
local tag_name="" | |
local force=false | |
# Parse arguments | |
while [[ $# -gt 0 ]]; do | |
case $1 in | |
--force|-f) | |
force=true | |
shift | |
;; | |
-*) | |
echo "Error: Unknown option $1" | |
usage | |
exit 1 | |
;; | |
*) | |
if [ -z "$tag_name" ]; then | |
tag_name="$1" | |
else | |
echo "Error: Too many arguments" | |
usage | |
exit 1 | |
fi | |
shift | |
;; | |
esac | |
done | |
# If tag name is provided, use it directly | |
if [ -n "$tag_name" ]; then | |
create_and_push_tag "$tag_name" "$force" | |
return $? | |
fi | |
# Otherwise, auto-increment the latest tag | |
local latest_tag=$(get_latest_tag) | |
if ! parse_semver "$latest_tag"; then | |
echo "Creating initial version v0.0.1" | |
create_and_push_tag "v0.0.1" "$force" | |
return $? | |
fi | |
# Increment patch version by default | |
patch=$((patch + 1)) | |
# Set the new tag name | |
if [ -n "$suffix" ]; then | |
tag_name="v$major.$minor.$patch$suffix" | |
else | |
tag_name="v$major.$minor.$patch" | |
fi | |
echo "Auto-incrementing to tag: $tag_name" | |
create_and_push_tag "$tag_name" "$force" | |
} | |
# Command: major - Increment major version | |
cmd_major() { | |
local force=false | |
# Parse arguments | |
while [[ $# -gt 0 ]]; do | |
case $1 in | |
--force|-f) | |
force=true | |
shift | |
;; | |
*) | |
echo "Error: Unknown option $1" | |
usage | |
exit 1 | |
;; | |
esac | |
done | |
local latest_tag=$(get_latest_tag) | |
if ! parse_semver "$latest_tag"; then | |
return 1 | |
fi | |
major=$((major + 1)) | |
minor=0 | |
patch=0 | |
suffix="" # Remove suffix when incrementing major version | |
tag_name="v$major.$minor.$patch" | |
echo "Bumping major version to: $tag_name" | |
create_and_push_tag "$tag_name" "$force" | |
} | |
# Command: minor - Increment minor version | |
cmd_minor() { | |
local force=false | |
# Parse arguments | |
while [[ $# -gt 0 ]]; do | |
case $1 in | |
--force|-f) | |
force=true | |
shift | |
;; | |
*) | |
echo "Error: Unknown option $1" | |
usage | |
exit 1 | |
;; | |
esac | |
done | |
local latest_tag=$(get_latest_tag) | |
if ! parse_semver "$latest_tag"; then | |
return 1 | |
fi | |
minor=$((minor + 1)) | |
patch=0 | |
suffix="" # Remove suffix when incrementing minor version | |
tag_name="v$major.$minor.$patch" | |
echo "Bumping minor version to: $tag_name" | |
create_and_push_tag "$tag_name" "$force" | |
} | |
# Command: patch - Increment patch version | |
cmd_patch() { | |
local force=false | |
# Parse arguments | |
while [[ $# -gt 0 ]]; do | |
case $1 in | |
--force|-f) | |
force=true | |
shift | |
;; | |
*) | |
echo "Error: Unknown option $1" | |
usage | |
exit 1 | |
;; | |
esac | |
done | |
local latest_tag=$(get_latest_tag) | |
if ! parse_semver "$latest_tag"; then | |
return 1 | |
fi | |
patch=$((patch + 1)) | |
# Keep suffix when incrementing patch version | |
tag_name="v$major.$minor.$patch$suffix" | |
echo "Bumping patch version to: $tag_name" | |
create_and_push_tag "$tag_name" "$force" | |
} | |
# Command: current - Show the current version | |
cmd_current() { | |
local latest_tag=$(get_latest_tag) | |
if ! parse_semver "$latest_tag"; then | |
echo "No valid semantic version tag found" | |
return 1 | |
fi | |
echo "Current version: $latest_tag" | |
echo " Major: $major" | |
echo " Minor: $minor" | |
echo " Patch: $patch" | |
if [ -n "$suffix" ]; then | |
echo " Suffix: $suffix" | |
fi | |
} | |
# Command: next - Show the next version without creating a tag | |
cmd_next() { | |
local part="patch" | |
if [ $# -gt 0 ]; then | |
part="$1" | |
fi | |
local latest_tag=$(get_latest_tag) | |
if ! parse_semver "$latest_tag"; then | |
return 1 | |
fi | |
local next_tag="" | |
case "$part" in | |
major) | |
major=$((major + 1)) | |
minor=0 | |
patch=0 | |
suffix="" | |
next_tag="v$major.$minor.$patch" | |
;; | |
minor) | |
minor=$((minor + 1)) | |
patch=0 | |
suffix="" | |
next_tag="v$major.$minor.$patch" | |
;; | |
patch) | |
patch=$((patch + 1)) | |
next_tag="v$major.$minor.$patch$suffix" | |
;; | |
*) | |
echo "Error: Unknown version part '$part'. Use 'major', 'minor', or 'patch'." | |
return 1 | |
;; | |
esac | |
echo "Next $part version would be: $next_tag" | |
} | |
# Main function | |
main() { | |
if [ $# -eq 0 ]; then | |
usage | |
exit 0 | |
fi | |
local command="$1" | |
shift | |
case "$command" in | |
push) | |
cmd_push "$@" | |
;; | |
major) | |
cmd_major "$@" | |
;; | |
minor) | |
cmd_minor "$@" | |
;; | |
patch) | |
cmd_patch "$@" | |
;; | |
current) | |
cmd_current "$@" | |
;; | |
next) | |
cmd_next "$@" | |
;; | |
help|--help|-h) | |
usage | |
;; | |
*) | |
echo "Error: Unknown command '$command'" | |
usage | |
exit 1 | |
;; | |
esac | |
} | |
# Execute main function | |
main "$@" | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add
alias.semver=!sh $HOME/git-semver.sh
to your ~/.gitconfig to add an alias