nix/bin/gc

198 lines
4.4 KiB
Bash
Executable file

#!/usr/bin/env bash
# Conventional commit helper
# Usage: gc [type] [message] [-s scope] [-b] [-B body]
# gc -e [type] (open in editor with template)
# gc (interactive mode)
set -o errexit
set -o nounset
TYPES="feat fix docs style refactor test chore perf ci build"
usage() {
echo "Usage: gc [type] [message] [--scope scope] [-b] [-B body]"
echo ' gc -e [type] # open in $EDITOR'
echo " gc (interactive mode)"
echo ""
echo "Options:"
echo ' -e, --edit Open commit message in $EDITOR'
echo " --scope Scope of the change"
echo " -b, --breaking Mark as breaking change (!)"
echo " -B, --body Commit body (longer description)"
echo ""
echo "Git passthrough flags:"
echo " -s, --signoff Add Signed-off-by trailer"
echo " -a, --all Stage all modified files"
echo " -S, --gpg-sign GPG sign the commit"
echo " -v, --verbose Show diff in editor"
echo " --amend Amend previous commit"
echo ""
echo "Types: $TYPES"
echo ""
echo "Examples:"
echo " gc feat 'add user auth'"
echo " gc fix 'resolve crash' -s api # fix(api): resolve crash"
echo " gc feat 'new api' -b # feat!: new api"
echo " gc feat 'new api' -s auth -b # feat(auth)!: new api"
echo " gc feat 'big change' -B 'Details here'"
echo ' gc -e feat # edit feat commit in $EDITOR'
exit 1
}
editor_mode() {
local type="$1"
shift
local git_args=("$@")
local template
template=$(mktemp)
trap "rm -f $template" EXIT
cat >"$template" <<'EOF'
# Conventional Commit Format:
# <type>(<scope>)!: <description>
#
# [optional body]
#
# [optional footer(s)]
#
# Types: feat fix docs style refactor test chore perf ci build
# Add ! before : for breaking changes (e.g., feat!: or feat(api)!:)
#
# Examples:
# feat: add user authentication
# fix(api): resolve null pointer exception
# feat(auth)!: change token format
# docs: update API documentation
EOF
# Prepend type if provided
if [ -n "$type" ]; then
sed -i.bak "1s/^/$type: /" "$template" && rm -f "$template.bak"
fi
git commit -e -t "$template" "${git_args[@]}"
}
build_message() {
local type="$1"
local scope="$2"
local breaking="$3"
local msg="$4"
local body="$5"
local commit_msg="$type"
if [ -n "$scope" ]; then
commit_msg+="($scope)"
fi
if [ "$breaking" = "true" ]; then
commit_msg+="!"
fi
commit_msg+=": $msg"
if [ -n "$body" ]; then
commit_msg+=$'\n\n'"$body"
fi
echo "$commit_msg"
}
# Interactive mode if no args
if [ $# -eq 0 ]; then
echo "Types: $TYPES"
printf "Type: "
read -r type
printf "Scope (optional): "
read -r scope
printf "Breaking change? [y/N]: "
read -r breaking_input
printf "Message: "
read -r msg
printf "Body (optional, enter for none): "
read -r body
breaking="false"
if [[ $breaking_input =~ ^[Yy] ]]; then
breaking="true"
fi
commit_msg=$(build_message "$type" "$scope" "$breaking" "$msg" "$body")
git commit -m "$commit_msg"
exit 0
fi
# Parse arguments
type=""
msg=""
scope=""
breaking="false"
body=""
use_editor="false"
git_args=()
while [ $# -gt 0 ]; do
case "$1" in
-e | --edit)
use_editor="true"
shift
;;
--scope)
scope="$2"
shift 2
;;
-b | --breaking)
breaking="true"
shift
;;
-B | --body)
body="$2"
shift 2
;;
-h | --help)
usage
;;
# Pass through git commit flags
-s | --signoff | -a | --all | -v | --verbose | -n | --no-verify | --amend | --no-edit)
git_args+=("$1")
shift
;;
-S | --gpg-sign)
git_args+=("$1")
shift
;;
*)
if [ -z "$type" ]; then
type="$1"
else
msg="$1"
fi
shift
;;
esac
done
# Editor mode
if [ "$use_editor" = "true" ]; then
editor_mode "$type" "${git_args[@]}"
exit 0
fi
# Validate type
if ! echo "$TYPES" | grep -qw "$type"; then
echo "Invalid type: $type"
echo "Valid types: $TYPES"
exit 1
fi
if [ -z "$msg" ]; then
echo "Message required"
usage
fi
commit_msg=$(build_message "$type" "$scope" "$breaking" "$msg" "$body")
git commit -m "$commit_msg" "${git_args[@]}"