TT Lab
はじめる
学ぶ 学習パス コース

Git実戦

1,000のコミットから犯人を10回で見つける

TT Labで続きを見る

一言でいうと

「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です。

現場での姿

二分探索が詰まるときの突破のしかた

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つまでしか絞り込めず、止まります。最後の一歩は、人が決めます。自動化が終わる場所がどこかが、このラボの結論です。