5.6 Git Bisect 二分查找


5.6 Git Bisect 二分查找

本节摘要:Git Bisect 用二分查找算法在提交历史里快速定位"引入 bug 的那一个提交"。本节先讲二分原理:好坏两个边界不断折半,N 个提交最多 log₂N 次测试;再讲手动标记与 bisect run 全自动化的操作,最后覆盖跳过不可构建提交、处理合并提交等注意点。核心收益:一百个提交找 bug,十次测试内搞定。

本节导航

阅读完本节,你应当能够:

  1. 说清 bisect 二分查找的原理与效率:N 个提交最多 log₂N 次测试。
  2. 完整跑一遍手动流程:start、bad、good、循环标记、reset。
  3. 写一个测试脚本配合 bisect run,全自动化定位 bug 提交。
  4. 用 125 退出码跳过不可构建的提交,理解 skip 的用法。
  5. 识别 bisect 的边界:非确定性 bug、合并提交、依赖环境的问题。

一、问题与直觉

线上报告了一个 bug:导出功能突然坏了。你记得上周还好好的,这周某次提交把它弄坏了。你数了数,从"上次确认正常"到"现在坏了"之间有 86 个提交。怎么找罪魁祸首?

穷举法是逐个提交 git checkout 过去测试——86 次,一上午就没了。运气好前几个就中,运气差一整天耗进去。你心里很清楚:肯定有个更聪明的办法

答案是二分查找。思路是教科书级的:每次把搜索范围砍半。已知旧版本好、新版本坏,取中间的提交测一次——如果中间版本也是坏的,说明 bug 更早引入,范围变成"好 ↔ 中间坏";如果中间版本是好的,说明 bug 在后面,范围变成"中间好 ↔ 坏"。无论哪种结果,搜索范围都缩小一半。86 个提交,⌈log₂86⌉ = 7 次测试就能锁定那个提交——比你从早查到晚快一个数量级

Git 把这套算法内置成了命令:git bisect。你只需要告诉它"哪个版本好、哪个版本坏",再在它检出的每个提交上回答"这个版本好还是坏",它会自动帮你折半、切版本、收窄范围,最后指认那个万恶的提交。

类比档案馆查卷宗:一份文件从"无错"到"出错",中间有八十多版修改稿。你不用从头逐版比对,而是先翻中间那一版——如果错误已经出现,往前半段找;如果还没出现,往后半段找。翻完七次,错就定位在唯一一版上。bisect 就是那个帮你"翻中间"的馆员。

二、核心原理

二分查找的正确性

bisect 有效的前提是好坏关系在时间上单调:坏提交之后的所有后代提交,一定都包含 bug(除非有人修复)。也就是说,历史必须能分成"连续的一段好 + 连续的一段坏",中间有一个分界点——这就是引入 bug 的提交。二分查找就是高效地找这个分界点。

每一轮:

  1. Git 在"当前已知最好"和"当前已知最坏"之间选一个中间的提交检出。
  2. 你测试这个提交:有 bug 标记 bad,没有标记 good。
  3. 范围被砍半,重复,直到范围只剩一个提交——它就是第一个坏提交。

效率数据很直观:1000 个提交最多 10 次测试,10000 个最多 14 次。线性找要测 N 次,二分找只要测 log₂N 次,提交越多,优势越夸张。

图 5-6 二分查找如何收窄范围

图 5-6 二分查找如何收窄范围

手动四步

第一步:起航

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 像导航一样把你一步步送到目的地。

自动化:bisect run

手动标记在"测试很快、人要在场"时够用。但很多 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,按退出码判定好坏:

  • 退出码 0:好提交(bug 不存在),范围收向后半段。
  • 退出码 1~127(除 125):坏提交,范围收向前半段。
  • 退出码 125:跳过当前提交(无法构建、测试无关)。
  • 其他退出码:中止整个 bisect。

脚本要保证"从任意提交上都能独立判断"——通常需要先构建再跑测试:

#!/bin/sh make && make test # 构建 + 跑测试

跑完 Git 自动输出那个首个坏提交。你泡杯茶的功夫,定位就完成了。

三、工程实践要点

测试脚本的三种退出码形态

退出码 语义 典型写法
0 好(无 bug) 测试全通过
1~127(非125) 坏(有 bug) 测试失败、断言不通过
125 跳过 构建失败、环境不满足

编写要点:脚本里"能构建才跑测试,构建失败返回 125"——否则一个历史版本构建不通过会污染二分结果。测试命令越短、越确定性,自动化越可靠。

skip:处理"测不了"的提交

手动 bisect 时,如果检出的提交因故测不了(构建失败的中间状态、依赖缺失),用 git bisect skip 让它跳过,Git 会另选一个提交继续。skip 的意义在于:别让一个"测不了"的提交卡死整个二分——算法绕开它,继续收窄范围。

边界:什么时候 bisect 不靠谱

  • 非确定性 bug:bug 时有时无,同一提交测两次结果不同,二分会被带偏。先想办法稳定复现,再用 bisect。
  • 依赖外部环境:bug 只在特定配置/数据/服务下出现,历史提交上难复现。先把必要条件固定下来。
  • 大型合并提交:二分可能定位到合并提交本身——bug 其实来自被合并的分支。检查合并提交的两个父提交,进一步缩小到"哪条分支引入的"。

与 merge 提交打交道

二分定位到合并提交是新手最容易懵的地方。它输出"某个 merge 提交是第一个坏提交",但你看它的 diff——可能跟 bug 毫无关系。原因:merge 提交本身不引入新代码,它的坏是"把一条坏分支合并进来的结果"。处理两步:

  1. git show 合并提交 看它合并了哪两条分支(父提交)。
  2. 对两个父提交分别测试:如果其中一个父提交就是坏的,说明 bug 来自那条分支,切过去对那条分支继续 bisect。

多数 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 想成"审讯室的排查表"。你不需要看完全部嫌疑人,每次问话(测试)都让一半人出局,七轮就能锁死一个。测试脚本就是那个"一问就知道是不是"的测谎仪。

常见问题与 FAQ

"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 可以重放一次会话——适合把一次完整的排错过程存档。

温故知新

  • 二分原理:好坏单调分界 + 每次砍半,N 个提交最多 log₂N 次测试。
  • 手动四步:start、bad、good、循环标记、最后一定 reset。
  • 自动化 run:脚本退出码 0 好、1~127 坏、125 跳过,全自动定位。
  • skip 的用法:测不了的提交跳过,别卡死二分进程。
  • 边界条件:非确定性 bug、环境依赖、合并提交是 bisect 的三个盲区。
  • 纪律:bisect 中只测试只标记不修改,定位后复位再动手修。
  • 排错配合:先 log 缩范围、show 看嫌疑提交、前后各测一两个确认分界。
  • 复位铁律:git bisect reset 必须执行,否则会停在游离旧提交上。

下一节处理"项目要拆成多个仓库"的复杂结构——Git Submodule 子模块,把独立仓库嵌进超项目。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U