本节摘要:Git Bisect 用二分查找算法在提交历史里快速定位"引入 bug 的那一个提交"。本节先讲二分原理:好坏两个边界不断折半,N 个提交最多 log₂N 次测试;再讲手动标记与 bisect run 全自动化的操作,最后覆盖跳过不可构建提交、处理合并提交等注意点。核心收益:一百个提交找 bug,十次测试内搞定。
阅读完本节,你应当能够:
线上报告了一个 bug:导出功能突然坏了。你记得上周还好好的,这周某次提交把它弄坏了。你数了数,从"上次确认正常"到"现在坏了"之间有 86 个提交。怎么找罪魁祸首?
穷举法是逐个提交 git checkout 过去测试——86 次,一上午就没了。运气好前几个就中,运气差一整天耗进去。你心里很清楚:肯定有个更聪明的办法。
答案是二分查找。思路是教科书级的:每次把搜索范围砍半。已知旧版本好、新版本坏,取中间的提交测一次——如果中间版本也是坏的,说明 bug 更早引入,范围变成"好 ↔ 中间坏";如果中间版本是好的,说明 bug 在后面,范围变成"中间好 ↔ 坏"。无论哪种结果,搜索范围都缩小一半。86 个提交,⌈log₂86⌉ = 7 次测试就能锁定那个提交——比你从早查到晚快一个数量级。
Git 把这套算法内置成了命令:git bisect。你只需要告诉它"哪个版本好、哪个版本坏",再在它检出的每个提交上回答"这个版本好还是坏",它会自动帮你折半、切版本、收窄范围,最后指认那个万恶的提交。
类比档案馆查卷宗:一份文件从"无错"到"出错",中间有八十多版修改稿。你不用从头逐版比对,而是先翻中间那一版——如果错误已经出现,往前半段找;如果还没出现,往后半段找。翻完七次,错就定位在唯一一版上。bisect 就是那个帮你"翻中间"的馆员。
bisect 有效的前提是好坏关系在时间上单调:坏提交之后的所有后代提交,一定都包含 bug(除非有人修复)。也就是说,历史必须能分成"连续的一段好 + 连续的一段坏",中间有一个分界点——这就是引入 bug 的提交。二分查找就是高效地找这个分界点。
每一轮:
效率数据很直观:1000 个提交最多 10 次测试,10000 个最多 14 次。线性找要测 N 次,二分找只要测 log₂N 次,提交越多,优势越夸张。

