#!/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: # ()!: # # [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[@]}"