从 1000 个提交里十次找出真凶
一句话总结
从“两个月前还能正常运行,现在却不行了”定位到具体一个提交,所需的检查次数不是提交数量,而是 log₂(提交数量)。1,000 个提交只需检查 10 次。
为什么需要它
收到一条错误报告:“以前明明运行得很好。”
打开 git log,发现期间共有 800 个提交。逐个检查需要 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 的前提
要让二分查找顺利工作,每一个提交都应处于可以构建并运行的状态。如果充满“WIP”“进行中”“先提交再说”之类的提交,只会不断增加 skip。平时保持提交小而完整的习惯,会在这里体现价值。
log -S——那段代码是什么时候加入的
只查找曾经添加或删除特定字符串的提交。
git log -S 'timeout=3' --oneline
它可以直接回答“这个配置值是什么时候改成 3 的?”需要使用正则表达式时,可以用 -G。
还可以通过 git log -p -- path/to/file 只查看一个文件的变更历史;加上 --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 移动过的所有位置。即使 rebase 出错或删除了分支,提交对象本身仍会存活一段时间。
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找到引入它的提交和原因。 - rebase 中提交消失 → 通过 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
**症状具有间歇性时,二分查找会说谎。**如果问题十次才出现一次,应让判断脚本重复尝试多次。如果仍具有概率性,与其二分查找,不如使用 log -S 查找相关代码何时加入,通常更快。
git log -S 'setTimeout(retry' --oneline -- src/
git log -L :handleRetry:src/client.js # 그 함수의 변천사만
**完成后必须恢复。**如果不执行 git bisect reset,仓库会停留在 detached HEAD 状态;此时继续提交,之后将很难找到该提交。
目的不是直接撤销。找到罪魁祸首后立即 revert,可能会让该提交原本修复的其他问题重新出现。二分查找的真正价值,是继续理解为什么这项变更会产生当前症状,然后进行修复。
下一项实验要做什么
你将亲自创建一段包含 40 个提交的历史并寻找罪魁祸首。其中混入了一个无法判断的提交;如果不使用 125,bisect 会非常自信地指出错误提交。你会并排运行两个脚本,直观观察差异。
正确使用 125 后,bisect 这次会只把范围缩小到两个候选项便停止。最后一步由人来决定——自动化应该在哪里结束,正是本实验的结论。