|
#!/bin/bash |
|
# Git commit message improvement script using Claude CLI |
|
# Can improve the last commit or all unpushed commits on current branch |
|
|
|
set -e |
|
|
|
# Colour codes for output |
|
RED='\033[0;31m' |
|
GREEN='\033[0;32m' |
|
YELLOW='\033[1;33m' |
|
NC='\033[0m' # No Colour |
|
|
|
# Default model (empty means use Claude Code's default) |
|
DEFAULT_MODEL="" |
|
|
|
# Function to print coloured output |
|
print_colour() { |
|
local colour=$1 |
|
shift |
|
echo -e "${colour}$@${NC}" |
|
} |
|
|
|
# Function to check if we're in a git repository |
|
check_git_repo() { |
|
if ! git rev-parse --git-dir > /dev/null 2>&1; then |
|
print_colour $RED "Error: Not in a git repository" |
|
exit 1 |
|
fi |
|
} |
|
|
|
# Function to check if message is generic |
|
is_generic_message() { |
|
local msg="$1" |
|
local msg_lower=$(echo "$msg" | tr '[:upper:]' '[:lower:]' | xargs) |
|
|
|
local generic_patterns=( |
|
"^update$" |
|
"^fix$" |
|
"^changes$" |
|
"^updates$" |
|
"^fixes$" |
|
"^wip$" |
|
"^commit$" |
|
"^save$" |
|
"^done$" |
|
"^test$" |
|
"^testing$" |
|
"^stuff$" |
|
"^minor.*$" |
|
"^small.*$" |
|
"^quick.*$" |
|
"^add.*$" |
|
) |
|
|
|
for pattern in "${generic_patterns[@]}"; do |
|
if [[ "$msg_lower" =~ $pattern ]]; then |
|
return 0 |
|
fi |
|
done |
|
|
|
return 1 |
|
} |
|
|
|
# Function to get improved commit message from Claude |
|
get_improved_message() { |
|
local original_msg="$1" |
|
local commit_sha="$2" |
|
|
|
# Get the diff for this commit |
|
local diff="" |
|
if [ -z "$commit_sha" ]; then |
|
# For staged changes |
|
diff=$(git diff --cached) |
|
else |
|
# For existing commit |
|
diff=$(git show --format="" "$commit_sha") |
|
fi |
|
|
|
# Check if diff is empty |
|
if [ -z "$diff" ]; then |
|
print_colour $YELLOW "Warning: No changes found in commit" >&2 |
|
return 1 |
|
fi |
|
|
|
# Create a temporary file with the context |
|
local temp_context=$(mktemp) |
|
cat > "$temp_context" << EOF |
|
The current commit message is: "$original_msg" |
|
|
|
Here are the changes: |
|
$diff |
|
|
|
Please analyze these changes and provide a better, more descriptive commit message that: |
|
1. Summarizes what was changed |
|
2. Explains why (if apparent from the changes) |
|
3. Follows conventional commit format if possible (feat:, fix:, docs:, etc.) |
|
4. Is concise but informative (ideally under 72 characters for the first line) |
|
|
|
Just output the new commit message, nothing else. |
|
EOF |
|
|
|
# Build claude command with optional model |
|
local claude_cmd="claude -p" |
|
if [ -n "$CLAUDE_MODEL" ]; then |
|
claude_cmd="$claude_cmd --model $CLAUDE_MODEL" |
|
fi |
|
|
|
# Debug mode - show command if DEBUG env var is set |
|
if [ -n "$DEBUG" ]; then |
|
print_colour $YELLOW "Debug: Running command: $claude_cmd" >&2 |
|
print_colour $YELLOW "Debug: Context length: $(wc -c < "$temp_context") bytes" >&2 |
|
print_colour $YELLOW "Debug: First 100 chars of diff: ${diff:0:100}..." >&2 |
|
fi |
|
|
|
# Call Claude CLI to improve the message, capture both stdout and stderr |
|
local error_file=$(mktemp) |
|
local output_file=$(mktemp) |
|
|
|
# Run the command and capture everything |
|
# Using proper Claude CLI syntax |
|
$claude_cmd < "$temp_context" >"$output_file" 2>"$error_file" |
|
local exit_code=$? |
|
|
|
# Read the output |
|
local new_msg=$(cat "$output_file") |
|
local error_msg=$(cat "$error_file") |
|
|
|
# Debug output |
|
if [ -n "$DEBUG" ]; then |
|
print_colour $YELLOW "Debug: Exit code: $exit_code" >&2 |
|
print_colour $YELLOW "Debug: Output length: $(echo -n "$new_msg" | wc -c) chars" >&2 |
|
print_colour $YELLOW "Debug: Error output: $error_msg" >&2 |
|
if [ -n "$new_msg" ]; then |
|
print_colour $YELLOW "Debug: Raw output: '$new_msg'" >&2 |
|
fi |
|
fi |
|
|
|
# Check for errors |
|
if [ $exit_code -ne 0 ]; then |
|
if [ -n "$DEBUG" ] || [ -n "$VERBOSE" ]; then |
|
print_colour $RED "Error: Claude CLI failed with exit code $exit_code" >&2 |
|
if [ -n "$error_msg" ]; then |
|
print_colour $RED "Error message: $error_msg" >&2 |
|
fi |
|
fi |
|
|
|
# Check if claude is installed |
|
if ! command -v claude &> /dev/null; then |
|
print_colour $RED "Error: 'claude' command not found. Please install Claude CLI first." >&2 |
|
print_colour $YELLOW "Visit: https://github.com/anthropics/claude-cli" >&2 |
|
fi |
|
fi |
|
|
|
# Clean up temp files |
|
rm -f "$temp_context" "$error_file" "$output_file" |
|
|
|
if [ $exit_code -eq 0 ] && [ -n "$new_msg" ]; then |
|
# Remove any markdown formatting that might be present |
|
new_msg=$(echo "$new_msg" | sed 's/^```.*//g' | sed 's/```$//g' | xargs) |
|
echo "$new_msg" |
|
return 0 |
|
else |
|
return 1 |
|
fi |
|
} |
|
|
|
# Function to confirm action with user |
|
confirm() { |
|
local prompt="$1" |
|
local response |
|
|
|
while true; do |
|
read -p "$prompt (y/n): " response |
|
case $response in |
|
[Yy]* ) return 0;; |
|
[Nn]* ) return 1;; |
|
* ) echo "Please answer yes (y) or no (n).";; |
|
esac |
|
done |
|
} |
|
|
|
# Function to improve last commit |
|
improve_last_commit() { |
|
local commit_sha=$(git rev-parse HEAD) |
|
local original_msg=$(git log -1 --pretty=%B) |
|
|
|
print_colour $YELLOW "Current commit message: '$original_msg'" |
|
|
|
if [ -n "$CLAUDE_MODEL" ]; then |
|
print_colour $YELLOW "Using model: $CLAUDE_MODEL" |
|
fi |
|
|
|
if ! is_generic_message "$original_msg"; then |
|
print_colour $GREEN "Commit message appears to be descriptive already." |
|
if ! confirm "Do you want to improve it anyway?"; then |
|
return 0 |
|
fi |
|
fi |
|
|
|
print_colour $YELLOW "Using Claude CLI to generate a better commit message..." |
|
|
|
local new_msg=$(get_improved_message "$original_msg" "$commit_sha") |
|
|
|
if [ $? -eq 0 ] && [ -n "$new_msg" ] && [ "$new_msg" != "$original_msg" ]; then |
|
print_colour $GREEN "Suggested commit message: '$new_msg'" |
|
|
|
if confirm "Apply this commit message?"; then |
|
git commit --amend -m "$new_msg" |
|
print_colour $GREEN "✓ Commit message updated successfully!" |
|
else |
|
print_colour $YELLOW "Commit message not changed." |
|
fi |
|
else |
|
print_colour $RED "Failed to generate improved message, keeping original" |
|
fi |
|
} |
|
|
|
# Function to improve all unpushed commits on current branch |
|
improve_branch_commits() { |
|
local upstream=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || echo "") |
|
|
|
if [ -z "$upstream" ]; then |
|
print_colour $RED "No upstream branch found. Please set an upstream branch first." |
|
exit 1 |
|
fi |
|
|
|
# Get list of unpushed commits |
|
local unpushed_commits=$(git rev-list $upstream..HEAD) |
|
|
|
if [ -z "$unpushed_commits" ]; then |
|
print_colour $YELLOW "No unpushed commits found on current branch." |
|
return 0 |
|
fi |
|
|
|
local commit_count=$(echo "$unpushed_commits" | wc -l) |
|
print_colour $YELLOW "Found $commit_count unpushed commit(s) on current branch." |
|
|
|
if [ -n "$CLAUDE_MODEL" ]; then |
|
print_colour $YELLOW "Using model: $CLAUDE_MODEL" |
|
fi |
|
|
|
# Process commits from oldest to newest |
|
local commits_array=() |
|
while IFS= read -r commit; do |
|
commits_array+=("$commit") |
|
done <<< "$(echo "$unpushed_commits" | tac)" |
|
|
|
local improved_count=0 |
|
|
|
for commit_sha in "${commits_array[@]}"; do |
|
local original_msg=$(git log -1 --pretty=%B "$commit_sha") |
|
print_colour $YELLOW "\nCommit: $commit_sha" |
|
print_colour $YELLOW "Current message: '$original_msg'" |
|
|
|
local should_improve=false |
|
|
|
if is_generic_message "$original_msg"; then |
|
print_colour $YELLOW "This appears to be a generic commit message." |
|
should_improve=true |
|
else |
|
if confirm "Do you want to improve this message?"; then |
|
should_improve=true |
|
fi |
|
fi |
|
|
|
if [ "$should_improve" = true ]; then |
|
print_colour $YELLOW "Generating improved message..." |
|
local new_msg=$(get_improved_message "$original_msg" "$commit_sha") |
|
|
|
if [ $? -eq 0 ] && [ -n "$new_msg" ] && [ "$new_msg" != "$original_msg" ]; then |
|
print_colour $GREEN "Suggested message: '$new_msg'" |
|
|
|
if confirm "Apply this commit message?"; then |
|
# Use interactive rebase to change the commit message |
|
GIT_SEQUENCE_EDITOR="sed -i '1s/pick/reword/'" git rebase -i $commit_sha^ |
|
GIT_EDITOR="echo '$new_msg' >" git rebase --continue |
|
|
|
((improved_count++)) |
|
print_colour $GREEN "✓ Commit message updated!" |
|
else |
|
print_colour $YELLOW "Skipping this commit." |
|
fi |
|
else |
|
print_colour $RED "Failed to generate improved message for this commit." |
|
fi |
|
fi |
|
done |
|
|
|
print_colour $GREEN "\nSummary: Improved $improved_count out of $commit_count unpushed commits." |
|
} |
|
|
|
# Function to install as git alias |
|
install_git_alias() { |
|
local script_path=$(realpath "$0") |
|
|
|
print_colour $YELLOW "Installing git aliases..." |
|
|
|
# Long form aliases |
|
git config --global alias.improve-commit "!$script_path" |
|
git config --global alias.improve-branch "!$script_path --branch" |
|
|
|
# Short form aliases |
|
git config --global alias.ic "!$script_path" |
|
git config --global alias.ib "!$script_path --branch" |
|
|
|
print_colour $GREEN "✓ Git aliases installed successfully!" |
|
print_colour $GREEN " Long form:" |
|
print_colour $GREEN " - Use 'git improve-commit' to improve the last commit" |
|
print_colour $GREEN " - Use 'git improve-branch' to improve all unpushed commits on current branch" |
|
print_colour $GREEN " Short form:" |
|
print_colour $GREEN " - Use 'git ic' to improve the last commit" |
|
print_colour $GREEN " - Use 'git ib' to improve all unpushed commits on current branch" |
|
} |
|
|
|
# Main script logic |
|
main() { |
|
# Check for model environment variable or flag |
|
CLAUDE_MODEL="$DEFAULT_MODEL" |
|
|
|
# Process model flag if present |
|
while [[ $# -gt 0 ]]; do |
|
case $1 in |
|
--model|-m) |
|
CLAUDE_MODEL="$2" |
|
shift 2 |
|
;; |
|
*) |
|
break |
|
;; |
|
esac |
|
done |
|
|
|
check_git_repo |
|
|
|
# Parse command line arguments |
|
case "${1:-}" in |
|
--last|-l) |
|
improve_last_commit |
|
;; |
|
--branch|-b) |
|
improve_branch_commits |
|
;; |
|
--install) |
|
install_git_alias |
|
;; |
|
--test) |
|
# Test claude CLI functionality |
|
print_colour $YELLOW "Testing Claude CLI installation..." |
|
|
|
if ! command -v claude &> /dev/null; then |
|
print_colour $RED "✗ claude command not found in PATH" |
|
exit 1 |
|
fi |
|
|
|
print_colour $GREEN "✓ claude found at: $(which claude)" |
|
|
|
# Try a simple test |
|
print_colour $YELLOW "Testing claude with a simple prompt..." |
|
local test_output=$(echo "Say 'Hello' and nothing else" | claude 2>&1) |
|
local test_exit=$? |
|
|
|
if [ $test_exit -eq 0 ]; then |
|
print_colour $GREEN "✓ claude is working! Output: $test_output" |
|
else |
|
print_colour $RED "✗ claude test failed with exit code: $test_exit" |
|
print_colour $RED "Error output: $test_output" |
|
print_colour $YELLOW "\nPossible issues:" |
|
print_colour $YELLOW "1. Claude CLI may not be authenticated" |
|
print_colour $YELLOW "2. Network connection issues" |
|
print_colour $YELLOW "3. Invalid API configuration" |
|
fi |
|
;; |
|
--help|-h) |
|
cat << EOF |
|
Git Commit Message Improvement Script |
|
|
|
Usage: $0 [OPTIONS] |
|
|
|
Options: |
|
--model, -m MODEL Use specific Claude model (e.g., claude-3-haiku-20240307) |
|
--last, -l Improve the last commit message (default) |
|
--branch, -b Improve all unpushed commits on current branch |
|
--install Install as git aliases (improve-commit, improve-branch, ic, ib) |
|
--help, -h Show this help message |
|
|
|
Environment Variables: |
|
DEBUG=1 Show debug information |
|
VERBOSE=1 Show error details |
|
CLAUDE_MODEL=... Set default model |
|
|
|
Model Options: |
|
You can specify a model in three ways: |
|
1. Command line: $0 --model claude-3-haiku-20240307 |
|
2. Environment variable: CLAUDE_MODEL=claude-3-haiku-20240307 $0 |
|
3. Git config: git config --global commit.claude-model claude-3-haiku-20240307 |
|
|
|
Common fast models: |
|
- claude-3-haiku-20240307 (fastest, good for simple commits) |
|
- claude-3-sonnet-20240229 (balanced speed and quality) |
|
- claude-3-opus-20240229 (highest quality, slower) |
|
|
|
Examples: |
|
$0 # Improve last commit with default model |
|
$0 --model claude-3-haiku-20240307 # Use Haiku (fast) model |
|
$0 --branch -m claude-3-haiku-20240307 # Improve branch with Haiku |
|
git ic # After installing aliases |
|
git ib # After installing aliases |
|
|
|
This script uses Claude CLI to analyze your commits and suggest better, |
|
more descriptive commit messages. |
|
EOF |
|
;; |
|
"") |
|
# No arguments - default to improving last commit |
|
improve_last_commit |
|
;; |
|
*) |
|
print_colour $RED "Error: Invalid option '${1}'. Use --help for usage information." |
|
exit 1 |
|
;; |
|
esac |
|
} |
|
|
|
# Check for git config model setting |
|
if [ -z "$CLAUDE_MODEL" ]; then |
|
GIT_MODEL=$(git config --get commit.claude-model 2>/dev/null || echo "") |
|
if [ -n "$GIT_MODEL" ]; then |
|
CLAUDE_MODEL="$GIT_MODEL" |
|
fi |
|
fi |
|
|
|
# Check for environment variable |
|
if [ -n "$CLAUDE_MODEL_ENV" ]; then |
|
CLAUDE_MODEL="$CLAUDE_MODEL_ENV" |
|
fi |
|
|
|
# Run main function |
|
main "$@" |