Skip to content

git-bisect

使用二分搜索查找引入 bug 的提交

概要

bash
git bisect start [--term-(bad|new)=<term-new> --term-(good|old)=<term-old>]
		   [--no-checkout] [--first-parent] [<bad> [<good>...]] [--] [<pathspec>...]
git bisect (bad|new|<term-new>) [<rev>]
git bisect (good|old|<term-old>) [<rev>...]
git bisect terms [--term-(good|old) | --term-(bad|new)]
git bisect skip [(<rev>|<range>)...]
git bisect next
git bisect reset [<commit>]
git bisect (visualize|view)
git bisect replay <logfile>
git bisect log
git bisect run <cmd> [<arg>...]
git bisect help

描述

此命令使用二分搜索算法查找项目历史中哪个提交引入了 bug。使用时,首先告诉它一个已知包含 bug 的"坏"提交和一个已知在引入 bug 之前的"好"提交。然后 git bisect 在这两个端点之间选择一个提交并询问你选定的提交是"好"还是"坏"。它继续缩小范围,直到找到引入更改的确切提交。

实际上,git bisect 可用于查找更改项目任何属性的提交;例如,修复 bug 的提交或导致基准性能提高的提交。为了支持这种更一般的用法,可以使用术语"旧"和"新"代替"好"和"坏",或者你可以选择自己的术语。有关更多信息,请参阅下面的"替代术语"部分。

基本二分命令:start、bad、good

例如,假设你正在尝试查找在项目版本 v2.6.13-rc2 中已知正常工作的功能被破坏的提交。你可以如下开始二分会话:

bash
$ git bisect start
$ git bisect bad                 # 当前版本是坏的
$ git bisect good v2.6.13-rc2    # v2.6.13-rc2 已知是好的

一旦你指定了至少一个坏提交和一个好提交,git bisect 选择该历史范围中间的一个提交,检出它,并输出类似以下内容:

Bisecting: 675 revisions left to test after this (roughly 10 steps)

你现在应该编译检出的版本并测试它。如果该版本正常工作,输入

bash
$ git bisect good

如果该版本被破坏,输入

bash
$ git bisect bad

然后 git bisect 将响应类似

Bisecting: 337 revisions left to test after this (roughly 9 steps)

重复此过程:编译树,测试它,然后根据它是好还是坏运行 git bisect goodgit bisect bad 来请求下一个需要测试的提交。

最终将不再有要检查的修订版本,命令将打印出第一个坏提交的描述。引用 refs/bisect/bad 将指向该提交。

二分重置

在二分会话之后,要清理二分状态并返回原始 HEAD,请发出以下命令:

bash
$ git bisect reset

默认情况下,这会将你的树返回到 git bisect start 之前检出的提交。(新的 git bisect start 也会这样做,因为它会清理旧的二分状态。)

使用可选参数,你可以返回到不同的提交:

bash
$ git bisect reset <commit>

例如,git bisect reset bisect/bad 将检出第一个坏修订版本,而 git bisect reset HEAD 将让你留在当前二分提交上并避免切换提交。

替代术语

有时你不是在寻找引入破坏的提交,而是寻找导致某些其他"旧"状态和"新"状态之间变化的提交。例如,你可能在寻找引入特定修复的提交。或者你可能在寻找源代码文件名最终全部转换为公司命名标准的第一个提交。或者任何其他情况。

在这种情况下,使用术语"好"和"坏"来指代"更改之前的状态"和"更改之后的状态"可能会非常令人困惑。因此,你可以使用术语"旧"和"新"分别代替"好"和"坏"。(但请注意,你不能在单个会话中混合使用"好"和"坏"与"旧"和"新"。)

在这种更一般的用法中,你向 git bisect 提供一个具有某个属性的"新"提交和一个不具有该属性的"旧"提交。每次 git bisect 检出一个提交时,你测试该提交是否具有该属性。如果有,将提交标记为"新";否则,将其标记为"旧"。当二分完成时,git bisect 将报告哪个提交引入了该属性。

要使用"旧"和"新"而不是"好"和"坏",你必须不带参数运行 git bisect start,然后运行以下命令添加提交:

bash
git bisect old [<rev>]

指示提交在寻求的更改之前,或

bash
git bisect new [<rev>...]

指示它在之后。

要获取当前使用术语的提醒,请使用

bash
git bisect terms

你可以使用 git bisect terms --term-oldgit bisect terms --term-good 仅获取旧术语;git bisect terms --term-newgit bisect terms --term-bad 可用于了解如何称呼比寻求更改更新的提交。

如果你想使用自己的术语而不是"坏"/"好"或"新"/"旧",你可以选择任何你喜欢的名称(现有的二分子命令如 resetstart 等除外),通过使用以下方式开始二分

bash
git bisect start --term-old <term-old> --term-new <term-new>

例如,如果你正在寻找引入性能回归的提交,你可以使用

