kasiopeiya

git-commit

git差分を分析し、フォーマットに従ったコミットを自動作成する。

kasiopeiya 0 Updated 1w ago
GitHub

Install

npx skillscat add kasiopeiya/claude-dev-template/git-commit

Install via the SkillsCat registry.

SKILL.md

Git Commit Helper

git の変更内容を分析し、適切なコミットメッセージを生成し、自律的にコミットを実行するスキルです。サブエージェントを使わず、このスキル内で完結して処理します。

  • 対象範囲はデフォルトでワーキングツリー・staged 双方の全差分。引数 work(ワーキングツリーのみ)/stage(stagedのみ)で範囲を限定できる
  • そのセッションで編集したかどうかは問わず、リポジトリの現在の git 状態にある差分ファイルすべてが対象。ただし並行する別セッションを検出したときは、このセッションで編集したファイルだけに絞る(Phase 1)
  • ブランチは確認しない。main ブランチでも確認なしでコミットしてよい
  • コミット実行前のユーザー承認は求めない。コミット分割は Phase 2.5 の機械的な規則(type×ディレクトリ)だけで決め、その規則で1通りに定まらないときだけ AskUserQuestion で確認する
  • git rebase は使用しない(マージには 3-way merge または fast-forward を使う)。履歴の書き換えがバグ特定を困難にするためである

コミットのルール

commit・branch 操作をする前に従うルール。

コミットの書き方

コミットメッセージには変更の理由・背景を書く。

「何を変えたか」はコードを読めばわかる。「なぜ変えたか」はコードからは読み取れない。その情報をメッセージに残す。

# Bad: 何をしたかしか書かれていない
パスワードバリデーションを追加

# Good: なぜ必要だったかが書かれている
パスワードバリデーションを追加

短いパスワードによる不正アクセスのリスクを下げるため、
8文字以上を必須とするバリデーションを実装した。

issue対応時のコミットメッセージ

