Files
git-release/git-release

116 lines
3.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# git-release
# https://github.com/obnitram/git-release
# -------------------------------------------------
# Usage: git-release [<projectname>] [-f]
# Accepts tags of the form vX.Y.Z or <projectname>-vX.Y.Z (no prerelease/build).
# Interactively bumps patch/minor/major, creates an annotated tag,
# and pushes it to origin.
#
# Requirements:
# - The last tag MUST match ^v[0-9]+\.[0-9]+\.[0-9]+$ or ^<projectname>-v[0-9]+\.[0-9]+\.[0-9]+$ (or none => starts from v0.0.0)
# - Working tree must be clean (unless -f is used).
set -euo pipefail
trap 'echo "❌ ${0##*/} failed at line $LINENO." >&2; exit 1' ERR
# --- Parse arguments ---
FORCE_FLAG=false
PROJECT_NAME=""
for arg in "$@"; do
if [[ "$arg" == "-f" ]]; then
FORCE_FLAG=true
else
PROJECT_NAME="$arg"
fi
done
if [[ -n "$PROJECT_NAME" ]]; then
TAG_PREFIX="${PROJECT_NAME}-v"
TAG_PATTERN="${PROJECT_NAME}-v[0-9]*.[0-9]*.[0-9]*"
TAG_REGEX="^${PROJECT_NAME}-v([0-9]+)\.([0-9]+)\.([0-9]+)$"
DEFAULT_TAG="${PROJECT_NAME}-v0.0.0"
else
TAG_PREFIX="v"
TAG_PATTERN="v[0-9]*.[0-9]*.[0-9]*"
TAG_REGEX="^v([0-9]+)\.([0-9]+)\.([0-9]+)$"
DEFAULT_TAG="v0.0.0"
fi
# --- Safety: ensure we are inside a Git repository ---
git rev-parse --is-inside-work-tree >/dev/null 2>&1
# --- Safety: require a clean working tree (unless -f flag is used) ---
if [[ "$FORCE_FLAG" == false ]]; then
if ! git diff-index --quiet HEAD --; then
echo "⚠️ Uncommitted changes detected. Please commit or stash before releasing."
echo " Or use -f flag to force the release anyway."
exit 1
fi
fi
# --- Get strictly matching tags and pick the highest (lexicographic by version) ---
# Using Git's glob to only list matching pattern, then version-sort.
LAST_TAG=$(git tag --list "$TAG_PATTERN" | sort -V | tail -n1)
CURRENT_TAG=${LAST_TAG:-$DEFAULT_TAG}
# --- Validate strict format ---
if [[ ! "$CURRENT_TAG" =~ $TAG_REGEX ]]; then
echo "❌ Latest tag '$CURRENT_TAG' does not match expected format."
if [[ -n "$PROJECT_NAME" ]]; then
echo " Please create a proper base tag, e.g.: git tag -a ${PROJECT_NAME}-v0.0.0 -m 'init'"
else
echo " Please create a proper base tag, e.g.: git tag -a v0.0.0 -m 'init'"
fi
exit 1
fi
# --- Parse numbers (strict) ---
MAJOR=${BASH_REMATCH[1]}
MINOR=${BASH_REMATCH[2]}
PATCH=${BASH_REMATCH[3]}
echo "📦 Current version: $CURRENT_TAG"
echo "Select release type:"
select TYPE in "patch" "minor" "major" "cancel"; do
case "$TYPE" in
patch) PATCH=$((PATCH + 1)); break ;;
minor) MINOR=$((MINOR + 1)); PATCH=0; break ;;
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0; break ;;
cancel) echo "❌ Release cancelled."; exit 0 ;;
*) echo "Invalid selection. Please choose 1, 2, 3, or 4." ;;
esac
done
# --- Build next tag ---
NEXT_TAG="${TAG_PREFIX}${MAJOR}.${MINOR}.${PATCH}"
# --- Double-check strictness before proceeding ---
if [[ ! "$NEXT_TAG" =~ $TAG_REGEX ]]; then
echo "❌ Computed tag '$NEXT_TAG' does not match expected format. Aborting."
exit 1
fi
# --- Refuse to overwrite (local & remote) ---
if git rev-parse -q --verify "refs/tags/$NEXT_TAG" >/dev/null; then
echo "❌ Local tag $NEXT_TAG already exists. Aborting."
exit 1
fi
if git ls-remote --tags origin "refs/tags/$NEXT_TAG" | grep -q .; then
echo "❌ Remote tag $NEXT_TAG already exists on origin. Aborting."
exit 1
fi
echo "🚀 Creating new release: $NEXT_TAG"
read -r -p "Confirm? (y/N): " CONFIRM
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
echo "❌ Release cancelled."
exit 0
fi
# --- Create and push the annotated tag ---
git tag -a "$NEXT_TAG" -m "Release $NEXT_TAG"
git push origin "$NEXT_TAG"
echo "✅ Release $NEXT_TAG created and pushed successfully!"