19、Git bisect:二分查找、自动二分、手动二分、定位bug引入点
说实话,Git 里我最怕听到的一句话就是:「这个 bug 上周还没有,谁改的?」
项目大了,提交几百个,你一个个翻?翻到天亮也未必找得到。这时候,git bisect 就是你的救命稻草。
它的核心思想很简单:二分查找。你告诉 Git「这个版本是好的,那个版本是坏的」,它自动帮你从中间切一刀,反复测试,直到揪出那个罪魁祸首的提交。
二分查找的基本原理
你想想看,如果我有 100 个提交,线性查找最坏要测 100 次。二分查找呢?最多 7 次。这就是算法的力量。
Git bisect 的工作原理就是:
- 你标记一个「坏」的提交(比如当前有 bug 的版本)
- 你标记一个「好」的提交(比如之前没 bug 的版本)
- Git 自动 checkout 中间的那个提交
- 你测试,告诉 Git 这个版本是好是坏
- 重复,直到找到第一个引入 bug 的提交
核心概念:bisect 不是帮你修 bug,而是帮你找到「哪个提交引入了 bug」。找到人,才能谈怎么修。
手动二分:一步步来
我记得刚用 bisect 时,还不太信任它。手动一步步走,心里踏实些。
手动二分的流程:
- 启动 bisect 会话:
git bisect start - 标记当前版本为坏:
git bisect bad - 标记一个已知的好版本:
git bisect good <commit-hash> - Git 自动 checkout 中间提交,你测试
- 测试通过:
git bisect good - 测试不通过:
git bisect bad - 重复直到 Git 告诉你「第一个坏提交是 xxx」
- 结束:
git bisect reset
# 启动 bisect
git bisect start
# 标记当前为坏
git bisect bad
# 标记 v2.0 为好的
git bisect good v2.0
# Git 会 checkout 一个中间版本
# 你测试后...
git bisect good # 如果这个版本没问题
# 或者
git bisect bad # 如果这个版本有问题
# 找到后,回到正常状态
git bisect reset
我的习惯:每次 bisect 前,我会先确认「好版本」真的没问题。有一次我标记错了,结果 bisect 了半天,最后发现是那个「好版本」本身就有 bug。嗯,白忙活两小时。
自动二分:让脚本替你跑
手动二分虽然直观,但每次都要手动测试,效率还是低。如果你有一个自动化测试脚本,可以让 Git 替你跑。
自动二分的命令:
# 自动二分,test.sh 返回 0 表示好,非 0 表示坏
git bisect start HEAD v2.0
git bisect run ./test.sh
这里的关键是:你的脚本必须用退出码来告诉 Git 结果。
- 退出码 0:这个提交是好的
- 退出码 125:这个提交无法测试(跳过)
- 其他非 0 退出码:这个提交是坏的
我曾经踩过的坑:有一次我写了个测试脚本,忘了处理编译失败的情况。结果 bisect 到一个中间提交,代码根本编译不过,脚本直接报错退出了。Git 以为这个提交是坏的,但实际上只是编译环境问题。后来我加了 exit 125 来处理编译失败的情况,才搞定。
跳过无法测试的提交
实际项目中,有些提交确实没法测试。比如:
- 提交只改了 README,跟 bug 无关
- 提交导致编译失败,没法运行测试
- 提交改动了测试框架本身
这时候用 git bisect skip 跳过:
# 手动跳过当前提交
git bisect skip
# 或者在自动二分中,脚本返回 125 表示跳过
exit 125
Git 会智能地选择其他提交来测试,不会因为一个坏提交就卡住。
bisect 的视觉化:理解二分过程
下面这张图展示了 bisect 的完整流程。从 10 个提交中,通过 4 次二分查找,定位到第一个坏提交。
你看,10 个提交,只测了 4 次就找到了。如果手动一个个查,可能要测 8 次。这就是二分法的威力。
bisect 的高级技巧
用多了,你会发现一些实用的小技巧:
| 场景 | 命令 | 说明 |
|---|---|---|
| 查看 bisect 进度 | git bisect log |
显示当前 bisect 会话的完整日志 |
| 可视化 bisect 过程 | git bisect visualize |
用 gitk 或图形工具展示当前 bisect 状态 |
| 查看剩余步骤 | git bisect view |
估算还需要测试多少次 |
| 回退一步 | git bisect replay |
从日志中重新执行 bisect 过程 |
| 跳过多个提交 | git bisect skip <range> |
跳过某个范围内的所有提交 |
我的经验:如果 bisect 过程中发现某个提交「既不是好也不是坏」,比如重构代码但没改逻辑,我会用 git bisect skip 跳过。但要注意,跳过的提交太多会影响二分效率。我一般控制在 2-3 个以内。
bisect 的注意事项
用 bisect 这么多年,我总结了几条铁律:
- 确保「好版本」真的没问题:如果好版本本身就有 bug,bisect 会给出错误结果。我习惯用 tag 或稳定分支作为好版本。
- bisect 会修改工作区:它会 checkout 不同的提交。记得先 stash 或 commit 当前工作,免得丢代码。
- 大项目注意性能:如果项目编译需要 10 分钟,bisect 10 次就是 100 分钟。我一般先缩小范围,再用 bisect。
- bisect 不是万能的:如果 bug 是间歇性的,或者依赖外部环境,bisect 可能找不到。这时候我会结合
git log -p手动排查。
我曾经犯过的错:有一次我在 bisect 过程中,忘了 stash 本地修改。结果 Git checkout 到另一个提交时,把我的本地修改覆盖了。嗯,那天的晚饭都没吃好。后来我养成了习惯:bisect 前先 git stash,结束后 git stash pop。
实战:用 bisect 定位一个真实 bug
假设我们的项目有个 bug:用户登录后页面白屏。我知道 v2.0 是好的,当前版本是坏的。
# 1. 启动 bisect
git bisect start
# 2. 标记当前为坏
git bisect bad
# 3. 标记 v2.0 为好
git bisect good v2.0
# 4. Git 自动 checkout 中间版本
# 输出:Bisecting: 5 revisions left to test after this (roughly 3 steps)
# 5. 运行测试脚本
npm test
# 6. 测试通过,标记为好
git bisect good
# 7. 继续测试下一个中间版本
# 输出:Bisecting: 2 revisions left to test after this (roughly 2 steps)
# 8. 测试失败,标记为坏
git bisect bad
# 9. 继续...
# 输出:Bisecting: 1 revision left to test after this (roughly 1 step)
# 10. 测试失败,标记为坏
git bisect bad
# 11. Git 输出结果
# 输出:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 is the first bad commit
# 12. 查看这个提交的详情
git show a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
# 13. 结束 bisect
git bisect reset
找到提交后,我一般会做三件事:
- 看提交信息,了解改动的意图
- 看 diff,找到具体改了什么
- 跟提交者沟通,确认是不是真的引入了 bug
记住:bisect 找到的提交,不一定是 bug 的「根因」,但一定是「第一个出现 bug 的提交」。有时候 bug 是多个提交累积导致的,bisect 找到的是「压死骆驼的最后一根稻草」。这时候需要结合代码审查来定位真正的根因。
好了,关于 bisect 就聊这么多。说白了,它就是 Git 给你的一把「时间回溯」的钥匙。用好它,定位 bug 的效率能提升一个数量级。下次遇到「不知道哪个提交引入的 bug」,别一个个翻了,试试 bisect 吧。