昨晚在 CI 上通过的 payment.spec.js 在今早的构建中失败了。自最后一个稳定发行版标签合并以来的提交数量为 200 个。如果在 git log 上向上滚动并浏览 diff,这相当于线性搜索,因此对每次提交运行构建和测试会消耗相当多的时间。让我们假定这并非亲身经历的案例,而是一个发生此类回归的虚构场景。
此时,git bisect 采用“每次排除一半”的方式,而不是“逐一检查”。以确认正常的提交(good)和发现失败的提交(bad)为边界,Git 会检出候选集合中间的提交。用户需要对该提交进行测试,并回答是 good 还是 bad。由于每次判定后候选数量减半,即使有 1,000 个提交,最多只需约 10 次(log₂1000 ≈ 10)即可缩小范围 1。如果是 200 个提交,则大约收敛到 8 次。
不过,这一计算仅在所有判定都反映实际提交行为时成立。如果将构建损坏导致无法执行的提交错误地判定为 bad,或者自动化脚本甚至无法执行并返回 exit code 127,情况就会变得不同。Git 可能会选择错误的一半,或者将所有提交标记为 bad,从而使结果毫无意义 2。
开始搜索前,准备复现条件和边界
要让 git bisect 正常工作,必须明确两个状态点。当前 HEAD 是 bad 意味着回归可以复现,而过去的某个标签是 good 意味着在该时间点不存在回归。如果这一前提动摇,整个搜索过程就会偏离方向。
Nodelog Engineer 指出,如果确定为 good 的点实际上已经处于损坏状态,结果可能会失真。因此,建议选择一个“确定在 bug 出现之前”的标签作为 good,并尽量向前选取,而不是选择“最近看起来正常的提交” 1。
当判定由脚本而非人工执行时,脚本的执行位置也会影响结果。tms-outsource 解释说,将判定脚本放在仓库之外更安全。原因是旧提交中不存在后来添加的测试文件,这可能导致因测试文件缺失而产生错误的失败判定 2。如果尝试在搜索范围内的旧提交中执行在 200 个提交后添加的 payment.spec.js,就无法区分失败是由于该提交的代码问题还是测试文件的存在与否。
因此,验证回归的命令应放在仓库外的路径,或者包含依赖项安装(如 npm ci 等)并进行隔离准备 1。这样做可以避免因旧提交的环境残留导致的误判。在搜索前彻底清理工作空间也是有益的。由于未提交的更改可能会干扰检出,建议使用 git stash 等命令来隔离更改 2。
设置 good 和 bad 边界以缩小搜索范围

会话以 git bisect start 开始。将发现失败的当前提交注册为 git bisect bad,将确认正常的点注册为 git bisect good。GitLab 官方文档将此顺序总结为“识别 bad 提交和 good 提交以指定边界值” 3。
git bisect start
git bisect bad # 현재 HEAD = 회귀 재현
git bisect good v1.4.0 # 태그·커밋 해시·상대참조 모두 가능也可以同时指定起始的两个修订版本作为缩写形式。像 git bisect start HEAD v1.4.0 那样,先写 bad 再写 good,可以一次性指定两个边界 2。
还有其他减少搜索空间的手段。可以添加 pathspec,仅针对特定路径有变更的提交。Git 文档示例中展示了方法,例如使用 git bisect start -- arch/i386 include/asm-i386 来仅将特定目录或文件的变更作为候选 2。在虚构的支付逻辑示例中,这可用于排除无关的文档更改或 CI 配置更改。
此外,指定多个 known-good 提交可以进一步缩小搜索空间。如果在 bad 提交之后列出多个正常提交,可以额外缩小范围 2。搜索范围越广,log₂ 计算的收益越大,因此这种预处理在缩小“边界的宽度”方面比缩小“失败点”更有效。
中间提交检查、通过 skip 绕行和可视化确认

确定两个边界后,Git 会自动检选出候选集合中间点的提交。此时,Git 会输出剩余候选数量和预计步骤数。
Bisecting: 63 revisions left to test after this (roughly 6 steps)
此输出允许用户预先估算需要进行判定的次数。官方文档示例中也确认了候选减半的流程,例如 675 revisions left, roughly 10 steps 12。
用户构建检出的提交并确认回归是否复现后,只需输入结果。
# 회귀가 재현되면
git bisect bad
# 정상이면
git bisect good如果回答中间提交为 bad,意味着“罪魁祸首在此之前(或就在此处)”,因此 Git 排除之后的区间。如果回答为 good,意味着“罪魁祸首在此之后”,因此排除之前的区间。此规则以 log₂ 步骤消除候选 2。最终,会显示类似 b9f0e1a2c3... is the first bad commit 的单个哈希以及该提交修改的文件列表 1。
有时也会发生构建本身失败,导致无法判断 good/bad 的提交。此时可以使用 git bisect skip 跳过当前提交。Git 会绕行到附近的另一个提交继续搜索 1。skip 也支持指定范围。可以像 git bisect skip v2.5..v2.6 那样标记整个区间 1。
但是,skip 需要以牺牲精度为代价。根据 tms-outsource 引用的 Git 官方文档手册,如果跳过与罪魁祸首提交相邻的提交,Git 无法准确指出“哪一个先发生” 2。虽然这是避免搜索中断的手段,但精度可能会下降。如果跳过的候选相邻且无法区分,Git 会列出所有候选并停止。此时可以使用 git bisect visualize 在 gitk 中可视化检查剩余的嫌疑候选,并选择一个相邻的正常提交进行手动判定以恢复 1。
自动化:git bisect run 和 exit code 决定结果

