「1ヶ月前は動いていたのに、いつの間にかバグっている」——数百〜数千コミットに及ぶ履歴の中から、バグを混入した犯人コミットを1つずつgit checkoutして確認するのは現実的ではありません。
git bisectは二分探索でこの問題を解決する標準装備の調査ツール。1000コミットでも最大10回のテストで犯人が確定します(log₂(1000)≒10)。正しく使えるかどうかで「1日かけても原因不明」と「30分で特定+回帰テスト追加」の差になります。
この記事では、bisectの数学的バックグラウンドから基本フロー、bisect runによる完全自動化、exit code 125でのskip戦略、monorepoやmerge多数履歴での--first-parentモード、path指定で範囲を絞るテクニック、bisect log/replayによる再現可能デバッグ、worktreeを使った並行bisect、性能リグレッションのbisect、フレークテスト・submodule・Docker環境での運用、そしてCI統合まで、2026年の実務で使い倒すための完全ガイドとしてまとめます。
この記事で学べること
- bisectの数学的原理(O(log₂N)の裏付けと必要テスト回数予測)
git bisect start / good / bad / resetの基本5コマンドgit bisect runで完全自動化する終了コード規約(0/1-127/125/128+)- 判定不能コミットを
skipで飛ばす戦略 --first-parentでmonorepo・merge多数履歴をPR単位で絞り込むgit bisect start -- path/でファイル影響範囲のみ二分探索git bisect log/replayで探索過程を再現・共有git worktree併用で本作業を止めずに並行bisect- 性能リグレッション(ベンチマーク比較)bisectのセットアップ
- フレークテスト・環境依存バグの扱い方(Docker・リトライ)
- CI統合:PR経由で自動bisectを回すワークフロー
bisectの原理と必要テスト回数の予測
bisectは二分探索です。N個の候補コミットから犯人を特定するのに必要なテスト回数は最悪 ⌈log₂(N)⌉ 回。1024コミットでも10回、1,000,000コミットでも20回しかかかりません。
実用的な結論:「直近1ヶ月で壊れた」が分かっていれば、普通のプロジェクトなら10〜15分でbisectが終わる。goodを決める段階でタグ(v1.2.0など)や先月のmainを指定するのがコツ。古すぎるgoodを選ぶとビルド失敗が頻発してskipだらけになります。
bisect中の進捗メッセージを読む
$ git bisect bad Bisecting: 512 revisions left to test after this (roughly 9 steps) [a1b2c3d] feat: 新機能追加 # "roughly 9 steps" が残りの最大テスト回数 # → あと9回テストで犯人が決まる
基本フロー:5コマンドで完結
# ① 開始(現在のHEADを基点に) git bisect start # ② 現状を"bad"(バグあり)としてマーク git bisect bad # ③ 正常だった時点を"good"として指定 git bisect good v1.2.0 # タグ指定 git bisect good HEAD~200 # 相対指定 git bisect good 3a8b2c1 # SHA指定 # Gitが自動で中間コミットへチェックアウト # → ここでアプリ起動/テストして再現確認 # ④ 再現したら bad、再現しなければ good git bisect bad # or git bisect good # Gitがまた次の中間点へ移動 → 繰り返し # … # a1b2c3d is the first bad commit ← 犯人判明 # ⑤ 終了(HEADを元の位置に戻す) git bisect reset
開始時に一気に指定する短縮形
# 1コマンドで開始(bad=現在、good=v1.2.0) git bisect start HEAD v1.2.0 # Git 2.29+ の新構文(より明示的) git bisect start --term-new=broken --term-old=working HEAD v1.2.0
用語をカスタマイズできる(–term-new/–term-old)
- “good/bad”はバグ特定の発想の言葉
- 機能が壊れたのではなく性能が劣化した/仕様が変わった調査なら
--term-new=slow --term-old=fastのようにカスタム可能 - 以降は
git bisect slow/git bisect fastでマーク old/newがデフォルトでも使える(good/badと同義)
git bisect runで完全自動化:終了コード規約
テストスクリプトで再現判定できるなら、bisect runで人間の介入ゼロ。夜間に回せば朝には犯人が確定しています。鍵はスクリプトの終了コードです。
#!/usr/bin/env bash # scripts/bisect_test.sh set -u # ① 依存インストール(失敗したらskip) if ! npm ci --silent; then exit 125 # このコミットでは判定不可 fi # ② ビルド(失敗もskip) if ! npm run build --silent; then exit 125 fi # ③ 再現テスト(失敗=bad、成功=good) npm test -- --testNamePattern="bug_X" --silent # npm testの終了コードをそのまま返す
chmod +x scripts/bisect_test.sh git bisect start git bisect bad git bisect good v1.2.0 git bisect run ./scripts/bisect_test.sh # 出力例 # running ./scripts/bisect_test.sh # Bisecting: 250 revisions left to test... # ... # a1b2c3d4e5f6... is the first bad commit # bisect run success
スクリプトのset -eはbisect runでは罠になります。依存インストール失敗でスクリプト全体が終了コード1で終わると、Gitが“バグあり”と誤判定しbad側に進んでしまう。ビルド失敗は必ずexit 125を明示的に返すことが鉄則。
skip戦略:判定不可コミットを飛ばす
「このコミットはビルドが壊れている」「テスト環境が動かない」など再現判定できないケースに遭遇したらbisect skip。bisectは該当コミットを飛ばして近傍を試します。
# 手動でskip git bisect skip # skip範囲を指定 git bisect skip HEAD~5..HEAD # 自動スクリプトでskipを返す exit 125 # → Gitが自動で別のコミットを試す
skipが多発した場合
skipだらけになると結論が “first bad commit could be any of”となり犯人が範囲内のどれか分からない状態で終わります。この場合:①goodを新しめに変更(壊れた古いコミット範囲を除外)、②Dockerで固定環境を用意、③path指定で範囲を絞る、のいずれかで対処。
# skipが多くて範囲確定する $ git bisect bad The first bad commit could be any of: a1b2c3d feat: 新機能 d4e5f6c refactor: ユーティリティ整理 9a8b7c6 fix: typo We cannot bisect more! # これら3つの中に犯人がいる → 手動で1つずつ確認 git show a1b2c3d # 差分チェック git show d4e5f6c git show 9a8b7c6
–first-parentでmerge多数履歴をPR単位に絞る
monorepoや頻繁にPRがmergeされるリポジトリでは、featureブランチ内の中間コミットまで全部bisectするとビルド不能なWIPコミットだらけでskipの嵐になります。
Git 2.29+(2020年10月以降)から使える--first-parentはmergeの第一親(通常はmainの履歴)のみを辿る機能。PR単位でbisectできるため、monorepoでは実質必須の設定です。
# Git 2.29+ で使えるfirst-parentモード git bisect start --first-parent git bisect bad HEAD git bisect good v1.2.0 # → mainのマージコミット系列だけを二分探索 # → PR単位で犯人が判明 # → 該当PRが分かれば、そのPR内で再度bisectを回せる
多段bisect:PR特定→PR内bisect
# Step1: first-parentで犯人PRを特定 git bisect start --first-parent HEAD v1.2.0 git bisect run ./scripts/bisect_test.sh # → mergeコミット M123 が犯人と判明 # Step2: PR内部で通常のbisect git bisect reset git bisect start git bisect bad M123 git bisect good M123^ # マージ直前 git bisect run ./scripts/bisect_test.sh # → PRのどのコミットでバグが入ったかまで特定
monorepoではこれが黄金パターン。全履歴を一気にbisectせず、first-parentで”どのPR”まで絞ってから内部を詳しく調べるとビルド失敗によるskipが激減します。mergeとrebaseの違いについては【Git】rebaseとmergeの違いと使い分け完全ガイドも参照。
path指定で影響範囲を絞り込む
git bisect start -- path/で特定ファイル・ディレクトリを変更したコミットだけを候補に絞れます。「画像処理モジュールのバグ」と分かっているのに全コミットをbisectするのは無駄。
# 特定ディレクトリのみ git bisect start -- src/image-processor/ git bisect bad git bisect good v1.2.0 # 複数path指定 git bisect start -- src/image-processor/ src/filters/ # 特定ファイルのみ(精密) git bisect start -- src/image-processor/core.js # 除外指定(例:テスト以外のコミットに絞る) git bisect start -- . ":(exclude)*.test.js"
path指定の威力
1000コミットでも特定モジュールに手を入れたのが50コミットだけなら、bisect対象が50に絞られてテスト回数が log₂(50)≒6 回で済みます。「どのファイルが関係しそうか」の当たりが付く場合は必ずpath指定を併用するとよい。
bisect log / replayで再現・共有・やり直し
bisect中に別の調査が必要になった、チームメンバーに共有したい、途中でgood/badを間違えた——そんな時に使うのがgit bisect logとreplay。
# 現在までのbisect操作を表示 git bisect log # git bisect start # git bisect bad HEAD # git bisect good v1.2.0 # git bisect bad a1b2c3d # git bisect good d4e5f6c # ファイルに保存(共有用) git bisect log > bisect-session.log # 後日または他メンバーが再現 git bisect replay bisect-session.log # → 記録された全操作が自動で再生される # → 同じ地点からbisect継続可能
間違えたgood/badを修正
# 保存 git bisect log > bisect-session.log # エディタで該当行を編集(bad → good など) nano bisect-session.log # 一旦abort git bisect reset # 修正した内容でやり直し git bisect replay bisect-session.log
チーム開発ではbisect logをIssueに貼る運用が実用的。「何コミットまで絞ったか」を他メンバーに共有でき、途中交代や夜間引き継ぎもスムーズ。バグレポートにbisect logを添付するのも良い習慣です。
worktreeで並行bisect:本作業を止めない
bisect中はHEADが動くため、通常作業ができません。git worktreeで別ディレクトリを用意すれば、本体ブランチで作業しつつ別ディレクトリでbisectという並行運用が可能。
# bisect用の別ディレクトリを切り出し git worktree add ../repo-bisect HEAD cd ../repo-bisect # ここでbisect(本体リポは自由に作業可) git bisect start git bisect bad git bisect good v1.2.0 git bisect run ../main-repo/scripts/bisect_test.sh # 終了 git bisect reset cd ../main-repo git worktree remove ../repo-bisect
worktreeのメリット
- 作業中断不要:本体でコードを書き続けられる
- 長時間bisect対応:一晩かかるbisectもblocking無し
- CI並列化:複数worktreeで異なる調査を同時進行
- 環境汚染防止:本体の
node_modulesを壊さない
性能リグレッションのbisect
機能は動くが遅くなったケースではbisectに工夫が必要。基準時間を超えたら”bad”とする閾値判定スクリプトを書きます。
#!/usr/bin/env bash
# scripts/bisect_perf.sh
set -u
# ビルド(失敗はskip)
npm ci --silent || exit 125
npm run build --silent || exit 125
# ベンチマーク実行(結果をmsで取得)
TIME_MS=$(node scripts/bench.js | awk '/response_time/ {print $2}')
# 閾値判定:100ms以上ならbad(遅延)
THRESHOLD=100
if (( $(echo "$TIME_MS > $THRESHOLD" | bc -l) )); then
echo "Slow: ${TIME_MS}ms > ${THRESHOLD}ms"
exit 1 # bad
else
echo "OK: ${TIME_MS}ms"
exit 0 # good
fi
カスタム用語で意図を明示
git bisect start --term-new=slow --term-old=fast git bisect slow # 現在は遅い git bisect fast v1.2.0 # v1.2.0では速かった git bisect run ./scripts/bisect_perf.sh # "slow is the first slow commit" と結果が出る
性能bisectの鍵は測定の安定性。benchmark中は他プロセスを止める、ウォームアップを入れる、複数回計測の中央値を使う、などで測定ノイズ>リグレッション幅の誤判定を防ぎます。
Docker/環境依存バグのbisect
「本番でだけ出る」「特定のNode.jsバージョンでしか再現しない」など環境依存バグでは、各コミットのテストをDockerで固定環境化するのが最強の対策。
#!/usr/bin/env bash # scripts/bisect_docker.sh set -u IMAGE="myapp-bisect:$(git rev-parse HEAD)" # 各コミットでイメージビルド(失敗はskip) docker build -t "$IMAGE" . || exit 125 # コンテナでテスト実行 if docker run --rm "$IMAGE" npm test; then exit 0 # good else exit 1 # bad fi
docker-composeを使った再現環境
#!/usr/bin/env bash # 例:DB/Redisが必要なテスト docker compose up -d --build || exit 125 trap "docker compose down" EXIT sleep 10 # 起動待ち(ヘルスチェック推奨) docker compose exec -T app npm test RESULT=$? exit $RESULT
submoduleが絡む場合
bisect中にgit checkoutされる度にsubmoduleのコミットもズレるため、スクリプトの冒頭でgit submodule update --init --recursiveを実行。これを忘れると古いsubmoduleで誤判定します。
フレークテスト・間欠バグの扱い方
bisectはテスト結果が決定的(deterministic)であることを前提にしています。「たまに落ちる」テストでは誤判定が起きやすい。
戦略①:リトライでノイズを減らす
#!/usr/bin/env bash
# bisect_flaky.sh: 3回テストして2回以上pass→good
set -u
npm ci --silent || exit 125
PASS_COUNT=0
for i in 1 2 3; do
if npm test --silent; then
PASS_COUNT=$((PASS_COUNT + 1))
fi
done
# 2回以上成功でgood
if [ $PASS_COUNT -ge 2 ]; then
exit 0
else
exit 1
fi
戦略②:テストを安定化してから再bisect
フレーク撲滅のコツ
- 時間依存を排除:
Date.now()をモック、jest.useFakeTimers() - 乱数固定:seedで再現可能に
- 外部API:HTTPモック(nock / msw等)で完全固定
- 並列テスト:
--runInBandや-j 1で順次実行 - sleep待機:waitForや条件ポーリングに置き換え
戦略③:範囲を絞る
フレークが酷い場合、全範囲でなく直近1週間など狭い範囲に絞ると判定ノイズが相対的に減ります。
CI統合:PR経由で自動bisect
手元で長時間かかるbisectはCIに任せるのが現代的。GitHub Actionsワークフローで「bisect」ラベル付きIssueから自動実行する例を示します。
name: Auto Bisect
on:
issue_comment:
types: [created]
jobs:
bisect:
if: contains(github.event.comment.body, '/bisect')
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 全履歴が必要
- name: Parse comment
id: parse
run: |
echo "bad=$(echo "${{ github.event.comment.body }}" | grep -oP 'bad=\K\S+')" >> $GITHUB_OUTPUT
echo "good=$(echo "${{ github.event.comment.body }}" | grep -oP 'good=\K\S+')" >> $GITHUB_OUTPUT
- name: Run bisect
run: |
git bisect start
git bisect bad ${{ steps.parse.outputs.bad }}
git bisect good ${{ steps.parse.outputs.good }}
git bisect run ./scripts/bisect_test.sh | tee bisect-result.txt
- name: Report result
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require("fs");
const result = fs.readFileSync("bisect-result.txt", "utf8");
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "```\n" + result + "\n```"
});
# Issueにコメント /bisect bad=HEAD good=v1.2.0 # → Actionsが起動し、数十分後に結果コメント # → 'a1b2c3d is the first bad commit'
CI bisectのコツ
fetch-depth: 0で全履歴取得(忘れるとshallow cloneでbisect不可)- タイムアウト長めに(120分程度)
- 結果をIssue/PRに自動コメント
- キャッシュ活用でビルド高速化
bisectでよくある罠と対処
①作業ツリーがクリーンでない
# エラー例 error: Your local changes to the following files would be overwritten by checkout: src/app.js # 対処:stash → bisect → stash pop git stash -u git bisect start ... # ... bisect終了後 git bisect reset git stash pop
②途中でabortしたい
# 任意のタイミングで中断
git bisect reset
# → 開始前のHEADに戻る
# 特定コミットへresetして終了したい場合
git bisect reset HEAD@{1}
③間違えてgoodとbadを入れ替えた
# 現在の操作を保存 git bisect log > session.log # session.logをエディタで開き、誤ったgood↔badを修正 # リセットして再生 git bisect reset git bisect replay session.log
④bisectが想定外の答えを返す
「first bad commitがバグと無関係そう」な場合、再現テストが別のバグを拾っている可能性があります。再現条件を狭める(特定のtest caseだけ実行)、goodを新しめに変更する、などで範囲を絞りましょう。フレーク誤判定ならリトライロジック追加。
⑤古すぎるgoodでビルド連続失敗
goodが数年前だとNode/Python等のバージョン差でビルドが通らずskipだらけになります。goodは新しめ(例:1〜3ヶ月前)から始めるのが基本。goodが見つからないなら、バージョン系タグ(v1.x.x)を順に遡って最初に通るポイントを基準goodにしましょう。
よくある質問
BISECT_HEADとして現在のbisectポインタが保持され、通常のHEADは各テスト対象コミットへ移動します。git bisect logで状態確認、git bisect resetで元の位置へ復帰。通常作業と並行したい場合はworktreeを使いましょう。git bisect start --term-new=slow --term-old=fastのようにカスタム用語を定義でき、性能リグレッションや仕様変更の特定に便利です。old/newもデフォルトで使えます(good/badと同義)。--first-parentでmain履歴だけ辿るとPR単位で絞れます。詳しくは本記事「–first-parentでmerge多数履歴を…」参照。exit 128以上を返すとbisect自体がabortします。set -eを使っていると想定外のコマンド失敗で終了コード1を返し「badコミット」と誤判定される罠も。スクリプト冒頭はset -uのみ、失敗時はexit 125を明示的に返すのが安全。fetch-depth: 0で全履歴を取得するか、git fetch --unshallowで補完してから実行。CI環境のactions/checkoutはデフォルトで浅いcloneなので要注意。git bisect start -- path/で指定ディレクトリ/ファイルを変更したコミットだけを候補化。範囲が1/10になればテスト回数もlog₂(対象数)に減るので、モジュールの当たりが付いている時は必ず使うべきテクニック。git bisect log > session.logで状態保存→git bisect resetで一時退避→別ブランチで調査→終わったらgit bisect replay session.logで再開。worktreeを併用すると切り替え不要で並行調査できます。git blameはファイルの各行の最終編集者・コミットを表示する静的な機能で、「このコード行はいつ追加されたか」に答えます。git bisectは動作確認(テスト)を通して「いつから壊れたか」を特定する動的な機能。blameで仮説を立ててからbisectで裏取り、という併用パターンがよく使われます。関連記事
- 【Git】よく使うgitコマンド決定版チートシート — bisect含む日常コマンド早見表
- 【Git】revertとresetの違いと使い分け完全ガイド — bisect後の修正・巻き戻し
- 【Git】rebaseとmergeの違いと使い分け完全ガイド — first-parentの背景理解
- 【Git】rebase中のエラー完全復旧ガイド — bisect前の履歴整形
- 【Git】タグ完全ガイド — goodの基準点としてのタグ運用
- 【Git】特定のコミットまで戻す方法 — bisect対象コミットの参照
- 【Git】コミット間の差分を比較する方法 — 犯人コミット特定後の詳細分析
- 【Git】mergeコミットを取り消す方法 — bisectで特定したmerge commitの取り消し
- 【Git】ブランチが削除できないときの原因と対処法完全ガイド — worktree削除エラーの対処
まとめ
- bisectは二分探索、最大 ⌈log₂(N)⌉ 回のテストで犯人特定(1000コミットでも10回)
- 基本5コマンド:
start/bad/good/run/reset bisect runで完全自動化。終了コード0=good、1-124=bad、125=skip、128+=abort- monorepo/merge多数は
--first-parentでPR単位に絞る(Git 2.29+) git bisect start -- path/でpath絞り込み、対象コミットを1/10にgit bisect log/replayで再現・共有・やり直し(Issue添付がおすすめ)git worktree併用で本作業を止めずに並行bisect- 性能リグレッションは閾値判定スクリプト+カスタム用語(slow/fast)
- 環境依存バグはDockerで各コミットの再現環境を固定
- フレーク対策:多数決リトライ、時間/乱数/外部APIのモック化、範囲を狭める
- CI統合でIssueコメント経由の自動bisect(
fetch-depth: 0必須) - 犯人特定後は回帰テスト追加まで実施して再発防止
bisectは”慣れていれば30分”と”知らなければ1日”を分ける最強のデバッグツール。単純な手動二分探索からbisect runでの完全自動化、--first-parent・path絞り込み・worktree並行実行といった応用テクニックを組み合わせれば、大規模リポジトリのバグ調査でも怖くなくなります。2026年の現場ではbisect log/replayを使った再現可能デバッグとCI連携による自動化が一線級エンジニアの標準装備です。

