Use an llm to automagically generate meaningful git commit messages
TL;DR: You can set a pre-commit-msg git hook to call the llm
cli and get a summary of your recent code changes as your commit message.
I love hacking on projects, but often I am super bad at making commits that make sense.
For instance:
Trash commit messages. I am lazy!
Never fear, LLMs are here.
Originally my buddy Kanno sent me a snippet that would allow you to have a simple git alias that would generate a commit message from the git diff. It was pretty robust.
# generate comment
gpt = "!f() { git diff $1 | sgpt 'Write concise, informative commit messages: Start with a summary in imperative mood, explain the 'why' behind changes, keep the summary under 50 characters, use bullet points for multiple changes, and reference related issues or tickets. What you write will be passed to git commit -m \"[message]\"'; }; f"
However, I wanted to use Simon’s LLM cli instead of shell gpt. LLM has way more model support, and can use local models, MLX, etc.
I also wanted the prompt to be stored externally so I could iterate on it without having to fuck with the .gitconfig
over and over again.
I went ahead and put my prompt in ~/.config/prompts/git-commit-message.txt
. Here is the prompt:
Write short commit messages:
- The first line should be a short summary of the changes
- Remember to mention the files that were changed, and what was changed
- Explain the 'why' behind changes
- Use bullet points for multiple changes
- Tone: Use a LOT of emojis, be funny, and expressive. Feel free to be profane, but don't be offensive
- If there are no changes, or the input is blank - then return a blank string
Think carefully before you write your commit message.
The output format should be:
Summary of changes
- changes
- changes
What you write will be passed directly to git commit -m "[message]"
And here is the updated gpt alias:
gpt = "!f() { git diff $1 | llm -s \"$(cat ~/.config/prompts/commit-system-prompt.txt)\" }; f"
This did everything I wanted it to do. However, I am lazy, so I wanted to add a bit more magic.
I asked claude to make it more interactive and allow me to abort the commit message if it sucked.
llm = "!f() { \
if git diff --quiet $1; then \
echo \"No changes to commit. Aborting.\"; \
else \
commit_msg=$(git diff $1 | llm -s \"$(cat ~/.config/prompts/commit-system-prompt.txt)\"); \
echo \"Commit message:\n$commit_msg\"; \
read -p \"Do you want to commit with this message? [y/N] \" confirm; \
if [[ $confirm =~ ^[Yy]$ ]]; then \
git commit -m \"$commit_msg\"; \
else \
echo \"Commit aborted.\"; \
fi; \
fi; \
}; f"
This was so so close. I asked claude again, and we got to this:
llm-staged = "!f() { \
git add -p; \
if ! git diff --cached --quiet; then \
commit_msg=$(git diff --cached | llm -s \"$(cat ~/.config/prompts/commit-system-prompt.txt)\"); \
echo \"Commit message:\n$commit_msg\"; \
read -p \"Do you want to commit with this message? [y/N] \" confirm; \
if [[ $confirm =~ ^[Yy]$ ]]; then \
git commit -m \"$commit_msg\"; \
else \
git reset HEAD .; \
echo \"Commit aborted.\"; \
fi; \
else \
echo \"No changes staged for commit. Aborting.\"; \
fi; \
}; f"
I was satisfied, but this was still too much work, and too kludgy.
Git Hooked
Then I remembered! Git hooks! Lol. Why would I have that in my brain - who knows!
I asked claude again, and they whipped up a simple script that would act as a hook that triggers with the prepare-commit-msg
event.
This is awesome, cuz if you want to add a git message, you can skip the hook. But if you are lazy, you exclude the message and it will call the LLM.
The commit hook is super simple:
#!/bin/sh
# Exit if the `SKIP_LLM_GITHOOK` environment variable is set
if [ ! -z "$SKIP_LLM_GITHOOK" ]; then
exit 0
fi
# ANSI color codes for styling the output
RED='\033[0;31m' # Sets text to red
GREEN='\033[0;32m' # Sets text to green
YELLOW='\033[0;33m' # Sets text to yellow
BLUE='\033[0;34m' # Sets text to blue
NC='\033[0m' # Resets the text color to default, no color
# Function to display a spinning animation during the LLM processing
spin_animation() {
# Array of spinner characters for the animation
spinner=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
# Infinite loop to keep the animation running
while true; do
for i in "${spinner[@]}"; do
tput civis # Hide the cursor to enhance the animation appearance
tput el1 # Clear the line from the cursor to the beginning to display the spinner
printf "\\r${YELLOW}%s${NC} Generating LLM commit message..." "$i" # Print the spinner and message
sleep 0.1 # Delay to control the speed of the animation
tput cub 32 # Move the cursor back 32 columns to reset the spinner position
done
done
}
# Check if the commit is a merge commit based on the presence of a second argument
if [ -n "$2" ]; then
exit 0 # Exit script if it's a merge commit, no custom message needed
fi
# Check if the `llm` command is installed
if ! command -v llm &> /dev/null; then
echo "${RED}Error: 'llm' command is not installed. Please install it and try again.${NC}"
exit 1
fi
# Start the spinning animation in the background
spin_animation &
spin_pid=$! # Capture the process ID of the spinning animation
# Generate the commit message using `git diff` piped into `llm` command
# The LLM command takes a system prompt from a file as input
if ! commit_msg=$(git diff --cached | llm -s "$(cat ~/.config/prompts/commit-system-prompt.txt)" 2>&1); then
# Stop the spinning animation by killing its process
kill $spin_pid
wait $spin_pid 2>/dev/null # Wait for the process to terminate and suppress error messages
# Finalizing output
tput cnorm # Show the cursor again
printf "\\n" # Move the cursor to the next line
printf "${RED}Error: 'llm' command failed to generate the commit message:\\n${commit_msg}${NC}\\n\\nManually set the commit message"
exit 1
fi
# Stop the spinning animation by killing its process
kill $spin_pid
wait $spin_pid 2>/dev/null # Wait for the process to terminate and suppress error messages
# Finalizing output
tput cnorm # Show the cursor again
echo # Move the cursor to the next line
# Display the generated commit message with color-coded headings
echo "${BLUE}=== Generated Commit Message ===${NC}"
echo "${GREEN}$commit_msg${NC}"
echo "${BLUE}=================================${NC}"
echo
# Write the generated commit message to the specified file (usually the commit message file in .git)
echo "$commit_msg" > "$1"
(ChatGPT added the documentation)
It works! And has a spinner! And catches errors! And is pretty!
Now, whenever I commit without a message, the commit hook executes and sends the diff of the changes to the llm cli with the system prompt previously defined. The output is really nice!
🤖💬 AI-powered git commit messages FTW! 🚀🎉
- Updated content/post/2024-03-11-ai-git-commit-messages.md
- Added links to my actual git hook and prompt in dotfiles repo 🔗
- Removed unnecessary code block formatting for the output example 🗑️
- AI is making us lazy devs, but who cares when commit messages are this awesome! 😂👌
Yay. Much better! You can see my hook and my prompt in my dotfiles.
You can even disable it by setting the SKIP_LLM_GITHOOK
environment variable.
How to set this up!
1. Install llm
.
Visit llm.datasette.io for instructions. I used pipx
to install it:
pipx install llm
Remember to set your key and default model.
Set your Openai key:
llm keys set openai
Set which model is default:
llm models default gpt-4-turbo
(The llm
cli is awesome. It supports lots of different models (including local models), and contexts. Worth digging in for sure)
2. Create a new directory for your prompts:
mkdir -p ~/.config/prompts
3. Add your system prompt:
The hook will look in ~/.config/prompts/commit-system-prompt.txt
for the system prompt. You can create a file with the following content:
Write short commit messages:
- The first line should be a short summary of the changes
- Remember to mention the files that were changed, and what was changed
- Explain the 'why' behind changes
- Use bullet points for multiple changes
- Tone: Use a LOT of emojis, be funny, and expressive. Feel free to be profane, but don't be offensive
- If there are no changes, or the input is blank - then return a blank string
Think carefully before you write your commit message.
The output format should be:
Summary of changes
- changes
- changes
What you write will be passed directly to git commit -m "[message]"
This prompt worked great for me - but let me know if you have changes. I consider this prompt v0.
4. Create a new directory for your global Git hooks.
For example, you can create a directory named git_hooks
in your home directory:
mkdir -p ~/.git_hooks
5. Touch the prepare-commit-msg
Create a new file named prepare-commit-msg
(without any extension) in the ~/.git_hooks
directory.
6. Open the prepare-commit-msg
file in a text editor (vi or death) and add the same content as before:
#!/bin/sh
# Exit if the `SKIP_LLM_GITHOOK` environment variable is set
if [ ! -z "$SKIP_LLM_GITHOOK" ]; then
exit 0
fi
# ANSI color codes for styling the output
RED='\033[0;31m' # Sets text to red
GREEN='\033[0;32m' # Sets text to green
YELLOW='\033[0;33m' # Sets text to yellow
BLUE='\033[0;34m' # Sets text to blue
NC='\033[0m' # Resets the text color to default, no color
# Function to display a spinning animation during the LLM processing
spin_animation() {
# Array of spinner characters for the animation
spinner=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
# Infinite loop to keep the animation running
while true; do
for i in "${spinner[@]}"; do
tput civis # Hide the cursor to enhance the animation appearance
tput el1 # Clear the line from the cursor to the beginning to display the spinner
printf "\\r${YELLOW}%s${NC} Generating LLM commit message..." "$i" # Print the spinner and message
sleep 0.1 # Delay to control the speed of the animation
tput cub 32 # Move the cursor back 32 columns to reset the spinner position
done
done
}
# Check if the commit is a merge commit based on the presence of a second argument
if [ -n "$2" ]; then
exit 0 # Exit script if it's a merge commit, no custom message needed
fi
# Check if the `llm` command is installed
if ! command -v llm &> /dev/null; then
echo "${RED}Error: 'llm' command is not installed. Please install it and try again.${NC}"
exit 1
fi
# Start the spinning animation in the background
spin_animation &
spin_pid=$! # Capture the process ID of the spinning animation
# Generate the commit message using `git diff` piped into `llm` command
# The LLM command takes a system prompt from a file as input
if ! commit_msg=$(git diff --cached | llm -s "$(cat ~/.config/prompts/commit-system-prompt.txt)" 2>&1); then
# Stop the spinning animation by killing its process
kill $spin_pid
wait $spin_pid 2>/dev/null # Wait for the process to terminate and suppress error messages
# Finalizing output
tput cnorm # Show the cursor again
printf "\\n" # Move the cursor to the next line
printf "${RED}Error: 'llm' command failed to generate the commit message:\\n${commit_msg}${NC}\\n\\nManually set the commit message"
exit 1
fi
# Stop the spinning animation by killing its process
kill $spin_pid
wait $spin_pid 2>/dev/null # Wait for the process to terminate and suppress error messages
# Finalizing output
tput cnorm # Show the cursor again
echo # Move the cursor to the next line
# Display the generated commit message with color-coded headings
echo "${BLUE}=== Generated Commit Message ===${NC}"
echo "${GREEN}$commit_msg${NC}"
echo "${BLUE}=================================${NC}"
echo
# Write the generated commit message to the specified file (usually the commit message file in .git)
echo "$commit_msg" > "$1"
You can see mine in my dotfiles.
7. Make the prepare-commit-msg
file executable
Run the following command in your terminal:
chmod +x ~/.git_hooks/prepare-commit-msg
8. Configure Git to use your global hooks directory
Run the following command to set your global hooks directory
git config --global core.hooksPath ~/.git_hooks
9. Code, build things and then commit something
Explanation on how it works
This command sets the core.hooksPath
configuration option to your global hooks directory (~/.git_hooks
).
Now, whenever you run git commit
in any of your repositories, Git will execute the global prepare-commit-msg
hook located in ~/.git_hooks/prepare-commit-msg
. The hook will generate the commit message based on the staged changes using the llm
command and the system prompt from ~/.config/prompts/commit-system-prompt.txt
.
By setting up a global prepare-commit-msg
hook, you can have the commit message generation functionality available in all your repositories without the need to set it up individually for each repository.
Remember to have the llm
command and the ~/.config/prompts/commit-system-prompt.txt
file set up correctly for the global hook to work as expected.
With this global hook in place, you can simply stage your changes normally using git add
or git add -p
, and then run git commit
.
The global prepare-commit-msg
hook will automatically generate the commit message for you, ready for review and editing before finalizing the commit.
If you want to skip the LLM commit message generation, just commit with a message: git commit -m “fixed issue #420”
. This seems to bypass the pre commit hook.
This is just a hack. AI will hallucinate.
I had fun building this, and it is hilarious.
I have had it hallucinate hilarious things. Never making up changes (thus far), but doing weird shit like adding “Fixed issue #54” at the end.
Like everything in life, YMMV.
If this is helpful, send me an email and let me know! My email is harper@modest.com.