与其让人工在每一步输入 good/bad,不如通过脚本的退出码来自动化判定。git bisect run <스크립트> 在每个检出的提交上运行命令,并将 exit code 解释为判定结果。
Christian Couder(在 LWN.net 于 2009 年 2 月 3 日发表的文章)明确指出,如果退出码为 0,则解释为 good;如果为其他值(除 125 和 128 及以上),则解释为 bad。他还提出了一个模式:对于因构建失败而无法判断回归的提交,返回 125 以请求 skip 4。
git bisect start
git bisect bad
git bisect good v1.4.0
git bisect run npm test -- payment.spec.js| exit code | 判定 | 情况 |
|---|---|---|
| 0 | good | 测试通过 |
| 1~124, 126, 127 | bad | 测试失败 |
| 125 | skip | 构建失败等无法验证 |
| 128~255 | abort | 搜索中断(信号、意图性终止等) |
这里必须指出两个需要注意的陷阱。
第一,exit code 127。POSIX shell 在找不到命令时返回 127。如果脚本路径中有拼写错误导致命令本身无法执行,就会出现 127。Git 将其解释为“bad”,将所有提交标记为 bad,搜索以无意义的结果结束 2。这是因为“测试失败”和“脚本不存在导致甚至无法执行”被归为同一类 bad。因此,在自动化之前,必须确认脚本是否处于可执行状态 2。
第二,区分构建失败和回归失败。如果返回 exit code 1 或 2 是由于编译错误而非测试失败,该提交会被记录为 bad。但是,失败的原因可能是构建环境而非回归。Couder 提出了 make || exit 125 模式,以明确在构建失败时进行 skip 4。
#!/usr/bin/env bash
# 저장소 밖에 두는 편이 안전합니다 [S13]
make build || exit 125 # 빌드 실패 = 검증 불가 → skip
if ./run-app --check-payment; then
exit 0 # good
else
exit 1 # bad (회귀 재현)
fi这一行条件语句是将“构建失败”与“回归”分离的核心机制。如果将此脚本放在仓库外部的路径,也可以减少因旧提交中没有测试文件而产生的误判 2。
自动化也有前提条件。在 Couder 的文章中,Andreas Ericsson(Git 开发者)解释说,使用 git bisect 需要所有提交保持小且可 bisect 的状态,并且需要一种在每个提交编写测试以揭示回归的工作流程 4。虽然自动化消除了判定输入的工作量,但要求始终准备好复现脚本的努力。Ericsson 报告称,基于内部跟踪器的报告-修复周期从平均 142.6 小时缩短到 16.2 小时,但这是特定跟踪系统中测量的经验数值,依赖于提交细化和回归测试编写等前提条件 4。
搜索结束后重置和结果解释

搜索结束后,必须运行 git bisect reset。在 bisect 期间,Git 以哈希形式检出任候选提交,因此处于 detached HEAD 状态。如果不 reset,工作将继续停留在中间提交。Nodelog Engineer 指导要“习惯性执行”,Pro Git 也警告说“跳过最终 reset 会使仓库处于异常状态” 12。
如果向 git bisect reset 传递参数,目的地会发生变化。
git bisect reset # 세션 시작 전 HEAD로 복귀
git bisect reset bisect/bad # first bad commit으로 복귀
git bisect reset HEAD # 탐색 중 커밋에 머무름如果不带参数运行,则返回到搜索前的分支 2。
输出的 first bad commit 仅告知“更改引入的提交”,并未解释回归发生的“原因”。最终输出显示提交哈希以及该提交修改的文件列表,阅读 diff 并确定原因的工作必须由人来完成 2。
first bad commit 也可能是合并提交。在这种情况下,正如 Nodelog Engineer 的指导,可以再次对该分支内部进行 bisect 以缩小到具体提交 1。
对于判定过程中的误判,也有恢复途径。在 10,928 个候选(约 14 步)中,第一个判定决定了其余 13 步搜索的一半。误判可能会将整个后期阶段引向错误的方向。此时,可以使用 git bisect log > bisect.log 保存判定历史,删除错误行,然后运行 git bisect reset 和 git bisect replay bisect.log 以继续搜索 12。
结论:判定的质量决定搜索的准确性
git bisect 是一个以 good 和 bad 边界为基准,每一步将候选减半的工具,对于 200 个提交,大约通过 8 次判定即可缩小 first bad commit 1。但是,这一结果仅在每个判定确实反映该提交的回归状态时才有效。
如果将构建失败视为 bad,则留下错误的一半;如果因脚本路径拼写错误导致 exit code 127,Git 会误以为所有提交都是 bad 2。因此,在自动化时,应将构建失败通过 exit code 125 分离并进行 skip 处理,将验证脚本放在仓库外,并在搜索结束后通过 git bisect reset 从 detached HEAD 返回原始分支,这些程序决定了结果的可信度 42。
如果足够靠前地设定 good 边界,并预先准备好包括测试隔离在内的复现条件,Git 指出的提交哈希和更改文件列表作为“回归引入点”的起点,具有足够的准确性 1。
参考资料
- git bisect — 버그가 처음 생긴 커밋 이진 탐색으로 찾기 — Nodelog Engineer · thivelab.com↩
- What Is Git Bisect? Debugging with Binary Search · tms-outsource.com↩
- Common Git commands | GitLab Docs · docs.gitlab.com↩
- ↩




