「昨日まで動いていた機能がいつの間にか動かない」
こういう時ありますよね。
心当たりがなく明確なエラーログも出ない不具合は、推測で追うと時間が溶けてしまうケースが多いです。
最近はバグ調査にもAIツールが役に立ちます。
しかし、ドメインに深く関わっておりロジック上は問題ない場合にはそもそもとっかかりが見つからず、プロンプトが不十分で解決できないことも多いです。
そんなとき強力なのが、動く/動かないの二択だけで原因コミットを二分探索する git bisect です。
この記事では、git bisect で原因コミット→該当PR特定→単独revertまで進めた流れをまとめます。
※内容はフェイクが混ぜてあります
起こった不具合は
Webアプリ上 /path/to/featureで成績データを取得し、それを成績表として印刷用に画面表示する機能において、画像化した表が表示されない問題です。
- 成績取得のAPIは問題なく実行できており、しばらく変更は入っていない
- フロントエンド側も直近は変更されておらず、コンソール等でエラーが出ていない
状態でした。
バックエンドでの変更は入っておらず、返されるデータにも問題なかったことからフロントエンド側のバグだと仮説を立てました。
原因には全く心当たりはありませんが、処理が成功したかどうかは一目瞭然で分かります。
このような場合にコミット単位で突き止められるのがgit bisectコマンドです。
二分探索によって問題箇所を特定できる機能です。
1) まず「不具合」をYes/No判定できる形に落とす
git bisect は、各コミットで good(動く)/bad(壊れてる)を確実に判定できることが命です。
今回の不具合は次のように定義しました。
-
再現手順(例)
-
/path/to/featureを開く - 対象を複数選択
- 「印刷」ボタンを押す
-
-
期待される動作
- 印刷ダイアログが開き、プレビューに生成物が表示される
-
バグを含んだ動作
- ダイアログは開くが、プレビューが空
この判定は毎回同じ基準で行います。
2) git bisectを始める前に、good と bad を決める
git bisectは good / bad の間を探索します。ここが曖昧だと探索範囲が広くなって遅くなります。
今回はリリースブランチを切って作業している時にバグが判明したと想定して下記のように考えます。
-
bad→現在の
masterブランチのHEAD -
good→確実に動いていた前回のリリースタグ
vX.Y.Z
goodがタグで明確だったため、最初から範囲が綺麗に切れました。
3) git bisectの基本
git checkout master
git bisect start
git bisect bad HEAD
git bisect good vX.Y.Z
// git bisect start <bad-commit> <good-commit>でもOKです
// 例)git bisect start HEAD vX.Y.Z
Gitが中間コミットに移動したら、アプリを起動して判定します。
- 動いた →
git bisect good - 壊れてる →
git bisect bad
git bisect good
# or
git bisect bad
これを繰り返すと、最後に「最初に壊したコミット」が特定されます。
<hash> is the first bad commit
commit <hash>
<message>
原因コミットが分かれば、あとはコミット内容から調査するフェーズに移れます。
ここまで来ればAIに投げてしまって解決することも多いかもしれません
バグを繰り返さないためにも原因調査はしっかりしたいところですね。
ちなみに例に挙げたバグは、ReactDOM.renderをReact18のcreateRoot().renderに置き換えたことが原因でした。
どうやら非同期に画像を書き出す処理が失敗していたようです。
大小多くの機能が実装されたコミットの山からこの変更をダイレクトに発見することは難しかったでしょう。
動作の確認方法
大きく分けてテスト用のスクリプトを書くか、手動で行うかだと思います。
個人的な肌感としてはスクリプトで確認できる場合はそもそもUnit Testで検知できるケースのため、手動でやることも多々あります。
テスト用のスクリプトがある場合はそれを指定します。
git bisect run <スクリプト名>
この方法であれば勝手にテスト実行→中間バージョンのチェックアウトを繰り返してくれるため、待つだけでOKです。
例に挙げたような印刷ダイアログのプレビューに生成物が表示されるかどうか等、UI上でしか確認が取れないケースは手動でないと難しいかと思います。
手動の場合は、適宜checkoutされる度に動作を確認し、先ほど挙げた通りのコマンドを打っていきます。
- 動いた →
git bisect good - 壊れてる →
git bisect bad
git bisect good
# or
git bisect bad
ホットリロードを適用しておくことで、スムーズに動作確認が進められます。
bisect中の注意点
原因が判明したらresetでbisectから抜ける
原因が判明したらresetでbisectから抜けるようにしましょう。
例えば、bisectはコミットを行き来するので、途中でHEAD (no branch)のようなdetached HEAD状態になることがあります。
正常な動作ですが、このまま作業するとコミットが混乱しやすいです。
即座にRevertを進められるよう、気をつけましょう。
git bisect reset
git checkout master
git bisectが刺さるポイント
- 即座に判定できる
- ローカルですぐにでも確認できる状況では大変役に立ちます
- 一方で、本番環境でしか再現しないなど時間がかかる場合はログを仕込むなど別の対応が必要です
- 判定基準が明確
- 二分探索では途中の判定が間違ってしまうと特定が難しくなります
- 動作が遅いなど基準が曖昧な場合は他の手段がベターかもしれません
- 確実に動いていたタイミングがある
- バグがいつ発生したのか不明でかなり遡ることがあっても、二分探索は高速にチェックできます(コミットがN件であればlogN回)
- 確実に動いているタイミングまで思い切って遡ってしまうと良いでしょう
- PR番号はコミットメッセージから拾えることがある
- スカッシュを採用しているとコミットがPR単位で行われています
- コミットメッセージからPRまで特定できるとRevertも楽になります
まとめ
git bisect は、原因が不明でも 「動く/動かない」だけで原因コミットに到達できるのが強みです。
今回のように「goodタグがあり」「手順が安定している」ケースでは、原因特定→PR特定→単独revertまでが一直線で進められます。
ClaudeCodeやCopilot, Cursor, codexなどバグ調査も楽になってきた昨今ですが、そもそも原因が特定できないケースにハマってしまうこともあります。
そんな時はコミット単位で特定できるとよりスムーズなバグ調査が行えることでしょう。