19、Git bisect:二分查找、自动二分、手动二分、定位bug引入点

说实话,Git 里我最怕听到的一句话就是:「这个 bug 上周还没有,谁改的?」

项目大了,提交几百个,你一个个翻?翻到天亮也未必找得到。这时候,git bisect 就是你的救命稻草。

它的核心思想很简单:二分查找。你告诉 Git「这个版本是好的,那个版本是坏的」,它自动帮你从中间切一刀,反复测试,直到揪出那个罪魁祸首的提交。

二分查找的基本原理

你想想看,如果我有 100 个提交,线性查找最坏要测 100 次。二分查找呢?最多 7 次。这就是算法的力量。

Git bisect 的工作原理就是:

  • 你标记一个「坏」的提交(比如当前有 bug 的版本)
  • 你标记一个「好」的提交(比如之前没 bug 的版本)
  • Git 自动 checkout 中间的那个提交
  • 你测试,告诉 Git 这个版本是好是坏
  • 重复,直到找到第一个引入 bug 的提交

核心概念:bisect 不是帮你修 bug,而是帮你找到「哪个提交引入了 bug」。找到人,才能谈怎么修。

手动二分:一步步来

我记得刚用 bisect 时,还不太信任它。手动一步步走,心里踏实些。

手动二分的流程:

  1. 启动 bisect 会话:git bisect start
  2. 标记当前版本为坏:git bisect bad
  3. 标记一个已知的好版本:git bisect good <commit-hash>
  4. Git 自动 checkout 中间提交,你测试
  5. 测试通过:git bisect good
  6. 测试不通过:git bisect bad
  7. 重复直到 Git 告诉你「第一个坏提交是 xxx」
  8. 结束: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 次二分查找,定位到第一个坏提交。

Git Bisect 二分查找流程 初始状态: G1 G2 G3 G4 G5 G6 G7 G8 B9 B10 good bad 第1次: G5 测试中 第2次: G7 测试中 第3次: G8 测试中 结果: B9 ← 第一个坏提交

你看,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

找到提交后,我一般会做三件事:

  1. 看提交信息,了解改动的意图
  2. 看 diff,找到具体改了什么
  3. 跟提交者沟通,确认是不是真的引入了 bug

记住:bisect 找到的提交,不一定是 bug 的「根因」,但一定是「第一个出现 bug 的提交」。有时候 bug 是多个提交累积导致的,bisect 找到的是「压死骆驼的最后一根稻草」。这时候需要结合代码审查来定位真正的根因。

好了,关于 bisect 就聊这么多。说白了,它就是 Git 给你的一把「时间回溯」的钥匙。用好它,定位 bug 的效率能提升一个数量级。下次遇到「不知道哪个提交引入的 bug」,别一个个翻了,试试 bisect 吧。