【Git】bisectでバグコミット特定完全ガイド|log₂N回の二分探索・bisect run自動化・–first-parent・path絞り込み・CI統合まで

Git

「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 logreplayで探索過程を再現・共有
  • git worktree併用で本作業を止めずに並行bisect
  • 性能リグレッション(ベンチマーク比較)bisectのセットアップ
  • フレークテスト・環境依存バグの扱い方(Docker・リトライ)
  • CI統合:PR経由で自動bisectを回すワークフロー
スポンサーリンク

bisectの原理と必要テスト回数の予測

bisectは二分探索です。N個の候補コミットから犯人を特定するのに必要なテスト回数は最悪 ⌈log₂(N)⌉ 回。1024コミットでも10回、1,000,000コミットでも20回しかかかりません。

候補コミット数 最大テスト回数 1回3分で所要時間
16 4 12分
100 7 21分
1,000 10 30分
10,000 14 42分
100,000 17 51分

実用的な結論:「直近1ヶ月で壊れた」が分かっていれば、普通のプロジェクトなら10〜15分でbisectが終わる。goodを決める段階でタグ(v1.2.0など)や先月のmainを指定するのがコツ。古すぎるgoodを選ぶとビルド失敗が頻発してskipだらけになります。

bisect中の進捗メッセージを読む

Gitが教えてくれる残り回数
$ git bisect bad
Bisecting: 512 revisions left to test after this (roughly 9 steps)
[a1b2c3d] feat: 新機能追加

# "roughly 9 steps" が残りの最大テスト回数
# → あと9回テストで犯人が決まる

基本フロー:5コマンドで完結

bisect基本の流れ
# ① 開始(現在の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

開始時に一気に指定する短縮形

start引数にbad/goodをまとめる
# 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 slowgit bisect fastでマーク
  • old/newがデフォルトでも使える(good/badと同義)

git bisect runで完全自動化:終了コード規約

テストスクリプトで再現判定できるなら、bisect run人間の介入ゼロ。夜間に回せば朝には犯人が確定しています。鍵はスクリプトの終了コードです。

終了コード 意味 bisectの判定
0 成功=バグなし good(古い側に原因あり)
1〜124
126〜127
失敗=バグあり bad(新しい側に原因あり)
125 判定不可(ビルド失敗等) skip(別のコミットで再試行)
128以上 致命的エラー bisect全体をabort
自動bisect用スクリプト(bash)
#!/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の終了コードをそのまま返す
自動bisectの実行
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 / 自動skip
# 手動で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指定で範囲を絞る、のいずれかで対処。

first bad commit の範囲絞り込み
# 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-parentmergeの第一親(通常はmainの履歴)のみを辿る機能。PR単位でbisectできるため、monorepoでは実質必須の設定です。

–first-parent bisect
# 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

PR単位で絞ってから詳細調査
# 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するのは無駄。

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 logreplay

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という並行運用が可能。

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”とする閾値判定スクリプトを書きます。

パフォーマンス閾値bisect
#!/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

カスタム用語で意図を明示

slow / fast でbisect
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で固定環境化するのが最強の対策。

Docker前提のbisectスクリプト
#!/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から自動実行する例を示します。

.github/workflows/bisect.yml
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コメントで発動
# 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を入れ替えた

bisect log編集でリカバリ
# 現在の操作を保存
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にしましょう。

よくある質問

Qbisect中のHEADはどうなっている?
ABISECT_HEADとして現在のbisectポインタが保持され、通常のHEADは各テスト対象コミットへ移動します。git bisect logで状態確認、git bisect resetで元の位置へ復帰。通常作業と並行したい場合はworktreeを使いましょう。
Qgood/badを使わない調査はできる?
Aはい。git bisect start --term-new=slow --term-old=fastのようにカスタム用語を定義でき、性能リグレッションや仕様変更の特定に便利です。old/newもデフォルトで使えます(good/badと同義)。
Qmergeコミットで犯人が止まるのはなぜ?
Amergeコミット自体がバグを生んだケース(片方のブランチの変更同士の衝突)か、bisectがブランチ側に潜ったまま迷子になったケース。--first-parentでmain履歴だけ辿るとPR単位で絞れます。詳しくは本記事「–first-parentでmerge多数履歴を…」参照。
Q自動bisectが途中で止まる
Aスクリプトがexit 128以上を返すとbisect自体がabortします。set -eを使っていると想定外のコマンド失敗で終了コード1を返し「badコミット」と誤判定される罠も。スクリプト冒頭はset -uのみ、失敗時はexit 125を明示的に返すのが安全。
Qshallow cloneだとbisectできない?
Aはい。fetch-depth: 0で全履歴を取得するか、git fetch --unshallowで補完してから実行。CI環境のactions/checkoutはデフォルトで浅いcloneなので要注意。
Qbisectの対象を特定のpathだけに限定できる?
A可能です。git bisect start -- path/で指定ディレクトリ/ファイルを変更したコミットだけを候補化。範囲が1/10になればテスト回数もlog₂(対象数)に減るので、モジュールの当たりが付いている時は必ず使うべきテクニック。
Qbinary(コンパイル済み)ファイルのリグレッションもbisectできる?
A可能ですが各コミットでビルド必須です。Dockerで固定環境化、キャッシュ活用でビルド時間短縮、成果物サイズが重要ならファイルサイズを閾値判定するなど、通常のテストbisectと同じフレームで対応できます。
Qbisect中に別のバグに遭遇したらどうする?
Agit bisect log > session.logで状態保存→git bisect resetで一時退避→別ブランチで調査→終わったらgit bisect replay session.logで再開。worktreeを併用すると切り替え不要で並行調査できます。
Qgit bisectとgit blameはどう使い分ける?
Agit blameファイルの各行の最終編集者・コミットを表示する静的な機能で、「このコード行はいつ追加されたか」に答えます。git bisect動作確認(テスト)を通して「いつから壊れたか」を特定する動的な機能。blameで仮説を立ててからbisectで裏取り、という併用パターンがよく使われます。

関連記事

まとめ

  • bisectは二分探索、最大 ⌈log₂(N)⌉ 回のテストで犯人特定(1000コミットでも10回)
  • 基本5コマンド:startbadgoodrunreset
  • 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連携による自動化が一線級エンジニアの標準装備です。