LabHub
学习 学习路径 课程

Git 实战

从 1000 个提交里十次找出真凶

在 LabHub 中继续学习

一句话总结

从“两个月前还能正常运行,现在却不行了”定位到具体一个提交,所需的检查次数不是提交数量,而是 log₂(提交数量)。1,000 个提交只需检查 10 次。

概念图: log₂(提交数量) · 退出码 · skip · 可以构建并运行的状态

为什么需要它

收到一条错误报告:“以前明明运行得很好。”

打开 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。

生产现场中的表现

二分查找被卡住时如何继续

git bisect 很强大,但实际运行时经常卡在无法判断的提交上。此时有几种处理方法。

**跳过无法构建的提交。**如果把它标记为 bad,结果就会偏离。

git bisect skip                    # 이 커밋으로는 판단할 수 없다
git bisect skip v2.1..v2.2         # 구간 통째로

跳过太多时,范围将无法继续缩小。此时应让判断脚本在构建失败时返回 125bisect 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 这次会只把范围缩小到两个候选项便停止。最后一步由人来决定——自动化应该在哪里结束,正是本实验的结论。