你在用 git bisect 追踪一个回归 bug,设置了 good 和 bad 边界后开始二分。但仓库历史里有一段时间的 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.json 或 Cargo.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 时测试脚本只检查单一的、具体的行为,不要一次检查多个症状。
相关阅读
- Rebase 之后 commit 消失了
- Stash 在 checkout 之后看不到了
- 找回 Git 历史里的旧版本文件
- Cherry-pick 解冲突后变成空 commit
- AI 意外修改了 Git 历史
- AI 帮你回滚代码改动
标签: #git #version-control #排查