Skip to content

Instantly share code, notes, and snippets.

@abiiranathan
Last active February 27, 2025 21:20
Show Gist options
  • Save abiiranathan/5c0fe0281021ea79cf8903495ca8feef to your computer and use it in GitHub Desktop.
Save abiiranathan/5c0fe0281021ea79cf8903495ca8feef to your computer and use it in GitHub Desktop.
A utility for managing semantic versioning with Git tags
#!/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 "$@"
@abiiranathan
Copy link
Author

Add alias.semver=!sh $HOME/git-semver.sh to your ~/.gitconfig to add an alias

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment