1,000のコミットから犯人を10回で見つける
一言でいうと
「2か月前は動いていたのに、今は動かない」から、コミットを1つ特定するのに必要な検査の回数は、コミットの数ではなく、log₂(コミット数)です。1,000個なら10回です。
なぜ必要なのか
バグレポートが届きました。「前は問題なく動いていたのですが」。
git logを開くと、その間にコミットが800個あります。1つずつ確認すると800回で、目で流し見すると、「関係がありそうな」コミットに偏ります。そして、経験上、犯人はたいてい関係がなさそうなコミットです。
git bisectは、二分探索を自動で行ってくれます。
git bisect start
git bisect bad # 지금은 깨져 있다
git bisect good v1.4.0 # 이 버전에서는 됐다
# → git 이 중간 커밋으로 체크아웃한다
# 확인 후
git bisect good 또는 git bisect bad
# 반복. 10번이면 끝난다.
git bisect reset
自動化: bisect run
検査をスクリプトで書けるなら、人が座っている必要もありません。
git bisect start HEAD v1.4.0
git bisect run ./check.sh
check.shは、終了コードで語ります。
| 終了コード | 意味 |
|---|---|
| 0 | good |
| 1–124, 126, 127 | bad |
| 125 | skip: このコミットは判定不能(ビルド失敗など) |
125が重要です。途中にビルドが壊れたコミットが混じっているとき、それをbadと判定すると、見当違いの場所に収束します。判定できないなら、skipを返す必要があります。
#!/bin/bash
make build || exit 125 # 빌드 실패 = 판정 불가
./run-test.sh || exit 1 # 테스트 실패 = bad
exit 0 # 통과 = good
bisectのための前提
二分がうまくいくには、コミット1つ1つがビルドでき、実行できる状態である必要があります。「WIP」「作業中」「とりあえずコミット」のようなコミットが大量にあると、skipが増えるだけです。普段から、小さく完結したコミットを積む習慣が、ここで効いてきます。
log -S: そのコードがいつ入ったか
特定の文字列が追加または削除されたコミットだけを探します。
git log -S 'timeout=3' --oneline
「この設定値は、いつ3に変わったのだろう」に、一度で答えます。正規表現で探すには、-Gを使います。
git log -p -- path/to/fileで、ファイル1つの変更履歴だけを見ることもでき、--followを付けると、名前が変更された履歴までたどります。
blame: 誰がではなく、なぜ
git blameは、各行を最後に修正したコミットを表示します。目的は、責任者探しではなく、文脈探しです。その行がなぜそう書かれたのかは、コミットメッセージと、そのコミットの他の変更の中にあります。
git blame -L 40,60 src/app.py # 40~60행만
git blame -w # 공백 변경 무시
git blame -C # 다른 파일에서 옮겨온 코드도 추적
-wが特に便利です。フォーマッターを一度実行したリポジトリでは、すべての行が「フォーマットのコミット」として表示されますが、-wは、それを飛ばして、実際の変更を探してくれます。
reflog: 失ったものを取り戻す
git reflogは、HEADが動いたすべての記録です。リベースを間違えたり、ブランチを削除したりしても、コミット自体は、しばらくの間生きています。
git reflog
# a1b2c3d HEAD@{5}: rebase (finish): returning to refs/heads/main
# 9f8e7d6 HEAD@{6}: commit: 잃어버린 작업
git checkout -b rescue 9f8e7d6
「gitでコミットしたものは、そう簡単には消えない」という言葉の根拠が、reflogです。
現場での姿
- 性能がいつから悪くなったのかわからない → ベンチマークスクリプトで
bisect runを使います。 - 奇妙な定数がコードに埋め込まれている →
log -Sで、入ったコミットと理由を探します。 - リベース中にコミットが消えた → reflogで復旧します。
二分探索が詰まるときの突破のしかた
git bisectは強力ですが、実際に回してみると、判定できないコミットで詰まります。そのとき使う手が、いくつかあります。
ビルドできないコミットは飛ばします。badと印を付けると、結果がずれます。
git bisect skip # 이 커밋으로는 판단할 수 없다
git bisect skip v2.1..v2.2 # 구간 통째로
飛ばしすぎると範囲が絞り込めないため、そのときは、判定スクリプトがビルド失敗を125で返すようにします。bisect runは、125を「スキップ」と読みます。
cat > /tmp/check.sh <<'EOF'
#!/bin/bash
make -s build || exit 125 # 판정 불가
./run-test || exit 1 # 나쁨
exit 0 # 좋음
EOF
git bisect run bash /tmp/check.sh
マージコミットが多いときは、first-parentだけを見ます。機能ブランチ内の中間コミットまで見て回ると時間がかかり、それらのコミットは、そもそもテストされていない状態です。
git bisect start --first-parent BAD GOOD
症状が断続的なら、二分探索は嘘をつきます。10回に1回起きる問題なら、判定スクリプトが複数回試すようにします。それでも確率的なら、二分探索より、log -Sでそのコードがいつ入ったかを探すほうが速いです。
git log -S 'setTimeout(retry' --oneline -- src/
git log -L :handleRetry:src/client.js # 그 함수의 변천사만
終わったら必ず元に戻します。git bisect resetをしないと、detached HEADのままになり、その状態でコミットすると、あとで探しにくくなります。
元に戻すことが目的ではありません。犯人のコミットを見つけたからといって、そのまま取り消すと、そのコミットが直していた別の問題が復活します。なぜその変更がこの症状を生むのかまで見て直すことが、二分探索の価値です。
次のラボですること
40個の履歴を自分で作って、犯人を探します。その履歴には、判定できないコミットが1つ混じっているため、125を使わないと、bisectが見当違いのコミットを、自信を持って指名します。2つのスクリプトを並べて実行し、その違いを目で見ます。
そして、125を正しく使うと、今度はbisectが候補を2つまでしか絞り込めず、止まります。最後の一歩は、人が決めます。自動化が終わる場所がどこかが、このラボの結論です。