bash
git bisect start --term-old fast --term-new slow

或者如果你正在寻找修复 bug 的提交,你可以使用

bash
git bisect start --term-new fixed --term-old broken

然后,使用 git bisect <term-old>git bisect <term-new> 代替 git bisect goodgit bisect bad 来标记提交。

二分可视化/查看

要在 'gitk' 中查看当前剩余的嫌疑对象,请在二分过程中发出以下命令(子命令 view 可用作 visualize 的替代):

bash
$ git bisect visualize

Git 通过各种环境变量检测图形环境:DISPLAY(在 Unix 系统的 X Window System 环境中设置)、SESSIONNAME(在 Cygwin 的交互式桌面会话中设置)、MSYSTEM(在 Msys2 和 Git for Windows 中设置)、SECURITYSESSIONID(可能在 macOS 的交互式桌面会话中设置)。

如果这些环境变量都没有设置,则使用 'git log' 代替。你还可以给出命令行选项,如 -p--stat

bash
$ git bisect visualize --stat

二分日志和二分重放

在将修订版本标记为好或坏后,发出以下命令显示到目前为止已完成的操作:

bash
$ git bisect log

如果你发现指定修订版本状态时出错,可以将此命令的输出保存到文件中,编辑它以删除不正确的条目,然后发出以下命令返回到更正的状态:

bash
$ git bisect reset
$ git bisect replay that-file

避免测试提交

如果在二分会话中间,你知道建议的修订版本不适合测试(例如它无法构建,并且你知道该失败与你正在追踪的 bug 无关),你可以手动选择附近的提交并改为测试该提交。

例如:

bash
$ git bisect good/bad			# 上一轮是好还是坏。
Bisecting: 337 revisions left to test after this (roughly 9 steps)
$ git bisect visualize			# 哦,那没意思。
$ git reset --hard HEAD~3		# 尝试建议之前的 3 个修订版本

然后编译并测试选定的修订版本,之后按通常方式将修订版本标记为好或坏。

二分跳过

你可以要求 Git 为你执行此操作,而不是自己选择附近的提交:

bash
$ git bisect skip                 # 当前版本无法测试

但是,如果你跳过与你要查找的提交相邻的提交,Git 将无法确切告诉这些提交中哪个是第一个坏提交。

你还可以使用范围表示法跳过一系列提交,而不仅仅是一个提交。例如:

bash
$ git bisect skip v2.5..v2.6

这告诉二分过程不应测试 v2.5 之后直到并包括 v2.6 的任何提交。

请注意,如果你还想跳过范围的第一个提交,你需要发出命令:

bash
$ git bisect skip v2.5 v2.5..v2.6

这告诉二分过程应跳过 v2.5v2.6 之间的提交(包括两端)。

二分下一步

通常,在将修订版本标记为好或坏后,Git 会自动计算并检出下一个要测试的修订版本。但是,如果你需要显式请求下一个二分步骤,你可以使用:

bash
$ git bisect next

你可能在通过检出不同修订版本中断二分过程后使用此命令恢复。

通过向 bisect start 提供更多参数来减少二分

如果你知道问题涉及树的哪个部分,可以通过在发出 bisect start 命令时指定 pathspec 参数来进一步减少试验次数:

bash
$ git bisect start -- arch/i386 include/asm-i386

如果你事先知道多个好提交,可以通过在发出 bisect start 命令时在坏提交之后立即指定所有好提交来缩小二分空间:

bash
$ git bisect start v2.6.20-rc6 v2.6.20-rc4 v2.6.20-rc1 --
                   # v2.6.20-rc6 是坏的
                   # v2.6.20-rc4 和 v2.6.20-rc1 是好的

二分运行

如果你有一个脚本可以判断当前源代码是好还是坏,你可以通过发出以下命令进行二分:

bash
$ git bisect run my_script arguments

请注意,脚本(上例中的 my_script)如果当前源代码是好/旧应以代码 0 退出,如果当前源代码是坏/新应以 1 到 127(包括)之间(125 除外)的代码退出。

任何其他退出代码将中止二分过程。应注意,通过 exit(-1) 终止的程序会留下 $? = 255(参见 exit(3) 手册页),因为该值被 & 0377 截断。

特殊退出代码 125 应在当前源代码无法测试时使用。如果脚本以该代码退出,当前修订版本将被跳过(请参阅上面的 git bisect skip)。选择 125 作为此目的的最高合理值,因为 126 和 127 被 POSIX shell 用于表示特定错误状态(127 表示未找到命令,126 表示找到命令但不可执行——这些细节无关紧要,因为就 bisect run 而言,它们是脚本中的正常错误)。

