前言
项目跑了一段时间以后,最麻烦的 Bug 往往不是一眼能看出来的语法错误,而是那种"之前明明是好的,现在突然坏了"的回归问题。
比如某个接口在上个月还能正常返回数据,最近发版后开始报错;某个页面之前可以打开,现在出现白屏;某个测试以前一直通过,某次合并之后开始失败。面对几十次、几百次提交,靠肉眼翻提交记录基本靠运气。提交信息写得好的项目还能猜一猜,提交信息混乱的项目就很难排。
Git Bisect 就适合处理这种问题。它不要求你知道是哪一段代码出了问题,只需要知道两个点:一个版本是好的,一个版本是坏的。Git 会在这两个版本之间不断取中间提交,让你判断当前状态是否有问题。每判断一次,搜索范围就缩小一半,最后定位到第一个引入问题的提交。
这个工具平时用得不多,但一旦遇到回归 Bug,会非常省时间。

一、什么时候适合用 Git Bisect
Git Bisect 最适合定位回归问题。
所谓回归问题,就是功能曾经正常,后来某个时间点开始坏掉。它有一个非常明确的特征:你能找到一个好版本,也能找到一个坏版本。
适合使用 Git Bisect 的场景包括:
text
某个测试以前通过,现在失败
某个接口以前正常,现在返回异常
某个页面以前能打开,现在白屏
某个性能指标以前正常,现在明显变慢
某个命令以前能执行,现在报错
不太适合的场景也要先排除。
如果这个功能从来没有正常过,Git Bisect 很难帮你定位"从哪里开始坏"。如果 Bug 依赖外部环境,比如第三方接口、线上配置、数据库数据、时间、网络状态,那么每次切换提交后的测试结果可能不稳定,二分结果也会变得不可靠。
我一般会先问自己三个问题:
text
能不能找到一个确定正常的提交或版本标签?
能不能找到一个确定有问题的提交?
当前问题能不能用一个稳定的测试步骤复现?
这三个问题都能回答,Git Bisect 就很适合上场。
二、手动定位一次问题提交
手动使用 Git Bisect 的流程很简单。
假设当前 HEAD 已经有问题,而 v1.8.0 这个版本还正常,可以这样开始:
bash
git bisect start
git bisect bad
git bisect good v1.8.0
这里的意思是:
text
当前提交是坏的
v1.8.0 是好的
请在这两个点之间开始二分查找
执行后,Git 会自动切到中间某个提交。你需要在这个提交上验证问题是否存在。
比如运行测试:
bash
npm test
或者手动启动项目验证:
bash
npm run dev
如果当前提交仍然有问题,标记为 bad:
bash
git bisect bad
如果当前提交没有问题,标记为 good:
bash
git bisect good
Git 会继续切到下一个中间提交。这个过程重复几次以后,会输出类似结果:
bash
a1b2c3d4e5f6 is the first bad commit
这就是第一个引入问题的提交。
看完结果后,不要忘记退出二分状态:
bash
git bisect reset
git bisect reset 会把工作区切回开始二分之前的位置。很多人第一次用的时候会忘记这一步,然后发现自己还停在某个历史提交上,以为分支出问题了。
定位到提交后,可以直接看变更:
bash
git show a1b2c3d4e5f6
如果想看某个文件的具体修改历史,再配合:
bash
git blame path/to/file
Git Bisect 负责找到可疑提交,真正修复问题还要回到代码变更本身。
三、能自动测试,就不要手动点
手动二分适合 UI 问题、交互问题、肉眼可判断的问题。只要问题能用测试脚本判断,最好交给 git bisect run。
它的基本思路是让 Git 每切换到一个提交,就自动执行一条命令。命令退出码决定当前提交是好是坏。
约定很简单:
text
退出码 0 表示 good
退出码 1 到 124 表示 bad
退出码 125 表示 skip
其他异常退出可能中断 bisect
比如 JavaScript 项目可以直接跑测试:
bash
git bisect start
git bisect bad HEAD
git bisect good v1.8.0
git bisect run npm test
Python 项目可以这样:
bash
git bisect start
git bisect bad HEAD
git bisect good v1.8.0
git bisect run pytest
如果现成测试能稳定复现问题,这种方式非常舒服。你设置好好坏边界,然后等结果就行。
真实项目里,测试步骤往往没这么简单。可能需要安装依赖、构建、运行某个特定测试文件,甚至要先清理缓存。可以写一个脚本:
bash
#!/usr/bin/env bash
set -e
npm ci
if ! npm run build; then
exit 125
fi
npm test -- tests/order-status.test.ts
假设文件叫 bisect-test.sh,放在仓库外部或一个整个历史范围内都存在的位置,然后执行:
bash
chmod +x ../bisect-test.sh
git bisect start
git bisect bad HEAD
git bisect good v1.8.0
git bisect run ../bisect-test.sh
这里有一个很容易踩的坑:测试脚本如果是在后来的提交里才新增的,二分切到旧提交时可能找不到脚本。所以脚本最好放在仓库外部,比如上级目录,或者确保它在整个二分区间内都存在。
如果某个历史提交编译不过,不要直接把它当成 bad。编译不过不一定是引入目标 Bug 的提交,可能只是历史上的临时状态。这个时候返回 125,让 Git 跳过它更稳。
四、复杂场景下怎么缩小范围
Git Bisect 默认会在两个提交之间查找所有相关提交。如果你已经知道问题只可能出现在某个目录,可以直接限制路径。
比如问题只出现在前端页面:
bash
git bisect start HEAD v1.8.0 -- apps/web
只查某个模块:
bash
git bisect start HEAD v1.8.0 -- src/order
只查某个文件:
bash
git bisect start HEAD v1.8.0 -- src/order/status.ts
这种写法能减少很多无关提交。尤其是 Monorepo 里,一个仓库里有前端、后端、文档、脚本、基础设施配置,如果不限制路径,Git 可能让你验证很多和问题无关的提交。
如果遇到无法测试的提交,可以跳过:
bash
git bisect skip
也可以跳过一段范围:
bash
git bisect skip v1.9.0..v1.9.3
不过跳过要谨慎。被跳过的提交越多,定位结果越可能变得不精确。尤其是坏提交就在跳过范围附近时,Git 可能只能告诉你"问题在这些提交之中",无法锁定唯一提交。
还有一种场景是找"修复问题的提交"。
比如某个 Bug 在旧版本存在,后来某个提交修好了它。用 good 和 bad 也能做,但语义上容易别扭。可以改用自定义术语:
bash
git bisect start --term-old=broken --term-new=fixed
git bisect broken v1.8.0
git bisect fixed HEAD
后续就用:
bash
git bisect broken
git bisect fixed
这样更符合直觉。旧提交是 broken,新提交是 fixed,最后找到第一个修复问题的提交。
五、定位以后还要做一次人工判断
Git Bisect 找到的是第一个让测试结果变坏的提交,但这个提交不一定就是最终的业务根因。
有时候它只是暴露了更早隐藏的问题。
比如某个提交升级了依赖,导致旧代码里的类型问题暴露出来。Bisect 会定位到依赖升级提交,但真正需要修的可能是旧代码里不兼容的写法。
有时候它定位到的是一次重构,但问题其实来自某个边界条件遗漏。你还需要看提交 diff,确认到底是哪一行改变了行为。
我一般会按这个顺序继续排查:
bash
git show <bad-commit>
先看这个提交改了什么。
bash
git show --stat <bad-commit>
看涉及哪些文件,判断影响范围。
bash
git diff <bad-commit>^ <bad-commit>
只看这个提交相对父提交的变化。
如果某个文件可疑,再看历史:
bash
git blame path/to/file
这里不要急着马上回滚。回滚可能会带走其他正常改动,尤其是一个提交里混了多个变更时。更稳的做法是先把问题最小化,写一个能复现 Bug 的测试,再修复代码。
理想流程是:
text
用 Git Bisect 找到坏提交
阅读 diff 判断可疑改动
补一个失败测试复现问题
修改代码让测试通过
再确认相关功能没有回归
Git Bisect 解决的是定位效率,修复质量还要靠测试和审查。
六、养成能被 Bisect 的提交习惯
Git Bisect 好不好用,很大程度取决于平时的提交质量。
如果每个提交都很大,一次提交里既改数据库结构,又改接口,又改前端页面,还顺手格式化了几十个文件,那么 Bisect 就算定位到这个提交,后续分析仍然很痛苦。
更适合 Bisect 的提交应该尽量原子化:
text
一个提交解决一个明确问题
不要把格式化和业务修改混在一起
不要把依赖升级和功能改造混在一起
提交后尽量保持项目能构建、测试能跑
提交信息写清楚为什么改,而不只写改了什么
如果团队习惯 squash merge,也不影响使用 Bisect,但 squash 后的提交会更大,定位粒度会变粗。对于回归问题较多的项目,可以在合并前要求 PR 里的关键提交保持相对清晰,或者至少保证 squash commit 的描述足够具体。
还有一点很重要:不要在有未提交修改的工作区里直接开始 Bisect。二分过程会频繁 checkout 历史提交,未提交修改很容易造成冲突或丢失判断。
开始前先看状态:
bash
git status
如果有临时改动,可以先 stash:
bash
git stash push -m "before bisect"
二分结束后再恢复:
bash
git bisect reset
git stash pop
这样流程更干净。
总结
Git Bisect 适合定位回归 Bug。只要能找到一个好版本和一个坏版本,再准备一个稳定的验证方式,它就能用二分查找把问题范围快速缩小到某个提交。
手动模式适合需要人工判断的场景,比如 UI 异常、交互问题、肉眼可确认的行为变化。自动模式适合测试能复现的问题,git bisect run 配合测试脚本可以省掉大量重复操作。
复杂项目里,可以通过路径限制减少无关提交,通过 skip 跳过无法测试的历史提交,通过自定义术语查找修复提交。定位结果出来以后,还要继续看 diff、补测试、分析真实根因,不要只看到第一个 bad commit 就急着回滚。
我更建议把 Git Bisect 当成日常调试工具,而不是最后没办法时才想起来的救命工具。只要项目有清晰提交、稳定测试和相对干净的历史,遇到"之前好好的,现在坏了"的问题,Git Bisect 往往比翻日志、猜提交、问同事都更快。