14 ポイント 投稿者 GN⁺ 2025-11-04 | 2件のコメント | WhatsAppで共有
  • 二分探索(binary search) の概念は面接問題だけでなく、実際の開発ツールである Git でも活用される
  • 大規模な monorepo 環境でテストが突然失敗したとき、ログだけでは原因を追跡しにくい状況が発生する
  • ある同僚が 良いコミットと悪いコミットを指定して git bisect で自動探索を行い、バグが始まった問題のコミットを正確に見つけ出した
  • 各段階でスクリプトを実行し、テスト結果に応じてコミットを自動分類しながら、最初に失敗したコミットを特定する
  • 二分探索の原理を活用した git bisect は、大規模コードベースでバグの原因を迅速に追跡する強力なツールである

アルゴリズムと実例

  • 二分探索(binary search) アルゴリズムは単なる面接問題にとどまらず、実際のデバッグツールでも中核となる原理として機能する
  • git bisect は「バグを最初に導入したコミット(first bad commit)」を見つけるために二分探索を使うツールとして利用できる
    • Leetcode の “First Bad Version” 問題と似た原理で動作する

実際の業務環境での問題状況

  • 大規模な monorepo を使う環境では、1日に数百〜数千のコミットが発生する
  • 特定のテスト失敗の原因は、ログだけでは追跡が難しい
  • 失敗の原因は、リモート呼び出しに必要なトークンを取得する設定ファイルの文字列変更で、別のアカウントを参照するようになったためテストが失敗したことにあった
  • この変更は統合テストを通過したが、実際には問題を引き起こしており、膨大なコミットの中のどの時点で発生したのかを見つけるのが難しかった

git bisect を使った問題解決

  • 他チームの同僚が git bisect コマンドを使って問題のコミットを素早く特定した
    • 良いコミット(good)悪いコミット(bad) を指定した後、自動で中間のコミットをチェックアウトしてテストを実行し、原因を絞り込んでいった
    • 各テスト実行には時間がかかったが、最終的に 正確に問題を導入したコミットを見つけ出した
    • そのコミットを元に戻すと、テストはすべて正常に復旧した

git bisect の実行過程

  • 例となるコミット履歴
    • Commit 1: 初期コミット(正常)
    • Commit 2: リファクタリング(正常)
    • Commit 3: バグ導入(エラー発生)
    • Commit 4~10: 非機能変更(エラー継続)
  • 実行コマンド例
    git bisect start  
    git bisect bad HEAD  
    git bisect good HEAD~9  
    git bisect run ./test_script.sh  
    
  • テストスクリプト(test_script.sh) は 成功時に 0、失敗時に異常コードを返す
  • git bisect は中間コミットを自動でチェックアウトしてテストスクリプトを実行し、
    テスト失敗時点を基準に 最初の悪いコミットを特定する
  • 出力結果から b982ed9373fe235fe61c74b15faf264bc7142398 コミットが 最初のバグコミットであることが確認された

結論

  • git bisect は、二分探索の原理をコード履歴の探索に適用した実用的なツールである
  • 大規模リポジトリや複雑な変更履歴でも、バグが導入された時点を迅速に追跡できる
  • テスト自動化と組み合わせることで、大規模コードベースでも安定したデバッグが可能になる

2件のコメント

 
kandk 2025-11-04

こうした問題があるため、TBD(trunk-based-develop)を使います。

 
GN⁺ 2025-11-04
Hacker Newsのコメント
  • 以前、テストカバレッジもなく抽象化もひどい巨大なコードベースで働いていたとき、git bisectはほぼ唯一まともに使えるツールだった。
    コードが複雑すぎて論理的にバグを追跡するのが不可能だったため、どのコミットで問題が発生したのかを見つけるほうがずっと簡単だった。
    しかし、品質の高いコードベースでは、あえて bisect を使う必要はなかった。各コンポーネントを独立してテストでき、可観測性(observability) も十分に整っていて、どこを見ればよいかが明確だった。

    • git bisectは決して不要ではないと思う。単にバグを見つけるだけでなく、そのバグがなぜ発生したのかを理解させてくれる。
      コミットメッセージが充実しているプロジェクトなら、bisect を通じて過去のコミットの文脈を把握し、その内容をバグ修正コミットに反映できる。こうした循環がコミット文化そのものを強化する。
    • 以前、OSSプログラムでおかしな文字列が混入するバグを見つけたことがある。Cコードで、原因は初期化されていない変数だった。
      直接追跡するのは不可能だったが、bisect スクリプトを書いて30分ほど回したら、問題のコミットを正確に特定できた。
    • git bisectはもともとLinuxカーネルのリグレッション(regression) を見つけるために導入されたツールだった。
      ハードウェアドライバのようにテストが不可能な場合でも、一般ユーザーが自分でカーネルを bisect して問題のコミットを特定できるようになった。
      以前はメールで開発者に助けを求める必要があったが、今ではユーザー自身で問題を絞り込めるようになった。
    • 単にバグを直すことが目的なら、bisect は不要かもしれない。しかし、バグがいつから存在していたのかを知る必要がある場合もある。
      たとえば誤って処理されたデータの範囲を追跡したり、「これはバグなのか仕様なのか」を判断したりするときに有用だ。
    • ときには、バグがいつ修正されたのかを知る必要もある。
      たとえば顧客が6年前のバージョンで問題を抱えているとき、4年前のバージョンにアップグレードすれば解決するか確認できる。
      あるいはコードが大規模にリファクタリングされたとき、そのバグ修正が意図的だったのか偶然だったのかも把握できる。
  • git bisectはうまく機能するときは素晴らしいが、あらゆるバグを見つけられるわけではない。
    バグによっては、導入された当初は症状がなく、後の別の変更によって表面化することもある。
    このような場合、bisect の前提(良いコミットと悪いコミットの間でバグが一度だけ登場する)が崩れる。
    テスト不能なコミットはskipできるが、それが問題のコミットだった場合、結果が曖昧になる。

  • 最近初めて本格的にgit bisectを使ってみたが、ほとんど魔法のようだった。
    同じ名前の関数が2つあり、コードフォーマット作業の最中に正しい関数の import が削除されたことで問題が発生していた。
    何度もコードを見直したが、bisect で問題のコミットを特定するまでは原因がまったく分からなかった。

  • 私はたいてい、バグが発生したファイルや関数の範囲をすでに把握しているので、bisect を頻繁には使わない。
    その代わり、git log -L :func_name:path/to/file.cコマンドで特定の関数の変更履歴を追跡する。
    .gitattributesの設定が必要だ。

    • .gitattributesの設定について知りたいという質問があった。どのような内容が必要なのか、もっと詳しく知りたいという反応だった。
    • 毎日 bisect を使うという人もいた。ワークフローがまったく異なる。
    • C++のように多態的な関数がある場合、git log -Lは弱い。同じ名前のオーバーロード関数のうち、特定のバージョンを追跡しにくいためだ。
    • .gitattributesがなければ、git log -Sで特定の文字列を含むコミットを探すのも一つの方法だ。
  • テストスクリプトではexit code 125を覚えておくとよい。
    ビルド失敗のようにテスト結果を判定できない場合、125を返せば bisect がそのコミットをスキップする。
    関連内容は私のブログ記事にまとめてある。

    • マージコミットが CI 通過地点を意味するリポジトリでは、git bisect --first-parentを使うと便利だ。
      こうすると「どのPRがバグを持ち込んだのか」を素早く見つけられ、その後そのブランチでさらに詳細な bisect をもう一度回せばよい。
  • フレーキーテスト(flaky test) が発生したとき、bisect は真価を発揮する。
    レースコンディションのせいで、確信を持つためにテストを何十万回も回す必要がある場合でも、bisect スクリプトをバックグラウンドで走らせておけば現実的に解決できる。

    • このような場合は、ベイズ的二分探索を適用すればテスト回数を大幅に減らせそうだ。
  • 最近、Svelteで作った音楽プレーヤープロジェクト(lets-make-sweet-music.com)で、bisect を使ってバグの原因を突き止めた。
    テストもなく、エラーログもなかったが、dependabotの更新でコミットが増えていて追跡が難しかった。
    bisect のおかげで問題のコミットを見つけ出せたが、原因は私が差し替えたファイルがイベントの多重バインディング機能を実装していなかったことだった。
    コミットを小さく保てば、bisect で見つけた問題の原因を素早く絞り込める。

  • 誰かが「面接で二分探索(binary search) を学ばなければならないなんて無理がある」と言っていたが、git bisectはその概念を実際に示す良い例だ。
    ただし自分で実装する必要はない。たいていの言語ではすでに標準ライブラリとして提供されている。

    • 興味深いことに、二分探索は1940年代に最初に提案されたが、バグのない実装が登場したのは1960年代になってからだという話がある。
      中間インデックスを計算するときに(low + high) / 2とするとオーバーフローが起こりうる。
    • 個人的には、すべての開発者が一度くらい任意精度整数を扱える言語(例: Python)で二分探索を自分で実装してみるとよいと思う。
      不変条件(invariant) に基づく思考を鍛える最高の練習だ。
  • Gitには bisect のほかにも、log -Llog -Sblameのような優れたコード探索ツールがある。
    以前、このテーマについてブログ記事を書いたことがある。