你可能会发现在二分会话期间,你希望对正在测试的修订版本应用临时修改(例如在头文件中 s/#define DEBUG 0/#define DEBUG 1/,或"没有此提交的修订版本需要应用此补丁来解决此二分不感兴趣的另一个问题")。

为了应对这种情况,在内部 'git bisect' 找到下一个要测试的修订版本后,脚本可以在编译之前应用补丁,运行真正的测试,然后决定修订版本(可能带有需要的补丁)是否通过了测试,然后将树倒回到原始状态。最后,脚本应以真正测试的状态退出,让 git bisect run 命令循环确定二分会话的最终结果。

选项

--no-checkout

在二分过程的每次迭代中不检出新的工作树。而是仅更新名为 BISECT_HEAD 的引用以使其指向应测试的提交。 当你在每一步执行的测试不需要检出树时,此选项可能很有用。 如果存储库是裸存储库,则假定 --no-checkout

--first-parent

在看到合并提交时仅跟随第一个父提交。 在检测通过合并分支引入的回归时,合并提交将被识别为 bug 的引入,其祖先将被忽略。 当合并的分支包含损坏或无法构建的提交但合并本身正常时,此选项在避免误报方面特别有用。

示例

  • 在 v1.2 和 HEAD 之间自动二分损坏的构建:
bash
$ git bisect start HEAD v1.2 --      # HEAD 是坏的,v1.2 是好的
$ git bisect run make                # "make" 构建应用程序
$ git bisect reset                   # 退出二分会话
  • 在 origin 和 HEAD 之间自动二分测试失败:
bash
$ git bisect start HEAD origin --    # HEAD 是坏的,origin 是好的
$ git bisect run make test           # "make test" 构建并测试
$ git bisect reset                   # 退出二分会话
  • 自动二分损坏的测试用例:
bash
$ cat ~/test.sh
#!/bin/sh
make || exit 125                     # 跳过损坏的构建
~/check_test_case.sh                 # 测试用例是否通过?
$ git bisect start HEAD HEAD~10 --   # 罪魁祸首在最后 10 个中
$ git bisect run ~/test.sh
$ git bisect reset                   # 退出二分会话

这里我们使用自定义脚本 test.sh。在此脚本中,如果 make 失败,我们跳过当前提交。如果测试用例通过,check_test_case.shexit 0,否则 exit 1。 如果 test.shcheck_test_case.sh 都在存储库外部会更安全,以防止二分、make 和测试过程与脚本之间的交互。

  • 使用临时修改(热修复)自动二分:
bash
$ cat ~/test.sh
#!/bin/sh

# 通过合并热修复分支调整工作树
# 然后尝试构建
if	git merge --no-commit --no-ff hot-fix &&
	make
then
	# 运行项目特定测试并报告其状态
	~/check_test_case.sh
	status=$?
else
	# 告诉调用者这无法测试
	status=125
fi

# 撤消调整以允许干净地切换到下一个提交
git reset --hard

# 返回控制
exit $status

这在每次测试运行之前应用热修复分支的修改,例如在你的构建或测试环境发生更改使得旧修订版本可能需要修复而新版本已经有了的情况下。(确保热修复分支基于你正在二分的所有修订版本中都包含的提交,这样合并不会拉入太多内容,或者使用 git cherry-pick 代替 git merge。)

  • 自动二分损坏的测试用例:
bash
$ git bisect start HEAD HEAD~10 --   # 罪魁祸首在最后 10 个中
$ git bisect run sh -c "make || exit 125; ~/check_test_case.sh"
$ git bisect reset                   # 退出二分会话

这表明如果你在一行上编写测试,可以不需要运行脚本。

  • 在损坏的存储库中定位对象图的良好区域
bash
$ git bisect start HEAD <known-good-commit> [ <boundary-commit> ... ] --no-checkout
$ git bisect run sh -c '
	GOOD=$(git for-each-ref "--format=%(objectname)" refs/bisect/good-*) &&
	git rev-list --objects BISECT_HEAD --not $GOOD >tmp.$$ &&
	git pack-objects --stdout >/dev/null <tmp.$$
	rc=$?
	rm -f tmp.$$
	test $rc = 0'

$ git bisect reset                   # 退出二分会话

在这种情况下,当 'git bisect run' 完成时,bisect/bad 将引用一个至少有一个父提交的提交,其可访问图可以按 'git pack objects' 要求的方式完全遍历。

  • 在代码中查找修复而不是回归
bash
$ git bisect start
$ git bisect new HEAD    # 当前提交被标记为 new
$ git bisect old HEAD~10 # 从现在起第十个提交被标记为 old

或者:

bash
$ git bisect start --term-old broken --term-new fixed
$ git bisect fixed
$ git bisect broken HEAD~10

获取帮助

使用 git bisect 获取简短用法描述,使用 git bisect helpgit bisect -h 获取详细用法描述。

另请参阅

使用 git bisect 对抗回归git-blame(1)

Git

git(1) 套件的一部分

基于 CC BY-NC-SA 3.0 许可发布