Writing Git commit messages with Claude

No more context switching

originally published Sat Oct 26 2024 00:00:00 GMT+0000 (Coordinated Universal Time)

Howdy!

Let me share a little something that may have slightly changed my life.

I don’t really enjoy writing Git commit messages. In fact, I will write the most terse, useless commits just to get around it. Maybe AI can help?

The script

Here’s my take on a solution using Claude. First, ai-commit-msg.sh . It’s one job is checking the staged files diff, looking at a couple of recent commits, and coming up with the commit message, and writing it to STDOUT. I’ve tweaked this prompt quite a bit, but please feel free to tweak it to your needs. I’ll also keep this updated if I make any significant changes:

#!/bin/bash

# Check if ANTHROPIC_API_KEY is set
if [ -z "$ANTHROPIC_API_KEY" ]; then
  echo "Error: ANTHROPIC_API_KEY environment variable is not set" >&2
  exit 1
fi

# Get git diff context
diff_context=$(git diff --cached --diff-algorithm=minimal)

if [ -z "$diff_context" ]; then
  echo "Error: No staged changes found" >&2
  exit 1
fi

# Get last 3 commit messages
recent_commits=$(git log -3 --pretty=format:"%B" | sed 's/"/\\"/g')

# Prepare the prompt
prompt="Generate a git commit message following this structure:
1. First line: conventional commit format (type: concise description) (remember to use semantic types like feat, fix, docs, style, refactor, perf, test, chore, etc.)
2. Optional bullet points if more context helps:
   - Keep the second line blank
   - Keep them short and direct
   - Focus on what changed
   - Always be terse
   - Don't overly explain
   - Drop any fluffy or formal language

Return ONLY the commit message - no introduction, no explanation, no quotes around it.

Examples:
feat: add user auth system

- Add JWT tokens for API auth
- Handle token refresh for long sessions

fix: resolve memory leak in worker pool

- Clean up idle connections
- Add timeout for stale workers

Simple change example:
fix: typo in README.md

Very important: Do not respond with any of the examples. Your message must be based off the diff that is about to be provided, with a little bit of styling informed by the recent commits you're about to see.

Recent commits from this repo (for style reference):
$recent_commits

Here's the diff:

$diff_context"

# Properly escape the prompt for JSON
json_escaped_prompt=$(jq -n --arg prompt "$prompt" '$prompt')

# Call Claude API with properly escaped JSON
response=$(curl -s https://api.anthropic.com/v1/messages \
    -H "Content-Type: application/json" \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -H "anthropic-version: 2023-06-01" \
    --data-raw "{
        \"model\": \"claude-3-sonnet-20240229\",
        \"max_tokens\": 300,
        \"messages\": [
            {
                \"role\": \"user\",
                \"content\": ${json_escaped_prompt}
            }
        ]
    }")

# Extract the commit message from the response
commit_message=$(echo "$response" | jq -r '.content[0].text')

# Output the commit message
echo "$commit_message"

ai-commit-msg.sh

And ai-commit.sh . This simply calls the other script and passes the commit message in. You can use this if you already have stuff staged and you’re ready to commit:

#!/bin/bash

# Get AI-generated commit message
commit_message=$(~/dev/scripts/ai-commit-msg.sh)

# Use git commit -m with -e to edit
git commit -m "$commit_message" -e

ai-commit.sh

Now I mostly commit things directly from Neovim, using Fugitive . Here’s my super simple solution for using this from Nvim:

-- run :AICommitMsg from a commit buffer to get an AI generated commit message
vim.api.nvim_create_user_command('AICommitMsg', function()
  local text = vim.fn.system('~/dev/scripts/ai-commit-msg.sh')
  vim.api.nvim_put(vim.split(text, '\n', {}), '', false, true)
end, {})

-- stage everything, then open a commit buffer with an AI generated commit message
vim.api.nvim_create_user_command('AICommit', function()
  vim.fn.system('git add .')
  vim.cmd('Git commit')
  vim.cmd('AICommitMsg')
end, {})

init.lua

Couldn’t be simpler. You can run AICommitMsg to insert an AI generated commit msg if you’re currently in a Git commit buffer. But the way I use it is generally the latter command, AICommit . This stages everything, starts a commit, and generates the message.

Benefits

Using this feels like I’ve uncovered some heretofore unknown secrets of the universe, and unlocked productivity I never knew was possible. Two large benefits I’ve noticed:

  1. First, the obvious one. I don’t need to stop and think so much about what to write. Critically though, when you use this, you’re not just accepting the commit message blindly. You have an opportunity to edit it and be the sanity check that’s needed when working with language models.
  2. Secondly, I’ve found I’m making many more, smaller, commits. I know these to be better in the long run of course, but laziness often prevents me from stopping what I’m doing and writing a commit for one small piece of work, even if I know it’s a nice logical chunk to commit. Simply because of the mental energy it takes to context switch and write the commit message. Now, I just run AICommitMsg when I know I’ve just finished a logical, self contained piece of work, and the commit message is almost always pretty good—I just occasionally tweak it a bit.

Well, that’s pretty much it! I hope you find it useful. Feel free to, like I said, tweak this prompt to your needs.