Claude Codeでコミットを分割する — git add -pを使わず1コミット1目的に切り直す
混ざった変更をgit resetで戻し、git diff -U0とgit apply --cachedでhunk単位に切り直す手順。分割ごとのテスト確認と、通し確認のrebase --execまで実機で確かめた出力つきで書きます。
機能追加とフォーマット修正が1つのコミットに混ざったとき、Claude Codeに頼むと「git add -pで選んで分けて」という方針になりがちです。しかしgit add -pはhunkごとにyかnを答えさせる対話コマンドで、人が画面を見ながら使う前提の作りになっています。
この記事では、git add -pを使わずに、戻す→差分を取る→欲しいhunkだけをindexに入れる→テストする、の順で切り直す手順を書きます。コマンドの出力は、Git 2.50.1の小さな検証用リポジトリで実際に走らせたものです。
git add -pをClaudeに任せると何が起きるか
git add -pの公式の説明は「作業ツリーとindexの差分のhunkを対話的に選び、indexに足す」です。1つのhunkごとに、y(入れる)・n(入れない)・s(さらに分割)・e(手で編集)などを聞いてきます。
標準入力にまとめて答えを流し込めば動くので、「できない」わけではありません。試すと、別の問題が見えました。calc.pyにmul関数の追加と、fmtの書式変更という2つの目的の変更が入っているファイルで、yとnを流してみた結果です。
printf 'y\nn\n' | git add -p calc.py
git diff --cached --stat(1/1) Stage this hunk [y,n,q,a,d,s,e,p,?]?
calc.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)プロンプトは(1/1)、つまりhunkが1つしかありませんでした。2つの変更の間が近いため、既定の文脈行(変更の前後に付く行)で1つのhunkにまとまっていたのです。yで全部が入り、nは読まれていません。目的を切り分けるつもりが、2つ分をまとめてステージしていました。
hunkが何個に分かれるかは、実際に差分を出すまで分かりません。答えを先に決め打ちして流し込む方式は、ここで破綻します。
混ざった変更をhunk単位で選ぶ3つの方法
ファイル単位で入れる
目的ごとにファイルが分かれているなら最も簡単です。同じファイルに2つの目的が入っていると使えません。
git add -pに答えを流す
hunkの数と順序を先に知らないと答えを書けません。上の実測のとおり、近い変更は1つのhunkになります。
パッチを作ってgit apply --cached
欲しい行だけを書いたパッチを、作業ツリーに触れずindexにだけ当てます。hunkの中身も自分で削れます。
手順の前に退避と許可を決める
切り直しはgit resetでindexを動かす操作を含みます。先に戻れる場所を作ってから始めます。
Claude Codeの/rewindは、Claudeが編集ツールで触ったファイルに対して、その後のBashコマンドの変更も戻せます。ただし、Bashコマンドだけが変えたファイルは追跡されず、戻すにはGitなどのバージョン管理を使う、と公式のチェックポイントのページに書かれています。git resetの周辺はこの後者にあたるので、退避はGitで取ります。
git branch backup/before-splitブランチを1本切るだけで、切り直しに失敗してもgit reset --hard backup/before-splitで元の状態に戻せます。
承認の回数を減らしたいなら、.claude/settings.jsonで必要なサブコマンドだけを許可します。権限ルールはBash(git commit *)のように、サブコマンドの後ろに*を置く書き方です。*をサブコマンドの前に置くと起動時に警告が出ます。
{
"permissions": {
"allow": [
"Bash(git reset *)",
"Bash(git apply *)",
"Bash(git commit *)"
],
"deny": [
"Bash(git push *)"
]
}
}切り直しの途中でpushされると取り返しがつかないので、git push *は拒否のままにします。公式のページにも、git pushで始まる呼び出しを拒否する同じ形の設定例があります。
git resetで未コミットの状態に戻す
分割したい変更が、まだコミットされていない場合は、ここは飛ばして次へ進みます。
すでに1つの大きなコミットになっているなら、そのコミットの手前までindexを戻します。--mixedが既定で、作業ツリーはそのまま、indexだけをHEADに揃えます。
git reset HEAD~1
git status --short数コミットにまたがる混在なら、HEAD~1をまとめ直したい範囲の手前(ベースのコミット)に替えます。--softは変更をステージしたまま戻すので、分け直す目的には向きません。ステージ済みの束から選び直す手間が増えます。
差分をhunk単位のパッチに取り出す
Claudeに分割の計画を書かせるところから始めます。計画を先に出させると、パッチを作る前に目的の切れ目を人が確認できます。
git diffを読んで、変更を1コミット1目的に分ける計画を作って。
コミット単位ごとに、含めるファイルとhunkを書いて。
git add -pは使わない。まだコミットはしないで。計画に同意したら、文脈行ゼロの差分をファイルに出します。-U0は変更行だけのhunkを作るオプションで、近い変更が1つのhunkにまとまる問題を避けられます。
git diff -U0 > all.patch検証用のリポジトリでは、次のようなパッチになりました。calc.pyの2つの変更が別々のhunkに分かれ、git add -pでは見えなかった切れ目が出ています。
diff --git a/calc.py b/calc.py
index 308eecf..36f036e 100644
--- a/calc.py
+++ b/calc.py
@@ -8,0 +9,4 @@ def sub(a, b):
+def mul(a, b):
+ return a * b
+
+
@@ -10 +14 @@ def fmt(x):
- return str(x)
+ return "{:.2f}".format(x)
diff --git a/test_calc.py b/test_calc.py
index 1428da4..258a77a 100644
--- a/test_calc.py
+++ b/test_calc.py
@@ -1 +1 @@
-from calc import add, sub, fmt
+from calc import add, sub, fmt, mul
@@ -4 +4,2 @@ assert sub(3, 1) == 2
-assert fmt(5) == "5"
+assert fmt(5) == "5.00"
+assert mul(2, 3) == 6見るべきは最後のhunkです。fmtのテスト修正とmulのテスト追加が1つのhunkに同居しています。これは行単位で分ける必要があります。
1コミット目のパッチだけをindexに当てる
最初のコミットは、mulの追加とそのテストだけにします。all.patchからcalc.pyの1つ目のhunkと、test_calc.pyの2つのhunkのうちmulの部分だけを残し、p1.patchとして作ります。同居していた最後のhunkは、fmtの行を落としてmulの行だけを追加する形に書き換えます。
@@ -4,0 +5 @@ assert sub(3, 1) == 2
+assert mul(2, 3) == 6hunkヘッダの-4,0 +5は「元の4行目の後ろに、新しい5行目として1行を足す」の意味です。行を削ったら、この数字も合わせて直す必要があります。数字がずれたパッチは当たりません。
パッチはindexにだけ当てます。--cachedは作業ツリーに触れず、indexだけを更新するオプションです。
git apply --cached --unidiff-zero p1.patch
git diff --cached --stat calc.py | 4 ++++
test_calc.py | 3 ++-
2 files changed, 6 insertions(+), 1 deletion(-)--unidiff-zeroが必要なのは、-U0で作ったパッチが文脈行ゼロだからです。git applyは既定で、少なくとも1行の文脈を持つ差分を前提にしています。文脈なしのパッチは、当て先の位置がずれたときの安全装置が働かないので、公式は勧めていません。パッチは必ず直前に作り直し、古いall.patchを使い回さないでください。
落とし穴
パッチを切り出すとcorrupt patchで落ちる
all.patchを行番号で切り出したとき、最初の試行は次のエラーで止まりました。
error: corrupt patch at line 11
原因は、次のhunkのヘッダ行まで切り出しに含めてしまい、中身のない@@が残ったことです。hunkごとに、ヘッダ行と、そのヘッダが宣言した行数ぶんの本文だけを数えて切ります。Claudeに切り出しを任せるときは、git apply --check p1.patchで当たるかを確かめてから本番を実行させると、indexを汚さずに済みます。
ステージした状態でテストを走らせる
ここで一つ気をつけます。作業ツリーにはまだfmtの変更が残っているので、そのままテストを走らせると、これからコミットする内容とは違う状態を検査してしまいます。
indexの中身だけを別ディレクトリに書き出して、そこでテストします。
rm -rf /tmp/idx
git checkout-index -a --prefix=/tmp/idx/
cd /tmp/idx && python3 test_calc.pygit checkout-index -aは、indexにあるファイルを全部書き出します。--prefixで出力先を指定できるので、作業ツリーは動きません。検証用のリポジトリでは、1コミット目のテストが成功しました。ビルドが重いプロジェクトでは、この確認がそのまま時間のかかる工程になるので、テスト対象を絞る指示もClaudeに添えます。
通ったらコミットします。
git commit -m "add: mul関数を追加"2つ目以降は、git diff -U0を撮り直すところから繰り返します。1コミット目を入れた後は、残りのhunkの行番号がずれるためです。残りが1つの目的だけになったら、git addでファイルごと入れて構いません。コミットメッセージの書式を揃えたい場合は、commitlintの設定で規約違反を差し戻せます。
分割後の全コミットを通しで検査する
各コミットを切るたびにテストしていても、最後に履歴全体を通しで確かめる価値があります。git rebase --execは、各コミットを作り直した直後に指定のコマンドを走らせます。公式の説明では、失敗したコマンドがあるとrebaseは中断し、終了コード1を返します。
公式の例はgit rebase -i --execの形で、-iはtodo一覧をエディタで開きます。GIT_SEQUENCE_EDITORにエディタを指定すると既定より優先されるので、trueを前置して編集を空振りさせます。Claudeが対話なしで実行するための工夫です。なお、手元のGit 2.50.1では -i を付けない git rebase --exec だけでも編集画面は開かず、そのまま最後まで走りました。
GIT_SEQUENCE_EDITOR=true git rebase --exec "python3 test_calc.py" HEAD~2Rebasing (2/4)Executing: python3 test_calc.py
Rebasing (3/4)Rebasing (4/4)Executing: python3 test_calc.py
Successfully rebased and updated refs/heads/main.2つのコミットそれぞれの直後にテストが走り、最後まで通りました。途中で落ちた場合は、落ちたコミットでrebaseが止まるので、そのコミットに修正を足してgit rebase --continueで続けます。
出来上がった履歴は、git log --onelineの出力をClaudeにパイプして読ませ、目的が1コミット1つに収まっているか見てもらえます。公式の共通ワークフローのページには、git log --oneline -20 | claude -p "summarize these recent commits"の例があります。
git log --oneline -5 | claude -p "各コミットの目的を1行ずつ要約して。2つ以上の目的が混ざっているものがあれば指摘して"要約に「と」で繋がる目的が現れたコミットは、まだ混ざっています。
どの切り方を選ぶか
目的ごとにファイルが分かれているなら、git add パスで足ります。パッチを書く必要があるのは、同じファイルに2つ以上の目的が入っているときだけです。
git reset HEAD~1は、まだpushしていないコミットに使います。pushを済ませたコミットの履歴を書き換えると、共有ブランチを使う他の人に影響が出ます。その場合は新しいコミットで直し、今回の方法は次のブランチから使います。
分割に使う基準は、巨大関数を切り出すときの1コミット1抽出と同じ考え方です。巨大関数の分割の記事では、抽出ごとにコミットを区切る基準を扱っています。切り終えたら、公式の共通ワークフローのページにあるとおり「create a pr」と頼めば、Claudeが変更の要約からPR作成まで進めます。コミット作成からPR作成までを1コマンドにしたい場合は、commit-commandsプラグインが使えます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。