第一步:起航
git bisect start
Git 记住你当前所在的分支和提交,方便最后复位。
第二步:给边界
git bisect bad # 当前 HEAD 是坏的(现在就有 bug) git bisect good v1.0 # 已知正常版本(可以写标签名、哈希、分支名)
Git 算出范围,检出中间提交,提示你测试。
第三步:循环标记
# 测试当前检出的提交 git bisect bad # 有 bug git bisect good # 没 bug
每标记一次,Git 报告剩余步数并检出新的中间提交。重复到你测出分界。
第四步:复位
git bisect reset
必须复位。bisect 会不停 checkout 提交,让你停在历史中的某个提交上;找到 bug 后不复位,你会在一个"游离的旧提交"上继续开发,一切都会乱套。
第一次用 bisect 的人常被它的输出吓到——一堆"Bisecting: ..."的中间状态。看懂它就不慌了。假设你标记好与坏后:
Bisecting: 43 revisions left to test after this (roughly 5 steps) [2b4d9f0] 重构支付模块的金额计算
Git 明确告诉你:还剩 43 个提交要排查,大概再测 5 次;并且已经帮你检出了中间提交 2b4d9f0,标题是"重构支付模块的金额计算"——这本身就是线索。你测完这个提交,标记 good 或 bad:
git bisect bad Bisecting: 21 revisions left to test after this (roughly 4 steps) [8c1e3a7] 调整订单状态的枚举定义
范围从 43 砍到 21,Git 又给出下一个中间提交。循环往复,直到输出:
2b4d9f0 is the first bad commit
看到这行,收工,git bisect reset 回位。整个过程的节奏感很强:测一个、标一个、范围减半、再测,Git 像导航一样把你一步步送到目的地。
手动标记在"测试很快、人要在场"时够用。但很多 bug 的判定可以写进脚本(跑一条命令、看退出码),这时用 git bisect run 全自动:
git bisect start git bisect bad git bisect good v1.0 git bisect run ./test_bug.sh
Git 每次检出提交后自动跑 ./test_bug.sh,按退出码判定好坏:
脚本要保证"从任意提交上都能独立判断"——通常需要先构建再跑测试:
#!/bin/sh make && make test # 构建 + 跑测试
跑完 Git 自动输出那个首个坏提交。你泡杯茶的功夫,定位就完成了。
| 退出码 | 语义 | 典型写法 |
|---|---|---|
| 0 | 好(无 bug) | 测试全通过 |
| 1~127(非125) | 坏(有 bug) | 测试失败、断言不通过 |
| 125 | 跳过 | 构建失败、环境不满足 |
编写要点:脚本里"能构建才跑测试,构建失败返回 125"——否则一个历史版本构建不通过会污染二分结果。测试命令越短、越确定性,自动化越可靠。
手动 bisect 时,如果检出的提交因故测不了(构建失败的中间状态、依赖缺失),用 git bisect skip 让它跳过,Git 会另选一个提交继续。skip 的意义在于:别让一个"测不了"的提交卡死整个二分——算法绕开它,继续收窄范围。
二分定位到合并提交是新手最容易懵的地方。它输出"某个 merge 提交是第一个坏提交",但你看它的 diff——可能跟 bug 毫无关系。原因:merge 提交本身不引入新代码,它的坏是"把一条坏分支合并进来的结果"。处理两步:
git show 合并提交 看它合并了哪两条分支(父提交)。多数 bisect 教程不提这个细节,但它决定了"定位到 merge 提交"时你能不能继续往下查。核心认知:merge 提交只是"分岔的汇合点",bug 的真正源头在被并入的那条分支上。
大仓库常有些历史提交在当时的构建环境下能过、现在复现不了(依赖版本、工具链变化)。这类提交会让 bisect 卡住:检出一个测不了的提交,没法标记好坏。解法是 skip——手动模式用 git bisect skip,脚本模式让脚本在"无法测试"时返回 125。Git 遇到 skip 会另选提交继续二分,虽然可能多测几轮,但比卡死强得多。
还有个效率技巧:别让二分范围从全历史开始。如果明显是"这周才坏的",把 good 边界直接标到上周的提交或标签上,能省好几轮测试。先想清楚范围,再让算法干活。
bisect 不是孤立工具,和前面学的配合威力更大:
git log 和直觉缩小:明确"上个月正常",就 bisect good 指向上月末的标签,省得多测。git show 提交 看这次改动到底改了哪几行——通常 bug 就在那里。⚠️ 常见坑:bisect 过程中直接在一个旧提交上"顺手修了个 bug"再提交——这会把提交链搞乱,二分也会失去意义。bisect 的纪律是:只测试、只标记、不修改。定位完 reset 回来,再在正确的分支上动手修复。
💡 关键直觉:把 bisect 想成"审讯室的排查表"。你不需要看完全部嫌疑人,每次问话(测试)都让一半人出局,七轮就能锁死一个。测试脚本就是那个"一问就知道是不是"的测谎仪。
"bisect 会不会改坏我的仓库?" 不会。bisect 只是不断 checkout 历史提交,配合 reset 复位。但它要求工作区干净(有未提交修改会阻止 checkout),所以开始前先 commit 或 stash。
"找不到 bug 引入点,是不是二分错了?" 可能原因:好/坏边界标记反了、bug 非确定性、或者 bug 是多个提交共同引入的(单调性假设不成立)。先检查边界标记,再想办法稳定复现。
"bisect 能找性能回归吗?" 能。脚本里跑一段基准测试,执行时间超阈值返回 1(坏),低于返回 0(好),bisect 就能定位性能骤降的提交。这是 bisect 仅次于 bug 定位的第二大用途。
"bisect 只能用在当前分支吗?" 建议在当前分支上做,因为历史是连续的。跨分支的历史在 merge 之后不再线性,bisect 的分界假设会失真。如果 bug 出现在某条 feature 分支上,就切到那条分支再 start。
"有没有比手敲快一点的启动方式?" 有。如果已知坏提交就是当前 HEAD、好提交是最近一个标签,可以合并成一条启动指令:git bisect start HEAD 上次好标签——start 接受两个参数分别作为 bad 和 good 的初始值,省两次输入。
"bisect run 跑到一半想停怎么办?" Ctrl-C 打断,然后 git bisect reset 复位。run 模式中途被打断不会破坏仓库,复位后可以重新开始。
"有图形界面看二分进度吗?" 有。git bisect visualize 会打开图形工具展示当前二分状态,git bisect log 能导出当前会话记录,git bisect replay 可以重放一次会话——适合把一次完整的排错过程存档。
下一节处理"项目要拆成多个仓库"的复杂结构——Git Submodule 子模块,把独立仓库嵌进超项目。