git bisectで「いつ壊れたか」を探す:AIが何度も書き換えた履歴の調べ方
AIを使いこなすClaudeで執筆し、GPTで自動校閲・修正しています。

結論から言うと、「正常だった古いコミット」と「壊れている今のコミット」を1つずつ決め、不具合を再現するコマンドを用意して git bisect run に渡せば、原因コミットを機械的に絞り込めます。bisectは二分探索なので、スキップのない一本道の履歴なら、候補が1,000コミットあっても確認の回数は10回前後(2の10乗が1,024)で済みます。同じコミットでは同じ判定が出ることと、調べる範囲で正常から不具合ありへ変わり、その後は直っていないことが前提です。修正と再発が混在する場合は、調べる範囲を分けます。 AIにコードを何度も書き換えさせていると、「前は動いていたのに、どの修正で壊れたのか分からない」状況がよく起きます。AIに「どこが悪いか」を推測させるより、履歴を事実として検査するほうが確実です。この記事では、次の2つを実際に示します。 • bisectの進め方(準備から後片付けまで) • 良し悪しを自動判定する小さなスクリプトの雛形(再現用と判定用の2ファイル) コマンドの仕様は git bisect の公式ドキュメント で確認した内容に基づきます。コードは説明用の例で、私の環境では実行していません。ファイル名や関数名はご自分のプロジェクトに置き換えてください。
この記事の目次
1. 先に「再現」を1コマンドにする
bisectが判定できるのは、コマンドの終了コードだけです。そのため最初にやることは、不具合を「1つのコマンドで、正常なら成功、不具合なら失敗」にすることです。
git bisect run は、終了コードを次のように解釈します。
- 0、bisectの解釈:良い(正常)
- 1〜127(125を除く)、bisectの解釈:悪い(不具合あり)
- 125、bisectの解釈:判定不能。このコミットはスキップ
- 128以上、bisectの解釈:bisect自体を中止
注意したいのは、1〜127のほとんどが「悪い」になる点です。再現用プログラムが「ファイルが無い」「import に失敗した」といった無関係な理由で落ちると、それも「悪い」と誤判定されます。AIが途中で関数名やファイル構成を変えた履歴では、これが頻繁に起きます。そこで本記事の雛形では、「不具合そのもの」と「判定不能」を終了コードで分けます。
2. 再現スクリプトと判定スクリプトの雛形
次の2ファイルを、リポジトリの外(例: ~/bisect-tools/)に置きます。リポジトリ内でコミットしてしまうと、bisectが古いコミットへ移動したときにファイルごと消えるためです。
再現スクリプト(説明用の例)
「合計金額を計算する関数が、割引後の金額を返さなくなった」という架空の不具合を想定しています。Node.jsのESMで書いています。
// ~/bisect-tools/repro.mjs 不具合そのものは終了コード10、判定不能は20
import { pathToFileURL } from 'node:url';
import { join } from 'node:path';
let calcTotal;
try {
const mod = await import(pathToFileURL(join(process.cwd(), 'src/price.js')).href);
calcTotal = mod.calcTotal;
} catch (e) {
console.error('読み込み失敗:', e.message);
process.exit(20); // 判定不能
}
if (typeof calcTotal !== 'function') {
console.error('calcTotalが存在しない');
process.exit(20); // 判定不能
}
// 期待値: 100円x2個を10%引きで180
const got = calcTotal([{ price: 100, qty: 2 }], 0.1);
if (got === 180) process.exit(0);
console.error(`期待値180, 実際${got}`);
process.exit(10); // 不具合を再現既存のテストで済む場合は、node --test を使う方法もあります。Node.jsのテストランナーは、テストが1つでも失敗すると終了コード1で終わります。ただし、下の判定スクリプトは終了コード10だけを「悪い」に変換するため、コマンドを単純に置き換えるとテスト失敗もスキップされます。利用する場合は、対象の不具合による失敗を1、無関係な失敗を125として返すように判定処理も変更してください。テストファイル自体が古いコミットに存在しない場合も判定が狂うので、その場合は外部に置いた再現スクリプトのほうが安全です。
判定スクリプト
#!/bin/sh
# ~/bisect-tools/check.sh git bisect run から呼ばれる
HERE=$(cd "$(dirname "$0")" && pwd)
cd "$(git rev-parse --show-toplevel)" || exit 128
# 依存の入れ直しが必要なプロジェクトだけ有効にする
# [ -f package-lock.json ] || exit 125
# npm ci --silent || exit 125
node "$HERE/repro.mjs"
status=$?
case $status in
0) exit 0 ;; # 良い
10) exit 1 ;; # 悪い(不具合を再現)
*) exit 125 ;; # それ以外は判定不能としてスキップ
esacポイントは最後の case です。「再現できた」場合だけ悪いと判定し、それ以外の失敗は全部スキップに回します。ビルドが必要なら、make || exit 125 のようにビルド失敗を125にする書き方が公式ドキュメントの例にもあります。
3. 良いコミットを決めて、bisectを走らせる
作業ツリーを片付ける
bisectはコミットを次々にチェックアウトします。未コミットの変更が残っていると切り替えに失敗したり、変更を巻き込んだりするので、事前にコミットか退避をしておきます。
git status # 変更がないことを確認
git stash # 残っていれば退避(必要なら -u も)「良い」コミットを探す
「たしかに動いていた」コミットを、推測ではなく実際に確かめて選びます。履歴を眺めて見当をつけ、そこへ移動してスクリプトを走らせます。
git log --oneline | head -50
git switch --detach <古そうなコミット>
sh ~/bisect-tools/check.sh; echo $? # 0なら良い
git switch - # 元のブランチへ戻る0が返ったコミットが見つかれば、それが「良い」の基準です。どこまで遡っても0にならない場合は、環境や依存関係の変化のほか、古い構成に再現スクリプトが対応していない可能性もあります。終了コードとエラー内容を確認し、正常な基準を確保できるまではbisectを始めません。
実行する
元のブランチへ戻ったら、sh ~/bisect-tools/check.sh; echo $? を実行し、今のコミットでは1が返ることも確認します。125なら判定不能なので、再現スクリプトや環境を見直します。
git bisect start HEAD <良いコミット>
git bisect run sh ~/bisect-tools/check.shgit bisect start <bad> <good> の形で、悪い側と良い側を同時に指定できます。候補を1つに絞れれば「最初の悪いコミット(first bad commit)」が表示されます。スキップした位置によっては、複数の候補が残ります。
結果を保存して戻る
git bisect log > ~/bisect-tools/bisect-log.txt # 経過を保存
git show <表示されたコミット> # 変更内容を確認
git bisect reset # 開始前の状態に戻るgit bisect log で経過を残しておけば、git bisect replay で状態を再現できると公式ドキュメントにあります。AIへの相談材料にもなります。終わったら必ず reset してください。
4. 判断表:途中で困ったとき
- 「最初の悪いコミット」がビルド設定や無関係なファイルの変更だった、原因の見立て:再現スクリプトが別の理由で落ちて、悪いと誤判定された、対処:判定スクリプトで想定外の失敗を125にしているか見直す
- スキップが多く、候補が絞り切れない、原因の見立て:AIの途中コミットに、動かない中間状態が多い、対処:手動なら git bisect skip。結果が「複数の候補のどれか」になる場合は、その範囲を git show で順に調べる
- マージコミットが原因と出る、原因の見立て:統合時の衝突解消で壊れた可能性、対処:git bisect start --first-parent で本流だけをたどる方法がある(公式ドキュメント参照)。マージ前の枝の側も別途確認
- 巨大な1コミットが原因と出る、原因の見立て:AIが一度に大量を書き換えた、対処:そのコミットの差分をファイル単位・関数単位に分けて、AIに範囲を絞って見せる
- 良いコミットを指定したのにエラーになる、原因の見立て:良いと悪いの前後関係が逆、または取り違え、対処:git bisect reset からやり直す。「良い」は実際にスクリプトで0を確認したものだけにする
- 古いコミットで依存パッケージが合わない、原因の見立て:当時の依存関係が現在と違う、対処:package-lock.json があるプロジェクトでは、判定スクリプトの依存インストール用の2行を有効にし、失敗は125にする
npm ci は、そのコミットのロックファイルに基づいて依存を入れ直します。既存の node_modules は削除されます。ロックファイルがない履歴や、当時とNode.js・npmの条件が違う場合は、プロジェクトに合う環境と復元手順を別途用意してください。
「不具合が直った瞬間」を探したい場合は、git bisect start --term-old broken --term-new fixed のように用語を変えられます。用語を変えても、git bisect run の終了コード0はold側(ここではbroken)、1はnew側(fixed)を意味します。本記事の判定スクリプトは正常なら0なので、そのまま使わず、修正前を0、修正後を1、判定不能を125に変更してください。手動なら git bisect broken と git bisect fixed で判定します。
5. 結果をAIに渡して直してもらう
原因コミットが分かったら、ここで初めてAIの出番です。履歴全体を渡すのではなく、絞り込んだ事実だけを渡すと、修正案の精度が上がりやすくなります。次は貼り付けて使える依頼文の例です。
次の不具合の修正案を出してください。
【症状】calcTotal([{price:100, qty:2}], 0.1) が180を返すべきなのに、現在は 200 を返す
【再現コマンド】リポジトリのルートで node ~/bisect-tools/repro.mjs(終了コード10で不具合)
【原因コミット(git bisectで特定)】<コミットID>
【そのコミットの差分】
<git show の出力を貼る>
お願い:
1. 差分のどの変更が症状に結びつくか、根拠つきで説明してください
2. 元の意図(機能追加など)を壊さない最小の修正案を出してください
3. 修正後に再現コマンドが成功するか、私が確認する手順も書いてください人が確認するポイントは3つです。
- 修正後に node ~/bisect-tools/repro.mjs の終了コードが0になるか(AIの「直りました」を信じず自分で実行する)
- 原因コミットの本来の目的(その変更で入れたかった機能)が失われていないか
- 再発防止のテストが追加され、通常のテスト実行に含まれているか
なお、コミットログやdiffに、パスワードや顧客データなどの機密が含まれていないかは、AIに貼る前に確認してください。必要なら架空の値に置き換えてから渡します。
6. 次回に備えて
AIに作業させるときは、1つの修正ごとにコミットするよう指示しておくと、次のbisectの精度が上がります。1コミットに大量の変更が詰め込まれていると、最初の悪いコミットまで特定できても、その中身を調べ直すことになります。コミット前に再現スクリプトまたはテストを走らせる運用にしておけば、そもそも壊れた状態が履歴に入りにくくなります。

