ある朝、ログイン周りの不具合を直してほしいとだけ伝えて Antigravity に任せたところ、戻ってきたのは 9 ファイル・約 420 行の差分でした。読み始めて気づいたのは、認証ミドルウェアの修正だけでなく、ついでに型定義の整理、未使用 import の削除、エラーメッセージの文言調整までが、ひとつの塊に混ざっていたことです。
どれも妥当な変更でした。けれど、これをそのまま git commit -am "fix login" で固めてしまうと、後から「文言調整だけ戻したい」と思っても、認証の修正ごと巻き戻すしかなくなります。レビューする側としても、420 行を一息に追うのは集中力が持ちません。結局その差分は、私が分け直すまで半日ほど宙に浮いたままになりました。
エージェントは関心を分けて書いてはくれません。一度に解けるものは一度に書きます。だからこそ、書かせたあとに「意味の単位へ分け直す」工程を自分の手元に持っておくことが、個人開発でも効いてきます。今日はそのやり方を、実際に使っているスクリプトとともに共有します。
なぜ「一括コミット」が後で重荷になるのか
巨大なコミットの問題は、見た目の読みにくさだけではありません。本質的に困るのは次の三つで、私はこの三点を分割の判断基準として推奨しています。
巻き戻しの粒度が粗くなること。git revert はコミット単位でしか働きません。機能追加とリファクタリングが同じコミットに入っていると、片方だけを取り消す手段が事実上なくなります。
レビューが形骸化すること。人間の注意力には限りがあります。一度に 400 行を見せられると、最初の数十行は丁寧に読んでも、後半は「たぶん大丈夫だろう」と流し読みになりがちです。私自身、自分が書いたコードですら、まとめてレビューしようとすると見落としが増えます。
git bisect が効きにくくなること。不具合の原因コミットを二分探索で特定するとき、ひとつのコミットに複数の関心が同居していると、犯人を絞り込めても「このコミットのどの変更が原因か」が分からないままになります。
これらは個人開発でも例外なく効いてくる落とし穴です。だからこそ早めに回避策を持っておきたいところです。
観点 一括コミット 意味単位コミット
巻き戻し 全部まとめてしか戻せない 関心ごとに revert できる
レビュー 後半が流し読みになりやすい 1件が小さく集中して読める
原因特定 bisect で犯人コミットは出ても中身が混在犯人コミット=犯人の変更
履歴の意味 「fix login」など曖昧 各コミットが意図を語る
理想は「1 コミット 1 関心」です。とはいえ、エージェントに最初からそう書かせるのは現実的ではありません。修正の過程で関心は自然に絡み合うからです。ですから、私は「書かせるときは自由に、コミットするときに分ける」と割り切っています。
まず素手でできること — git add -p
分割の基本は、Git に標準で備わっている対話的ステージングです。git add -p は差分を hunk(変更のかたまり)単位で見せ、ひとつずつ採否を選ばせてくれます。
# 作業ツリーの変更を hunk 単位で確認しながらステージング
git add -p
採否のプロンプトでは、y(この hunk を含める)、n(含めない)、s(さらに小さく分割する)、e(手で編集する)がよく使うキーです。認証の修正だけを y で拾い、文言調整は n で見送り、まず認証ぶんだけコミットする、という流れです。
# 認証関連だけをステージしたあとでコミット
git commit -m "fix(auth): セッション Cookie の SameSite 属性を修正"
# 残りの変更を次の関心としてステージし直す
git add -p
git commit -m "refactor: 認証ミドルウェアの型定義を整理"
この素手の方法は確実ですが、9 ファイルにまたがる 420 行を一 hunk ずつ捌くのは骨が折れます。s で分割しても、関連する hunk が別々のファイルに散っていると、どれとどれが同じ関心なのかを頭の中で覚えておく必要があります。ここを Antigravity に手伝わせるのが、今日の本題です。
Antigravity に「関心ごとの仕分け」だけをさせる
ポイントは、エージェントにコミットそのものを任せないことです。任せるのは「この差分は、いくつの関心に分けられるか。各 hunk はどの関心に属するか」という分類だけにします。コミットの実行は、検証可能な形で自分の手元に残します。
まず、機械が扱いやすい形で差分を取り出します。
# ステージ前の全変更を、ファイルパスつきで取得
git diff > /tmp/work.diff
# 変更のあったファイル一覧も添える
git diff --stat
そのうえで、Antigravity に次のような出力契約を明示して投げます。曖昧な依頼ではなく、戻り値の形を固定するのが肝心です。
添付の git diff を、レビュー単位として意味のある「関心」に分類してください。
制約:
- 各関心には、命令形の短い日本語タイトルと Conventional Commits の type を付ける
- 各 hunk を必ずいずれか1つの関心に割り当てる(重複・取りこぼし禁止)
- リファクタリング → 機能修正 → テスト → 文言・整形 の順に並べる
- 相互依存する hunk(同じ関数の同じ箇所)は同じ関心にまとめる
出力は次の JSON のみ。説明文は不要:
{
"commits": [
{ "type": "refactor", "title": "...", "files": ["path:行範囲", ...] }
]
}
出力を JSON に固定しておくと、次の工程でそのまま機械処理に渡せます。私は最初、自由形式で「いい感じに分けて」と頼んでいましたが、説明が長くなるばかりで再利用できませんでした。型を決めてから、ぐっと安定しました。
なぜ並び順を「リファクタリング → 機能 → テスト → 整形」と指定するかというと、レビュアーがこの順で読めると、変更の意図を追いやすいからです。土台を整える変更を先に、振る舞いを変える変更を後に置く。これは人間がコードを書くときの自然な思考順でもあります。
分類結果をもとに、束ねてコミットする
分類が手に入ったら、関心ごとに hunk を選んでコミットしていきます。完全自動にすると誤割り当てを見逃すので、私は「1 関心ぶんをステージ → 中身を目視 → コミット」という半自動の刻みにしています。
ファイル単位で関心が分かれている場合は、git add にパスを渡すだけで十分です。
# 関心1: リファクタリング(型定義の整理)
git add src/types/auth.ts src/lib/session.ts
git diff --cached --stat # ステージ内容を必ず目視
git commit -m "refactor(auth): セッション型を1ファイルに集約"
# 関心2: 機能修正(本丸のバグ修正)
git add src/middleware/auth.ts
git diff --cached
git commit -m "fix(auth): SameSite=Lax で Cookie が送られない問題を修正"
同じファイルの中で関心が混ざっている場合は、git apply --cached にパッチを部分適用する方法が確実です。Antigravity に「この関心に属する hunk だけを含む有効な unified diff を出して」と頼み、それを当てます。
# エージェントが出力した部分パッチを保存して、インデックスにのみ適用
cat /tmp/concern-3.patch | git apply --cached
git diff --cached # 意図した hunk だけが入っているか確認
git commit -m "style: ログインエラーの文言を統一"
# 作業ツリー側に残った変更は次の関心へ
git apply --cached は作業ツリーを変えず、インデックス(ステージ領域)にだけパッチを当てます。もし当たらなければ、その時点で error: patch failed と教えてくれるので、壊れた部分適用を黙ってコミットしてしまう事故を防げます。
コミット1件ごとにビルドを通す検証ゲート
分割で一番怖いのは、「途中のコミットだけ取り出すとビルドが壊れる」状態です。たとえば型定義の移動を先にコミットしたのに、その型を使う側の修正が後のコミットに回ると、最初のコミット単体ではコンパイルが通りません。git bisect はまさに「途中のコミット」を取り出すので、これは実害になります。
そこで、各コミットの直後に最小限の検証を挟みます。
# 直近コミットの状態で型チェックと lint を走らせる小さなゲート
npm run typecheck && npm run lint
if [ $? -ne 0 ]; then
echo "⚠️ このコミット単体で検証が通りません。hunk の割り当てを見直してください"
fi
すべての分割が終わったあとに、各コミットを順に取り出して検証することもできます。git rebase の --exec を使うと、コミットごとにコマンドを自動実行できます。
# 分割した一連のコミットそれぞれで typecheck を走らせる
git rebase -i HEAD~4 --exec "npm run typecheck"
途中のコミットで失敗したら、rebase が止まります。その地点で hunk の順序を入れ替えるか、依存する変更を前のコミットへ寄せれば、各コミットが自立した状態になります。私はここで初めて「型の移動と利用側の修正は、本当は同じ関心だった」と気づくことがよくあります。分割の検証が、関心の境界そのものを教えてくれるのです。
相互依存する hunk をどう扱うか
分割が必ずしも綺麗にいくわけではありません。よく出くわすのが、フォーマッタの巻き込みです。エージェントが 1 行直したついでに、保存時整形でファイル全体のインデントが変わり、差分が膨らむことがあります。この場合、整形だけを別コミットに切り出すより、整形を一度元に戻してから本質的な変更だけを残すほうが、結局きれいになります。
# フォーマッタの巻き込みが激しいファイルは、いったん戻して手当てし直す
git checkout -- src/components/LoginForm.tsx
# 本質的な変更だけを Antigravity に最小差分で書き直してもらう
もうひとつは、同じ関数の同じ箇所に二つの関心が重なるケースです。これは物理的に hunk を割れません。無理に割らず、ひとつのコミットにまとめたうえで、コミットメッセージの本文に「この変更には X と Y が含まれる」と正直に書きます。分割の目的はレビューしやすさであって、コミット数を増やすことではないからです。
判断の目安を整理すると、次のようになります。
状況 とる対応
関心がファイル単位で分かれている git add <path> で素直に分割
同一ファイル内で関心が混在 git apply --cached で部分適用
フォーマッタの巻き込みで差分が膨張 いったん checkout で戻して書き直す
同じ箇所に二つの関心が物理的に重複 無理に割らず1コミットにし、本文で明記
手元に残す小さなまとめスクリプト
最後に、ここまでの流れを毎回手で組むのは面倒なので、私は差分の取得と分類依頼までを一本のスクリプトにしています。コミット自体は目視を挟みたいので、あえて自動化していません。
#!/usr/bin/env bash
# split-prep.sh — 差分を取り出し、分類用の素材を整える
set -euo pipefail
OUT = "/tmp/split-$( date +%s)"
mkdir -p " $OUT "
git diff > " $OUT /work.diff"
git diff --stat > " $OUT /stat.txt"
if [ ! -s " $OUT /work.diff" ]; then
echo "ステージ前の変更がありません。先に作業ツリーへ変更を残してください"
exit 1
fi
echo "差分を $OUT /work.diff に出力しました($( wc -l < " $OUT /work.diff") 行)"
echo "この内容を Antigravity に渡し、関心ごとの JSON 分類を受け取ってください"
echo "分類後は git add / git apply --cached で1関心ずつコミットします"
このスクリプトは差分を素材として整えるだけで、判断は人間に残します。エージェントに書かせる速さと、自分でレビューできる粒度。その両方を手放さないための、ささやかな仕掛けです。
巨大な差分が返ってきたとき、そのまま固めるか、いったん分け直すか。その一手間が、三ヶ月後に「あの変更だけ戻したい」と思ったときの自分を助けてくれます。まずは次にエージェントから大きな差分を受け取ったら、git add -p をひとつ試してみてください。分け直すという発想が手に馴染むと、コミット履歴が自分の味方になっていきます。