コミットは、件名の末尾に (#issue番号) を付与する。

形式は type: 説明 (#issue番号) とする。

# Bad: どのissue対応か追跡できない
fix: ログイン失敗時のエラーメッセージを修正

# Good: 件名にissue番号があり、対応関係を追跡できる
fix: ログイン失敗時のエラーメッセージを修正 (#42)

issue番号を記録するのは、コミットとissue(背景・議論・受け入れ条件)を双方向に辿れるようにするためである。

なお、Closes #42 のようなクローズ用キーワードは、コミットメッセージの件名・本文に書かない。本プロジェクトの開発フローではissueのタスク一覧(チェックリスト)を1つずつ進める運用であり、コミット単位でissueを自動クローズすると作業途中で閉じてしまうためである。

この禁止が掛かるのはコミットメッセージだけである。PR本文には Closes #42 を書いてよい。 PRのマージは作業が完了した時点なので、途中で閉じてしまう問題が起きない。

「なぜ」をどこに書くか(本文とissueの使い分け)

「なぜ変えたか」を残す場所は、コミット本文 と issue の2つがある。件名(type: 説明)はどの場合も必須である。

issueの書かれ方 「なぜ」の置き場所 コミット本文
issueの「この変更が必要な理由」節に、この変更の理由が書かれている issue 省略可((#番号) で辿る)
issueに無い判断をした issue+コミット その判断の理由を書く
# Good: issueに背景があり、本文は省略
fix: ログイン失敗時のエラーメッセージを修正 (#42)

# Good: issueに無い「実装上の判断」を本文に残す
fix: ログイン失敗時のエラーメッセージを修正 (#42)

ライブラリXのバリデーションは多言語対応が不十分なため、
issueの方針とは別に自前のメッセージ生成へ差し替えた。

この使い分けが成立する前提は、上記の表の1行目の条件を満たすことである。満たさない場合は、本方式は理由がどこにも残らない状態になるため、本文に書く。

決定記録を残すコミット

コミットに含まれる決定が、docs/policy/adr-policy.md の判定で「コミットの決定記録」になるときは、本文に決定・理由・却下した案を書き、最後のトレーラーの段落に Decision-Record: yes を足す。Issue の「対応方針」の書き先に決定記録と書かれていれば、それに従う。このトレーラーの付いたコミットは、/past-decision-search が git log から引く。トレーラーの段落以外に Decision-Record: yes の行を書かない。

feat: 一覧 API のページングをカーソル方式にする (#123)

決定: 一覧 API のページングは、オフセットではなくカーソル(最後に返した行のキー)で行う。
理由: 一覧の途中で行が追加・削除されると、オフセット方式では行の重複や抜けが起きる。
却下した案:
- オフセット方式:件数が増えるほど遅くなり、途中の追加・削除で重複や抜けが起きる

Decision-Record: yes

コミットの粒度

粒度は基本的に任意とする。ただし、コミットメッセージと無関係なファイルを含めてはならない。気づいた割れ窓は直すか起票するが、無関係な変更を1つのコミットに詰めるのは別問題である(「割れ窓を放置しない」refined-engineer-judgment-principles)。

# Bad: ログイン修正とは無関係なCSSの変更が混入している
fix: ログイン失敗時のエラーメッセージを修正
- src/auth/login.ts
- src/styles/global.css   ← 関係ない

# Good: メッセージに関係するファイルだけが含まれている
fix: ログイン失敗時のエラーメッセージを修正
- src/auth/login.ts

ブランチ命名規則

ブランチ名は <prefix>/<説明> の形式とする。

  • feat/:新機能・機能変更
  • fix/:バグ修正
  • refactor/:振る舞いを変えない構造の変更
  • chore/:設定変更・依存関係の更新など
  • ci/:CI/CD・GitHub Actionsの変更
  • test/:テストの追加・修正
  • docs/:ドキュメントの変更

処理フロー

Phase 1:対象範囲の決定と変更内容の取得

引数に応じて対象範囲を決定する:

  • なし(デフォルト):ワーキングツリー・staged 双方の差分
  • work:ワーキングツリー(未ステージ)の差分のみ
  • stage:staged の差分のみ

並行セッションを検出する

同じ作業ツリーを別の Claude セッションが編集していると、既定スコープは無関係な変更まで巻き込む。巻き込まれた側は自分の作業が別 Issue のコミットに紛れたことに気づけない。そこで、範囲を決めたらまず同じ作業ツリーで動いている claude セッションの数を数える。

top=$(git rev-parse --show-toplevel)
if command -v lsof >/dev/null 2>&1; then
  ps -eo pid=,comm= | awk '$2 == "claude" {print $1}' | while read -r pid; do
    cwd=$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p')
    [ -n "$cwd" ] && [ "$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)" = "$top" ] && echo "$pid"
  done | wc -l
else
  npm run -s --prefix "$top" count-registered-sessions
fi

2以上なら並行セッションがある。0 は判定できなかったことを示すので、2以上と同じく扱う。このとき既定スコープを「このセッションで自分が Edit/Write したファイルのみ」に切り替える(引数 work / stage の指定も、この絞り込みの中で適用する)。切り替えたことと、除外したファイルのパスを Phase 5 の出力に列挙する。

1なら単独セッションなので、既定スコープをそのまま使う。

範囲を決めたら現在の状態を把握する。スコープに不要なコマンドは実行しない(work なら staged 側、stage なら worktree 側は取得不要)。依存関係のないコマンドは1回の Bash 呼び出しにまとめる:

git status --short                # 常に取得
git diff --stat                   # スコープが「なし」または work のときのみ
git diff --cached --stat          # スコープが「なし」または stage のときのみ

git diff(内容)と git log --oneline は無条件に取得しない。Phase 2 の type 判定・Phase 3 の本文執筆で実際に必要になった時点で、必要な対象だけに絞って取得する(type がファイル名だけで判定できるならどちらも不要)。

ステータス出力の先頭2文字でステージ状態・変更タイプを判定する:

M  file.ts      # Modified(ステージ済み)
 M file.ts      # Modified(未ステージ)
A  file.md      # Added(ステージ済み)
?? file.txt     # Untracked

Phase 2:Commit Type の判定

type の語彙は上記「ブランチ命名規則」の prefix 一覧に従う(末尾の / を除いたものが type)。まずファイル名・パス・ステータス(A/M)だけで判定を試み、それで確定しない場合のみ diff 内容を取得する:

  • テストファイル(*.test.*, *.spec.*)のみ:test
  • docs/ または *.md のみ:docs
  • 設定ファイル(package.json, tsconfig.json, *.config.* など)のみ:chore
  • CI・GitHub Actions の変更:ci
  • ソースの新規追加(A)による機能拡張:feat
  • ソースの修正(M)によるバグ修正:fix
  • 振る舞いを変えない構造変更:refactor

A=feat・M=fix はあくまで目安。修正が機能追加のこともあるため、ファイル名だけでは判断がつかない場合に限り diff 内容(該当ファイルのみ)を読む。それでもdiff 内容が type 一覧(feat/fix/refactor/docs/chore/ci/test)のどれとも一致しない、または複数の type に同時に当てはまるときは AskUserQuestion でユーザーに確認する。

Phase 2.5:コミット分割の判定

上記「コミットの粒度」(無関係なファイルを混ぜない)を実践する。ファイルを type × トップレベルディレクトリでグループ化し、2グループ以上に分かれる場合は分割して進める:

  • 異なる type の混在(例:docs と feat)→ type ごとに分割
  • 異なるトップレベルディレクトリの混在 → ディレクトリごとに分割
  • 同一 type・同一ディレクトリ → 原則1コミット

同じファイルが複数の type に同時に当てはまり、上記の規則で1グループに定まらないときだけ AskUserQuestion で確認してよい。

Phase 3:メッセージの生成

上記「コミットのルール」に従って組み立てる:

  1. 件名:type: 説明。説明は変更内容が伝わるよう簡潔に(目安30文字以内)。
  2. issue番号:件名末尾に (#番号) を付与する。番号はブランチ名等から推定し、不明な場合はユーザーに確認する。
  3. 本文(なぜ):本文を書くかどうかは上記「『なぜ』をどこに書くか」の表で判断する。書く場合はコードから読み取れない背景・判断理由を記す。決定記録にあたる決定を含むなら、上記「決定記録を残すコミット」の書式で書く。

Phase 4:ステージング対象の決定

  • Phase 1 で決定した範囲(デフォルト=work+stage全体、または work/stage 指定範囲)に含まれるソース・ドキュメント・設定・テストの各変更ファイルを対象に含める。単独セッションなら、セッション内で編集したかどうかは問わない。
  • 除外して警告表示:.env*, node_modules/, dist/・build/, .DS_Store/Thumbs.db, *.log。並行セッションを検出したときは、このセッションで Edit/Write していないファイルも除外して警告表示する。
  • デフォルト範囲かつ既にステージ済みのファイルは対象に含め、確認表示では ✓ を付ける。

Phase 5:コミット実行

ユーザー承認は求めず、自律的に実行する。グループごとに、次の手順を順に行う。

1. ステージする:Phase 4 で決めた対象ファイルのパスを明示して git add <パス>... する(-A や . を使わない)。

2. index を突き合わせる:ステージ済みの一覧を取り、Phase 4 の対象リストと一致するか確認する。

git diff --cached --name-only
  • 対象外のパスが混じっている:別セッションが staged にしたもの。git reset で外さない(相手の index を壊す)。手順3のパス指定でコミット対象から外れるので、除外したパスとして出力に列挙する
  • 対象のパスが出てこない:別セッションが先にコミットした可能性がある。git log --oneline -3 と git show --stat <hash> で取り込み済みか確かめ、済んでいればグループから外す

3. コミットする:対象パスを -- の後ろに並べ、index 全体ではなくそのパスだけをコミットする。

git commit -m "<件名>" -- <対象パス>...

パス指定を省くと、index に載った他セッションの変更ごとコミットされる。Phase 1 でスコープを正しく絞っても実行段階で巻き込むので、ここは AI の判断に委ねず常にパスで縛る。

このコマンドは index を経由せず作業ツリーの内容をコミットする。手順1のステージ後にファイルを直していると、index には古い内容が残り、次のコミットがそれを拾う。コミット後に git status --short を見て、対象ファイルが残っていたら git add <パス> で index を揃える。pre-commit hook が再生成して再ステージする docs/adr/ 配下のファイルは別で、コミット後に MM で残る。hook の git add が効くのは一時 index だけだからである。作業ツリーは hook が再生成した最新で HEAD と同じだから、git restore --staged <パス> で index だけを HEAD に戻す。

ファイルの一部だけをステージしているときは、pre-commit hook がそのファイルの prettier 整形と再ステージを見送る(git add <パス> が未ステージの変更ごとコミットに混ぜてしまうため)。整形の取りこぼしは、push 前の pre-push hook(npm run format:check)が止める。

=== Git Commit Summary ===

Commit Type: feat
Message: feat: アカウント同期処理の実装 (#42)

Committed Files:
  ✓ src/sync.ts

Excluded (warnings):
  ⚠ .env is ignored (environment file)
  ⚠ docs/guide/foo.html — 並行セッションを検出したため除外(このセッションで編集していない)

分割する場合は各コミットの Type・Message・対象ファイルを順に列挙する。

  • 複数コミットの途中で失敗(pre-commit hook 等)した場合、成功済みコミットはロールバックしない。失敗したグループはステージされたまま残し、原因と再実行を促す。
  • 完了後 git log --oneline -n <件数> で結果を表示する。

使用方法

/git-commit         # デフォルト:ワーキングツリー+staged全差分が対象
/git-commit work    # ワーキングツリー(未ステージ)のみ対象
/git-commit stage   # stagedのみ対象