Avatar

Harper Reed's Blog

Use an llm to automagically generate meaningful git commit messages

· 970 単語 · 5 分 ·

他の言語で読む: English 🇺🇸

TL;DR: pre-commit-msg Git フックに llm CLI を組み込めば、最近のコード変更を要約したコミットメッセージが自動で生成される。

プロジェクトをいじるのは大好きなのに、意味のあるコミットメッセージを書くのはめちゃくちゃ苦手。自分でも驚くほど怠け者だ。

たとえばこれ:

My terrible commit messages

コミットメッセージが完全にダメ。

Never fear, LLMs are here.

友人の Kanno が、git diff からコミットメッセージを生成してくれるシンプルな Git エイリアスを送ってくれた。かなりしっかり動く。

# 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"

けれど自分は shell-gpt ではなく、Simon の LLM CLI を使いたかった。LLM は対応モデルが豊富で、ローカルモデルや MLX も利用できる。

さらに .gitconfig を何度もいじるのは面倒なので、プロンプトは外部ファイルに分けたい。

そこで ~/.config/prompts/git-commit-message.txt にプロンプトを保存した。内容はこうだ:

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]"

エイリアスを次のように更新した:

gpt = "!f() { git diff $1 | llm -s \"$(cat ~/.config/prompts/commit-system-prompt.txt)\" }; f"

これでやりたいことはほとんど実現したが、まだ手間が残る。

そこで Claude に頼んで、メッセージが気に入らなければコミットを中断できるインタラクティブ版を作ってもらった。

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"

あと一歩というところだったので、さらに詰めてこうなった:

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"

満足できたけれど、まだゴチャゴチャしている。

Git Hooked

そこで思い出したのが Git フックだ。

Claude に再度頼んで、prepare-commit-msg イベントで動くシンプルなフックを書いてもらった。

メッセージを自分で入力すればフックはスキップされるし、メッセージを省けば LLM が呼ばれる。

フックの内容は以下のとおり:

#!/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() {
  spinner=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
  while true; do
    for i in "${spinner[@]}"; do
      tput civis
      tput el1
      printf "\\r${YELLOW}%s${NC} Generating LLM commit message..." "$i"
      sleep 0.1
      tput cub 32
    done
  done
}

# Skip on merge commits
if [ -n "$2" ]; then
  exit 0
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 spinner
spin_animation &
spin_pid=$!

# Generate the commit message
if ! commit_msg=$(git diff --cached | llm -s "$(cat ~/.config/prompts/commit-system-prompt.txt)" 2>&1); then
  kill $spin_pid
  wait $spin_pid 2>/dev/null
  tput cnorm
  printf "\\n"
  printf "${RED}Error: 'llm' command failed to generate the commit message:\\n${commit_msg}${NC}\\n\\nManually set the commit message"
  exit 1
fi

kill $spin_pid
wait $spin_pid 2>/dev/null
tput cnorm
echo

echo "${BLUE}=== Generated Commit Message ===${NC}"
echo "${GREEN}$commit_msg${NC}"
echo "${BLUE}=================================${NC}"
echo

echo "$commit_msg" > "$1"

(ChatGPT added the documentation)

ちゃんと動くし、スピナーもエラーハンドリングもあって見た目もいい。

これでメッセージなしで git commit するとフックが発火し、ステージした変更を llm CLI とシステムプロンプトに送り、コミットメッセージを生成してくれる。出力はこんな感じ:

🤖💬 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! 😂👌

最高だ。自分の フックプロンプト は dotfiles に置いてあるので参考にどうぞ。SKIP_LLM_GITHOOK 環境変数をセットすれば無効化できる。

How to set this up!

1. Install llm.

手順は llm.datasette.io を参照。自分は pipx でインストールした。

pipx install llm

OpenAI のキーとデフォルトモデルを設定:

llm keys set openai
llm models default gpt-4-turbo

(LLM CLI はローカルモデルを含む多数のモデルをサポートしており、試す価値あり)

2. Create a new directory for your prompts:

mkdir -p ~/.config/prompts

3. Add your system prompt:

フックは ~/.config/prompts/commit-system-prompt.txt を読み込む。次の内容を保存しよう。

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]"

これは暫定版 (v0)。改善案があればぜひ教えてほしい。

4. Create a new directory for your global Git hooks.

mkdir -p ~/.git_hooks

5. Touch the prepare-commit-msg

~/.git_hooks に拡張子なしで prepare-commit-msg ファイルを作成する。

6. Open the prepare-commit-msg file in a text editor (vi or death) and add the same content as before:

(訳注: “vi or death” は「vi を使うか死ぬかだぜ」というジョーク)

先ほどのシェルスクリプトをそのまま貼り付けて保存。

7. Make the prepare-commit-msg file executable

chmod +x ~/.git_hooks/prepare-commit-msg

8. Configure Git to use your global hooks directory

git config --global core.hooksPath ~/.git_hooks

9. Code, build things and then commit something

Explanation on how it works

core.hooksPath~/.git_hooks に設定したことで、どのリポジトリでも git commit を実行するとグローバル prepare-commit-msg フックが走る。フックはステージした変更を llm~/.config/prompts/commit-system-prompt.txt に渡し、生成されたメッセージをコミットメッセージファイルに書き込む。

LLM の生成をスキップしたい場合は、メッセージを付けてコミットすればいい:

git commit -m "fixed issue #420"

This is just a hack. AI will hallucinate.

作るのはとても楽しかったし笑える。でもときどき「Fixed issue #54」みたいな謎行を付け足すことがある。今のところ変更内容を捏造した例はないけれど、結果は人それぞれだ。

役に立ったらぜひメールしてほしい → harper@modest.com

この投稿は98%人間が執筆しました。