Bisect 在 skipped commit 上卡住

git bisect 二分查找 bug 时,因为大量 commit 无法构建而反复执行 skip,最终 bisect 无法确定 first bad commit 就退出了。本文给出绕过 skip 区间、精准定位 bug 引入点的操作技巧。

你在用 git bisect 追踪一个回归 bug,设置了 goodbad 边界后开始二分。但仓库历史里有一段时间的 commit 因为缺少依赖、构建脚本变化或者测试环境不一致无法编译,你只能反复执行 git bisect skip。最后 bisect 打印出:「There are only ‘skip’ped commits left to test. The first bad commit could be any of…」,列出一堆 SHA 就退出了,什么结论都没有。Bisect 被 skip 区间淹没,无法给出唯一的 first bad commit。

常见原因

1. 历史里有一段构建彻底坏掉的 commit(build breakage window)

最常见。某段时间里多人互相依赖的功能同时开发,中间有几十个 commit 构建失败。这些 commit 都需要 skip,bisect 的二分范围被大量 skip 点填满,无法缩小到单个 commit。

怎么判断git log --oneline <good>..<bad> | wc -l 查看总 commit 数;再估算需要 skip 的 commit 数量,若超过总数的 30%,bisect 很可能无法收敛。

2. skip 的 commit 恰好在 good/bad 边界区域

若 skip 的 commit 正好夹在唯一 bad commit 前后,bisect 无法排除这些 commit 的嫌疑,最终输出的「可能是 bad」的列表就包含了真正的 bad commit 和无法判断的 skip commit,混在一起。

怎么判断git bisect log 查看 bisect 过程,若最后几步 skip 的 commit 都集中在同一个小范围,说明 bad commit 就在这个范围内。

3. 测试脚本本身有 flaky 行为导致误判为 skip

自动化 bisect(git bisect run)的测试脚本在某些 commit 上随机返回 125(skip),而这些 commit 实际上是可以判断的,被误 skip 掉了。

怎么判断:手动 checkout 某个被 skip 的 commit,多次运行测试,若结果不一致,说明测试脚本有 flaky 问题。

4. 依赖 lockfile 与 commit 时期不匹配

package-lock.jsonCargo.lock 是 checkout 到历史 commit 时的旧版本,而本机的 node/cargo 版本是新的,导致安装依赖时报错。不是 commit 本身有问题,而是构建环境不兼容。

怎么判断node --version 与历史 commit 时期的 Node.js 版本对比;若差距大(如从 18 升到 22),老 commit 的依赖可能无法在新环境安装。

5. bisect 范围设置错误(good/bad 方向颠倒)

误把 bad commit(有 bug 的那个)标记为 good,或者 good/bad 的时间顺序搞反,导致 bisect 在错误方向搜索,遇到的都是”无关”的 commit,只能 skip。

怎么判断git log --oneline <good-sha>..<bad-sha> 若输出为空(即 bad 比 good 更旧),说明方向颠倒了。

最短修复路径

Step 1:查看当前 bisect 进度

git bisect log
git bisect visualize --oneline  # 或 git bisect view

Step 2:若 skip 区间集中,手动 checkout 区间内的候选 commit 逐一测试

bisect 输出了几个候选 SHA 时,手动逐一检查:

git bisect reset
# 备份 bisect log 以便重新开始
git bisect log > /tmp/bisect-backup.log

# 手动检查每个候选
git checkout <candidate-sha-1>
# 运行测试,判断 good/bad

Step 3:用 git bisect skip 精准跳过已知无法测试的范围

git bisect start
git bisect bad <known-bad-sha>
git bisect good <known-good-sha>

# 跳过已知不可用的 commit 范围
git bisect skip <start-sha>..<end-sha>

Step 4:使用 Docker 或 nvm 为历史 commit 恢复兼容的构建环境

# 用 nvm 切换到历史 commit 时期的 Node 版本
nvm use 18
npm install
npm test

# 若测试通过,标记为 good
git bisect good

# 若失败,标记为 bad
git bisect bad

Step 5:自动化 bisect 时给测试脚本加 flaky 重试

# bisect-test.sh
#!/bin/sh
npm install 2>/dev/null
# 运行 3 次,若都失败才判定为 bad
for i in 1 2 3; do
  npm test -- --testNamePattern="regression test" && exit 0
done
exit 1
git bisect run ./bisect-test.sh

Step 6:若范围太大,缩小 good/bad 边界

git bisect reset
# 找到更近期的 good commit(而不是几个月前的)
git bisect start
git bisect bad HEAD
git bisect good HEAD~20  # 先测最近 20 个 commit

预防建议

  • 保持仓库历史中每个 commit 都可构建(尤其是合并进 main 的 commit),CI 的 build check 是保障,不允许 broken build 进 main。
  • 将自动化测试封装成 Docker 镜像,固定构建环境版本,这样 checkout 到任何历史 commit 都能用同一个镜像构建,避免环境兼容问题导致的 skip。
  • 维护 LTS(长期支持)分支或定期打构建快照 tag,bisect 时以这些快照为 good/bad 边界起点,减少需要测试的 commit 数量。
  • 对于 flaky 测试,在测试框架层面添加自动重试(如 Jest 的 --testRetries=3),bisect 用的测试脚本应该是稳定的。
  • 重大功能用 feature flag 控制,而不是单纯靠分支,bisect 时可以快速通过 flag 定位是哪个功能引入的回归。
  • 执行 bisect 之前记录 git bisect start 前的 HEAD,出现问题时 git bisect reset 后能快速回到原始状态。

常见问答 (FAQ)

Q: git bisect run 的退出码规则是什么? A: 0 = good(bug 不在此 commit),1-127(除 125)= bad(bug 在此 commit),125 = skip(此 commit 无法测试),128+ = 终止 bisect(严重错误)。注意 125 是特殊的 skip 退出码,测试脚本里不要把它用于其他含义。

Q: bisect 找到了 first bad commit,但那个 commit 是个 squash merge 怎么办? A: squash merge 把多个 commit 压缩成一个,bisect 只能定位到这个 squash commit,无法进一步细分。需要在那个 squash commit 里手动审查改动,或者在 squash 之前的 feature 分支历史里再做一次 bisect。

Q: bisect 结束后能看到完整的测试过程记录吗? A: git bisect log 显示完整的 bisect 会话记录,包括每个测试 commit 的 SHA 和标记(good/bad/skip)。若 bisect 已经 reset,这些记录就消失了,建议在 bisect 期间用 git bisect log > bisect.log 保存。

Q: 多个 bug 同时存在时,bisect 只能找到第一个引入点,如何处理? A: bisect 设计为找一个 regression 的引入点。若有多个独立 bug,需要多次 bisect,每次聚焦一个具体的测试用例。或者使用 git bisect run 时测试脚本只检查单一的、具体的行为,不要一次检查多个症状。

相关阅读

标签: #git #version-control #排查