以前の記事では、git add と git commit の間で何が起きているか、つまり「コミットを作る瞬間」の仕組みを扱った。今回は視点を変え、「過去に作られた大量のコミットの中から、特定の不具合を持ち込んだ1つを、どう突き止めるか」という調査の場面を扱う。git blame と git bisect は、どちらも「いつ・誰が・何を変えたか」を過去に遡って調べるためのコマンドだが、向いている場面がはっきり異なる。
git blame — 1行の履歴をそのまま辿る
git blame <ファイル> を実行すると、そのファイルの各行について「直近にその行を変更したコミット」がハッシュ・作者・日時つきで一覧表示される。バグの原因になっていそうな行がすでに特定できている場合、この行がいつ・どのコミットで、どんな意図で追加されたのかを直接調べられる。
git blame -L 40,60 core/db_backup_diagnostics.py
-L で行範囲を絞ると、ファイル全体を見る必要がなく調査が速い。さらにコミットハッシュを git show <ハッシュ> に渡せば、その変更がどんなコミットメッセージ・どんな差分の一部だったかまで遡れる。blame の弱点は、「この行がいつ書かれたか」は分かっても「この行が原因で今の不具合が起きているかどうか」までは教えてくれない点にある。原因の行が分かっていない段階では、blame は出発点にならない。
git bisect — 「良かった時点」と「壊れている時点」から二分探索する
原因のコード箇所そのものが分かっていない場合に有効なのが git bisect だ。考え方はシンプルで、「このバージョンでは正常に動いていた」というコミット(good)と「現在は壊れている」というコミット(bad)を1つずつ指定すると、Gitがその間のコミット群のちょうど中間地点に自動でチェックアウトしてくれる。そこで動作確認をして「まだ壊れている」か「もう直っている」かを Git に伝えると、範囲がさらに半分に絞り込まれる。
補足: 二分探索(binary search)とは、探したい対象が並び順のある集合の中にあると分かっている場合に、毎回対象範囲の中央を調べて、そちらの半分を捨てていく探索方法。100個の候補があっても、理論上は7回前後の確認で1個に絞り込める(2の7乗が128のため)。
git bisect start
git bisect bad # 現在のコミット(HEAD)は壊れている
git bisect good v1.6.8 # このタグの時点では正常だった
# → Git が自動的に中間のコミットをチェックアウトする
# 動作確認して...
git bisect good # まだ正常なら
git bisect bad # 壊れているなら
# ここまでの手順が自動で繰り返され、最終的に1コミットに絞り込まれる
git bisect reset # 終わったら元のブランチに戻す
候補が数百コミットあっても、手作業で1件ずつ確認する場合に比べて、確認すべきコミット数を大きく減らせるのが bisect の利点である。線形に1件ずつ遡るよりも、少ない確認回数で候補が絞り込まれていく。
自動化: git bisect run でテストスクリプトに判定を任せる
good/bad の判定を人間が毎回目視で行う必要はない。判定を再現できるスクリプトやテストがあれば、git bisect run <コマンド> にそのコマンドを渡すことで、Git が各中間コミットで自動的にそのコマンドを実行し、終了コード 0 を good、0 以外を bad として判定を自動で進めてくれる。
git bisect start
git bisect bad HEAD
git bisect good v1.6.8
git bisect run python3 -m unittest tests.test_db_backup_diagnostics
このコマンドが通れば、最初の good/bad 指定さえ与えれば、原因コミットの特定までを Git が自律的に進める。ただし判定用のスクリプトやテストが、調べたい不具合を確実に再現できることが前提になる。曖昧な判定基準(たまに失敗するテストなど)を使うと、二分探索の前提である「範囲の片側は必ず良い・もう片側は必ず悪い」が崩れ、誤った結果に収束することがある。
実例: シェル環境依存のパース不具合が生まれた地点を辿る
さくらインターネットなど、ログインシェルが csh 系のサーバーで DB バックアップが必ず失敗する不具合が過去にあった。原因は、ある時点で追加した進捗ログの出力処理が、bash を前提にした挙動に依存しており、csh 環境ではログの形式が変わってパースに失敗するというものだった。この種の不具合は「いつ動かなくなったか」は分かっている(csh 環境での動作報告を受けた時点)が、「どのコミットが原因か」は数十〜数百コミット離れた過去のどこかにある、という典型的な bisect 向きのケースである。csh 環境を再現するテストがあれば git bisect run にそのテストを渡すだけで原因コミットまで自動的に絞り込め、無ければ該当環境で都度手動確認しながら good/bad を答えていく形になる。原因コミットさえ特定できれば、そのコミットに対して git blame で前後の変更意図を確認し、修正方針を立てる、という流れで両者は補い合う関係にある。
blame と bisect の使い分け
| 状況 | 使うコマンド |
|---|---|
| 怪しい行はすでに分かっている・いつ書かれたか知りたい | git blame |
| 「壊れている」ことは分かるが、原因の行やコミットが分からない | git bisect |
bisect で原因コミットが絞り込めた後、その変更の意図を確認したい |
git blame / git show <コミット> |
bisect で「このコミットが原因」まで絞り込めたら、そのコミットの差分を git show で読み、必要なら blame で同じ箇所の別の変更履歴も遡る、という組み合わせで使うのが実務上の流れになる。「範囲を絞る bisect」と「1点を深掘りする blame」は、調査の異なる段階を担っていると捉えると使い分けやすい。
まとめ
git blame は「この行が最後に変わったのはどのコミットか」を直接教えてくれる一方、原因の行そのものが分かっていない段階では出発点にならない。git bisect は good/bad の2点さえ指定すれば、二分探索でその間のコミット群から原因コミットを絞り込んでくれ、判定を再現できるテストがあれば git bisect run に判定そのものを委ねられる。前回のステージングエリアの記事で見た「コミットは時点固定のスナップショットである」という性質があるからこそ、過去のどのスナップショットが原因だったかを二分探索で遡れる、という点でも両者はつながっている。