Git bisect: バイナリサーチでバグを引き起こしたコミットを素早く特定する

Git tutorial - IT technology blog
Git tutorial - IT technology blog

本番環境でエラーが発生しても、原因となったコミットがわからないとき

スプリントが終わり、本番環境にデプロイして30分後、Slackが荒れ始める:「バグだ、フィーチャーAが壊れてる!」git logを実行すると…スプリント中に47件のコミット。一つずつレビューする?午後がまるごと潰れてしまう。

この状況はよくある話だ。4人のマイクロサービスプロジェクトでは、毎スプリント数十件のコミットが積み上がる。最初の反応はgit checkoutでコミットを一つずつ戻して手動テストすることだった。そのとき、以前読んだことのあるgit bisectのことを思い出した――読んではいたが、実際に使ったことはなかったツールだ。

初めてbisectを使って3時間かかっていたバグを5分で見つけたあの日から、もう古いやり方には戻れなくなった。

Git bisectの仕組み

アイデアはシンプルだ:コミットを順番にたどる代わりに、git bisectはコミット履歴を二分割して「このコミットにバグがあるか?」と問う。「ある」か「ない」で答えると、残りをさらに二分割する。これがまさにバイナリサーチだ。

1000件のコミットがある場合、手動では最悪1000回の確認が必要だ。bisectなら最悪でもlog₂(1000) ≈ 10回で済む。47件なら?約6回の確認で終わる。

すべてのロジックは2つのシンプルなラベルを中心に動く:

  • bad:バグのあるコミット(通常は現在のHEAD)
  • good:バグのないコミット(正常と確認できる古いコミット)

Git bisectは自動的に中間のコミットにチェックアウトする。テストしてbadまたはgoodとマークすれば、それを繰り返して犯人を絞り込んでいく。

実際の例を使ったステップバイステップの実践

bisectセッションの開始

mainブランチにいて、HEADが最新のバグありコミットだとしよう:

# 開始
git bisect start

# 現在のコミットをbad(バグあり)とマーク
git bisect bad

# 古いコミットをgoodとマーク — リリースタグまたは特定のハッシュを使用
git bisect good v1.2.0
# またはハッシュを使用
git bisect good abc1234

Gitは即座に中間のコミットにチェックアウトし、残りのステップ数を教えてくれる:

Bisecting: 23 revisions left to test after this (roughly 5 steps)
[f3d9a2b] Fix login validation

テスト → マークのループ

Gitが誘導する各コミットで、テストを実行するかバグを手動で再現し、マークする:

# このコミットにバグがある場合
git bisect bad

# このコミットにバグがない場合
git bisect good

Gitが発見を通知するまで繰り返す:

e4f5c6d is the first bad commit
commit e4f5c6d
Author: Nguyen Van A <[email protected]>
Date:   Mon Feb 10 14:32:11 2026 +0900

    Refactor user authentication module

:040000 040000 abc... def... M    src/auth

これだ。コミットe4f5c6dが犯人だ。

終了してHEADに戻る

git bisect reset

このコマンドで元のHEADに戻る。終わったら必ずこれを実行すること――しないと履歴の途中のどこかのコミットにいることになり、間違いが起きやすい。

スクリプトで自動化する

これが一番気に入っている部分だ。各コミットを手動でテストする代わりに、自動スクリプトを書いてgit bisectに任せてしまえる:

git bisect start
git bisect bad HEAD
git bisect good v1.2.0

# 自動スクリプトを実行 — exit code 0 = good、非ゼロ = bad
git bisect run ./test_bug.sh

test_bug.shはシンプルなこんな内容だ:

#!/bin/bash
# test_bug.sh — ログイン機能が正常に動作するか確認する
cd /path/to/project
python -m pytest tests/test_auth.py::test_login -x -q
# pytestの自動exit code: 0 = pass、1 = fail

またはNode.jsの場合:

#!/bin/bash
npm run build 2>/dev/null && node -e "
const auth = require('./dist/auth');
const result = auth.validateUser('[email protected]', 'password');
process.exit(result ? 0 : 1);
"

Git bisect runはこのスクリプトを各コミットで自動実行し、exit codeに基づいてbad/goodを自動マークする。あとは眺めているだけでいい。

bisectの過程をログで確認する

どのコミットを経由したか確認したい場合:

git bisect log

調査過程をバグレポートに添付する必要がある場合、これを一緒に送れば十分だ――チームリードが追加で質問する必要がなくなる。

テストできないコミットをスキップする

ビルドできないコミットに遭遇することがある――古い依存関係、設定の不足、未解決のコンフリクトなどで。そんなときは:

git bisect skip

Gitが自動的にスキップして、隣のコミットを試してくれる。

実体験から得た注意点

「good commit」を正確に特定する:リリースのマークにはgit tagをよく使う。タグがない場合は「2週間前にはこの機能は正常だった」という形で推測する。その後git log --oneline --since="2 weeks ago"を実行して適切なハッシュを探す。

ユニットテストはbisectの力を最大化する:テストカバレッジが充実したプロジェクトなら、関連するテストケースを実行するbisect runスクリプトを書くのが最速だ。この方法で8分でバグを見つけたことがある――完全自動で、待っているだけでよかった。

マージコミットに注意:bisectがマージコミットで止まることがある。その場合は早まって結論を出さず、両方の元ブランチを確認してコンテキストを正しく理解する必要がある。

慎重さについて言えば――誤って別のブランチにforce pushしてしまい、大切なコードを失った経験から、あらゆるgit操作に一層気を配るようになった。bisectも同様だ:badなコミットを見つけても、すぐにgit revertしてはいけない。まずdiffをよく読むこと。そのコミットが複数の変更を含んでいる場合、全体をrevertすると別の問題が発生することがある。

バグレポートを受けたときの実践的なワークフロー

bisectを使ったデバッグを繰り返す中で、一定のフローが確立された:

  1. HEADでバグが再現することを確認する――git statusで正しいブランチにいることを確かめる
  2. 「last known good」を探す――通常は直近のリリースタグか現在のスプリント前のコミット
  3. 可能であればバグを再現する最小限のテストケースを書く(bisect runで使用するため)
  4. スクリプトがあればgit bisect start + git bisect runを実行し、なければ手動でマークする
  5. 修正前にgit show <hash>でbadコミットのdiffをしっかり読む
  6. git bisect resetでHEADに戻ってから修正を書き始める

まとめ

ほとんどの開発者はgit bisectの存在を知っている――しかし「最後に使ったのはいつ?」と聞くと沈黙が返ってくる。それはもったいない。

6ヶ月間実際に使ってきて、bisectが最も活躍するのは2つの状況だと気づいた。一つは、多くのコミットを積み重ねた長いスプリント後のリグレッションバグ。もう一つは、どこからコードを読めばいいかわからないほどコンテキストが不足している大規模なコードベースだ。

複雑なシンタックスを覚える必要はない――覚えることは:bisect start → bad/goodをマーク → 各コミットをテスト → bisect resetの流れだけだ。次にどのコミットがバグを引き起こしたかわからないときは、手動でgit logを眺める代わりにbisectを試してみよう。

Share: