昨晩 CI を通過していた payment.spec.js が、朝のビルドで失敗しました。最終的な正常リリースタグ以降にマージされたコミットは 200 件です。git log を上方へスクロールして diff を目で追うことは線形探索に該当するため、全コミットでビルドとテストを実行すると相当な時間を要します。ここで取り上げるのは、実際に遭遇した事例ではなく、そのようなリグレッションが発生した仮想シナリオです。

この状況では、git bisect が「一つずつ確認する」代わりに「毎回半数ずつ除外する」アプローチを採用します。正常と確認できたコミット(good)と失敗が発見されたコミット(bad)を境界とし、Git は候補集合の中間コミットをチェックアウトします。ユーザーはそのコミットをテストし、good または bad で判定を返答する必要があります。各判定ごとに候補数が半数に減るため、コミット数が 1,000 件であっても、最大で約 10 回(log₂1000 ≈ 10)で範囲を絞り込めます 1。200 件であれば、8 回前後で収束します。

ただし、この計算はすべての判定が実際のコミットの動作を正確に反映する場合にのみ成立します。ビルドが壊れて実行自体が不可能なコミットを誤って bad と判定したり、自動化スクリプトが起動すらできず exit code 127 を返したりすると状況は変わります。Git は誤った半分を選択したり、全コミットを bad とマークしたりして、結果が無意味になります 2。

探索を開始する前に、再現条件と境界の準備

git bisect が正しく機能するには、2 時点の状態が明確である必要があります。現在の HEAD が bad であることは、リグレッションが再現されることを意味し、過去の某タグが good であることは、その時点ではリグレッションが存在しないことを意味します。この前提が揺らげば、探索プロセス全体が狂ってしまいます。

Nodelog Engineer は、good として指定した時点が実際にはすでに故障していた場合、結果が歪む可能性があると指摘しています。そのため、「直近で正常に見えたコミット」よりも、「バグ発生前であることが明らかなタグ」を余裕を持って前倒しで good として指定することが推奨されます 1。

判定を人間ではなくスクリプトで行う場合、スクリプトの実行場所も結果に影響します。tms-outsource は、判定スクリプトをリポジトリ外に配置する方が安全だと説明しています。その理由は、過去のコミットには後に追加されたテストファイルが存在せず、テストファイルの欠如という誤った理由で失敗が発生し得るためです 2。200 件のコミット後に追加された payment.spec.js を、探索範囲に含まれる過去のコミットで実行しようとすると、それが当該コミットのコードの問題なのか、テストファイルの有無の問題なのかを区別できません。

したがって、リグレッションを検証するコマンドは、リポジトリ外部のパスに配置するか、依存関係のインストール(npm ci など)を含めて隔離された状態で準備する必要があります 1。こうすることで、過去のコミットの環境残留物による誤判定を回避できます。探索の前にワークスペースをクリーンに整えることも有用です。未コミットの変更がチェックアウトを妨げ得るため、git stash などを使用して変更を分離しておくことが推奨されます 2。

good と bad の境界を設定して探索範囲を絞り込む

good と bad の境界を設定して探索範囲を絞り込む

セッションは git bisect start で開始します。失敗が発見された現在のコミットを git bisect bad として、正常と確認された時点のコミットを git bisect good として登録します。GitLab 公式ドキュメントでは、この手順を「bad コミットと good コミットを特定し、境界値を指定する」と要約しています 3。

bash
git bisect start
git bisect bad          # 현재 HEAD = 회귀 재현
git bisect good v1.4.0  # 태그·커밋 해시·상대참조 모두 가능

開始時点で 2 つのリビジョンを一括で渡す短縮形も使用可能です。git bisect start HEAD v1.4.0 のように bad を先に、good を後ろに記述すれば、両方の境界が一度に指定されます 2。

探索空間そのものを削減する手段もあります。特定のパスに変更のあるコミットのみを候補とする pathspec を追加できます。Git ドキュメントの例では、git bisect start -- arch/i386 include/asm-i386 のように特定のディレクトリやファイルの変更のみを候補に限定する方法が示されています 2。仮想的な決済ロジックの例では、関連のないドキュメント変更や CI 設定変更を候補から除外するのに活用できます。

また、known-good コミットを複数指定すると、探索空間はさらに狭まります。bad コミットの後に複数の正常コミットを列挙することで、範囲を追加で縮小できます 2。探索範囲が広いほど log₂ 計算の利得は大きくなるため、これらの前処理は「失敗時点」よりも「境界の幅」を縮める上で有効です。

中間コミットの検査、skip による迂回と視覚確認

中間コミットの検査、skip による迂回と視覚確認

両境界が確定すると、Git は候補集合の中間コミットへ自動的にチェックアウトします。この際、Git は残りの候補数と想定ステップ数を出力します。

Bisecting: 63 revisions left to test after this (roughly 6 steps)

この出力は、ユーザーが判定を必要とする回数を事前に把握することを可能にします。公式ドキュメントの例でも、675 revisions left, roughly 10 steps のように候補が半数ずつ減少していく流れが確認できます 12。

ユーザーはチェックアウトされたコミットをビルドし、リグレッションの再現有無を確認した後、結果のみを入力します。

bash
# 회귀가 재현되면
git bisect bad
# 정상이면
git bisect good

中間コミットを bad と判定した場合、「原因コミットはそれ以前(またはここ)」という意味になるため、Git はそれ以降の区間を除外します。good と判定した場合、「原因コミットはそれ以降」という意味になるため、それ以前の区間を除外します。この規則により、log₂ のステップ数で候補が消去されていきます 2。最終的には、b9f0e1a2c3... is the first bad commit のように単一のハッシュと、当該コミットが修正したファイル一覧が表示されます 1。

ビルド自体の失敗により good/bad を判定できないコミットが発生する場合もあります。このような場合、git bisect skip で現在のコミットをスキップできます。Git は近傍の別コミットへ迂回し、探索を継続します 1。skip は範囲指定も可能です。git bisect skip v2.5..v2.6 のように、区間をまとめてマークアップできます 1。

ただし、skip には精度犠牲という代償が伴います。Git 公式マニュアルを引用した tms-outsource の説明によると、原因コミットと隣接するコミットを skip すると、Git は「どちらが先か」を正確に特定できなくなります 2。探索中断を回避するための手段ですが、精度は低下する可能性があります。skip された候補が隣接しており区別不可能な場合、Git は候補リストをすべて出力して停止します。この際は、git bisect visualize を使用して gitk で残りの疑わしい候補を視覚的に確認し、隣接する正常コミットを一つ選んで手動で判定することで復旧できます 1。

自動化:git bisect run と exit code が結果を左右する

自動化:git bisect run と exit code が結果を左右する

人間が毎回 good/bad を入力する代わりに、スクリプトの終了コードで判定を自動化できます。git bisect run <스크립트> は、各チェックアウトされたコミットでコマンドを実行し、exit code を判定として解釈します。

Christian Couder(LWN.net、2009 年 2 月 3 日寄稿)は、終了コードが 0 なら good、それ以外の値(125 および 128 以上を除く)なら bad と解釈されることを明記しています。ビルド失敗によりリグレッションの有無を判断できないコミットについては、125 で skip を要求するパターンを提示しています 4。

bash
git bisect start
git bisect bad
git bisect good v1.4.0
git bisect run npm test -- payment.spec.js

exit code の意味は以下の表のとおりです 12。

exit code判定状況
0goodテスト通過
1~124, 126, 127badテスト失敗
125skipビルド失敗など検証不能
128~255abort探索中断(シグナル、意図的な終了など)

ここでは注意すべき 2 つの落とし穴を押さえておく必要があります。

第一に、exit code 127 です。POSIX シェルはコマンドが見つからなかった場合に 127 を返します。スクリプトパスに誤字があってコマンド自体が実行されないと 127 が返ります。Git はこれを「bad」と解釈して全コミットを bad とマークし、探索は無意味な結果で終わってしまいます 2。「テストが失敗した」と「スクリプトが存在せず起動すらできなかった」が同じ bad に混同されるためです。したがって、自動化の前にスクリプトが実行可能な状態であることを必ず確認する必要があります 2。

第二に、ビルド失敗とリグレッション失敗の区別です。テスト失敗ではなくコンパイルエラーで exit code 1 や 2 が返されると、そのコミットは bad と記録されます。しかし、失敗の原因はリグレッションではなくビルド環境かもしれません。Couder はビルド失敗時に skip を明確にするため、make || exit 125 パターンを提示しています 4。

bash
#!/usr/bin/env bash
# 저장소 밖에 두는 편이 안전합니다 [S13]

make build || exit 125    # 빌드 실패 = 검증 불가 → skip

if ./run-app --check-payment; then
  exit 0                  # good
else
  exit 1                  # bad (회귀 재현)
fi

この 1 行の条件文が「ビルド失敗」と「リグレッション」を分離する中核的な仕組みです。リポジトリ外部のパスにこのスクリプトを配置すれば、過去のコミットにはテストファイルが存在しないことに起因する誤判定も削減できます 2。

自動化には前提条件もあります。Couder の寄稿で Andreas Ericsson(Git 開発者)は、「git bisect を使用するには、すべてのコミットが小さく、bisect 可能な状態に維持される必要がある」と述べ、リグレッションを露呈させるテストをコミットごとに作成するワークフローが必要だと説明しています 4。自動化は判定入力の労力を排除しますが、再現スクリプトを常に準備しておく努力を要します。Ericsson は社内トラッカー基準で報告-修正サイクルが平均 142.6 時間から 16.2 時間へ短縮されたと報告していますが、これは特定の追跡システムで測定された経験値であり、コミットの細分化とリグレッションテスト作成という先行条件に依存しています 4。

探索終了後の reset と結果の解釈

探索終了後の reset と結果の解釈

探索が完了したら、必ず git bisect reset を実行する必要があります。bisect 中、Git は candidate コミットをハッシュでチェックアウトするため、detached HEAD 状態になります。reset しないと、中間コミットに留まったまま作業が続いてしまいます。Nodelog Engineer はこれを「習慣的に実行せよ」と案内しており、Pro Git も「最終 reset を省略すると、リポジトリが異常な状態に残る」と警告しています 12。

git bisect reset に引数を渡すと、着地地点が異なります。

bash
git bisect reset          # 세션 시작 전 HEAD로 복귀
git bisect reset bisect/bad  # first bad commit으로 복귀
git bisect reset HEAD      # 탐색 중 커밋에 머무름

引数なしで実行すると、探索前のブランチへ戻ります 2。

出力された first bad commit は「変更が入ったコミット」という事実のみを伝え、「なぜ」リグレッションが発生したかは説明しません。最終出力ではコミットハッシュと当該コミットが修正したファイル一覧が表示されますが、diff を読んで原因を特定するのは人間が行う作業です 2。

first bad commit がマージコミットになる場合もあります。その際は、Nodelog Engineer の案内にあるとおり、そのブランチ内部を再度 bisect して詳細コミットまで絞り込めます 1。

判定中の誤判定に対する復旧経路もあります。10,928 件の候補(約 14 ステップ)において、最初の判定一つが残り 13 ステップが探索する半分を決定します。誤判定は後半の全ステップを誤った方向へ導く可能性があります。この際は、git bisect log > bisect.log で判定履歴を保存し、誤った行を削除した上で、git bisect reset と git bisect replay bisect.log を実行して探索を再実行できます 12。

結論:判定の質が探索の精度を決定する

git bisect は、good と bad の境界を基準に候補を毎回半数ずつ削減し、200 件のコミットであれば約 8 回の判定で first bad commit を絞り込むツールです 1。しかし、この結果は各判定が当該コミットのリグレッション有無を実際に反映する場合にのみ有効です。

ビルド失敗を bad とすると誤った半分が残存し、スクリプトパスの誤りで exit code 127 が返ると、Git は全コミットを bad と勘違いします 2。したがって、自動化時はビルド失敗を exit code 125 で分離して skip 処理とし、検証スクリプトはリポジトリ外に置き、探索終了後は git bisect reset で detached HEAD から元のブランチへ復帰する手順が、結果の信頼性を決定づけます 42。

good の境界を余裕を持って設定し、テスト隔離を含む再現条件を事前に準備しておけば、Git が特定したコミットハッシュと変更ファイル一覧は、「ここでリグレッションが入った」ことを示す出発点として十分な精度を持っています 1。

参考文献

  1. ↩
  2. ↩
  3. ↩
  4. ↩