Git Bisect:利用算法与自动化追踪代码库问题指南

Git Bisect 入门指南

注意:这篇文章发布已超过两年,因此其中包含的信息可能已过时。如果您发现了问题,请留下评论,我公司会尽力更正。

2024 年 3 月 31 日 - 阅读时长 27 分钟

Git bisect 是一个 Git 命令,借助它能更轻松地找出代码库中引入问题的位置。在 Drupal 开发、Drupal 模块开发以及 Drupal 升级等工作场景中,如果代码出现问题,Git bisect 就可以发挥重要作用,帮助开发者快速定位问题所在,尤其是对于 Drupal11 这样较新的版本。

在大型项目里,可能会在代码添加改动后引发问题,这时就需要找出问题的源头。明确问题是在哪个地方引入的,能让调试工作变得轻松许多。

你可以逐个检出提交,直至找出问题所在的提交,但 Git 自带的 bisect 工具能辅助完成这个过程,甚至实现自动化,从而快速定位问题。

在开始使用 Git bisect 之前,先来看看二分查找算法,这也是 Git 用于找出相关提交的方法。

一、二分查找算法

二分查找算法是一种在数组中查找特定元素的方法。相较于按顺序遍历列表查找元素,这种方法的效率要高得多。

以一个包含 10 个元素、编号从 1 到 10 的列表为例。

1 2 3 4 5 6 7 8 9 10

如果使用线性查找来找到数字 8,就需要逐个检查数字,直到找到目标。这意味着至少要进行 8 次比较才能找到正确数字。

而使用二分查找,会先选择列表的中间点,然后判断目标数字是比中间点的值大还是小。

1 2 3 4 5 6 7 8 9 10
        ^

已知所选的中间元素(即 5)比目标元素(即 8)小,所以接着选择 5 和最大元素之间的一个点,这样就能找到数字 8。这意味着只需要两步就找到了目标元素。不过,如果选择了 7,可能就需要三步(这取决于如何对数值进行四舍五入)。

1 2 3 4 5 6 7 8 9 10
              ^

这就是 Git bisect 的工作原理。只不过,这里不是比较大小,而是将特定提交标记为“好”或“坏”。

接下来,看看如何运行 Git bisect 以及二分查找算法是如何应用的。

二、运行 Git Bisect

首先要做的是运行 “git bisect start” 命令;这会告知 Git 要开始一次二分查找,并将仓库置于二分查找模式。

git bisect start

此时,Git 等待我们将某个提交标记为“好”或“坏”。可以选择任意一个,但由于知道当前代码仓库的状态是“坏”的,所以最简单的做法是从标记“坏”提交开始。

那么,把当前提交标记为“坏”。

git bisect bad

也可以通过指定一个 Git 引用(例如 HEAD)、提交的 SHA 值,甚至是一个标签,来运行这个带“坏”提交的命令。如果当前查看的是仓库的头指针,并且知道这就是“坏”提交,那么上面的命令和运行下面的命令效果相同。

git bisect bad HEAD

接下来,需要告诉 Git 最后一次已知的“好”提交是什么时候;这同样可以是一个 Git 引用、提交的 SHA 值或一个标签。使用标签通常是个不错的起点,因为它应该指向一个能正常工作的版本。

git bisect good <SHA 值|标签>

找出最后一次已知的“好”提交可能有点棘手,但使用 Git bisect 的目的就在于,我们不知道问题是何时引入的,只知道问题肯定不存在的时间点。在 Git bisect 模式下,仍然可以使用常用的 “git log” 和 “git checkout” 命令在仓库中导航,但在进入二分查找之前找到一个“好”提交可能是个好主意。

一旦为二分查找命令设置了“好”和“坏”提交,它就会开始工作。它会检出“好”提交和“坏”提交之间中间位置的一个提交。这就是 Git 开始使用二分查找算法,帮助找出问题引入位置的地方。

本质上,所做的就是为 Git bisect 提供搜索参数的上下限,然后它会使用这些参数来搜索可用的提交,以找出问题引入的位置。

设置好这些范围后,会看到一条类似这样的消息。

Bisecting: 3 revisions left to test after this (roughly 2 steps)
[57abd582330996e929df7a7b4be2e17e3e6b2705] Some commit message.

这会显示当前检出的提交的 SHA 值和提交信息。它告诉我们二分查找算法还剩下大约 2 个步骤,这意味着只需再将提交标记为“好”或“坏”几次,就能得到结果。

如果这个提交不包含问题,可以将其标记为“好”。

git bisect good

然后,Git 会运行二分查找算法,并选择一个位于这个“好”点和最初指定的“坏”点之间的点。

如果新的提交包含问题,就将其标记为“坏”提交。

git bisect bad

如果这样做,Git 会运行二分查找算法,并选择一个位于这个“坏”点和最初指定的“好”点之间的点。

然后,Git 会检出一个新的提交,重复这个过程,直到二分查找算法缩小搜索范围,找到问题首次引入的提交。

最终,Git 会输出一条类似这样的消息,显示问题首次引入代码库的时间。

f88f003da43d691c12c548290cf2d0e03e8e2ad3 is the first bad commit
commit f88f003da43d691c12c548290cf2d0e03e8e2ad3
Author: Phil Norton <anon@example.com>
Date:   Fri Mar 29 22:15:39 2024 +0000

    Adding this amazing feature that totally won't break anything.

 index.php | 1 +
 1 file changed, 1 insertion(+)

这清楚地表明,index.php 文件被修改以引入一个功能,但这个功能也导致了问题。

一旦知道了“坏”改动是在哪个提交引入的信息,就可以通过运行 reset 命令将所有内容恢复到正常状态。

git bisect reset

有了这个提交的 SHA 值,就可以弄清楚为什么要引入这个改动,并着手修复它。或者,将这个问题分配给最初引入问题的开发者。

三、自动化测试

如果手动检查大量不同的提交看起来是个繁琐的过程,那么好消息是可以轻松实现自动化。

Git bisect 可以运行一个命令,能用它以某种方式测试代码,自动检测“好”或“坏”提交。可以使用一个独立的命令,或者在项目中运行某个脚本,其结果应该是:如果是“好”提交,退出代码为 0;如果是“坏”提交,退出代码在 1 到 127 之间(不包括 125)。一个好的单元测试系统如果测试失败,应该返回非零退出代码,但也可以依靠脚本产生的错误信息。

来看一个简单的例子,使用一个脚本来检测 index.php 文件中是否添加了一个名为 “variableName” 的变量。

以下是要使用的脚本。它非常简单,只是使用 grep 命令查看 index.php 文件的内容中是否包含该变量,如果找到,将返回退出代码 1。

#!/bin/sh

if grep -q variableName index.php; then
  exit 1
fi

exit 0

虽然脚本不会返回任何输出,但可以使用 “echo $?” 查看退出代码。注意,在像这样运行脚本之前,需要使其可执行。

$ ./test.sh
$ echo $?
1

然后,以正常方式启动 Git bisect,指定起始的“好”和“坏”提交。在这个例子中,选择 HEAD 作为“坏”提交,标签 “1.0” 作为最后一次已知的“好”提交。

git bisect start
git bisect bad
git bisect good 1.0

然后,可以使用 “git bisect run” 来运行脚本,并自动判断提交是“好”还是“坏”。

git bisect run ./test.sh

这将检出一个提交并运行测试,根据测试结果判断提交是“好”还是“坏”。由于这是一个简单的测试,bisect 命令运行非常快,几乎可以立即返回结果。

这种方法的挑战在于,需要能够运行可能存在于代码库中的测试或构建过程。由于 Git bisect 本质上是检出提交让你进行检查,不能更改测试并运行 Git bisect,否则会导致覆盖错误。此外,如果在项目中运行测试,而该测试因其他原因失败,这会产生一个错误的“坏”提交,从而影响自动化流程。如果创建一个小脚本(就像上面做的那样)从外部角度测试代码,这个过程通常会更有效。

四、一个示例

来看一个在真实的 Git 仓库中使用 Git bisect 的具体示例。

以下脚本将设置一个 Git 仓库,并向一个文件中添加一些文本,每次添加后都提交更改。第一个提交会被标记,这样就可以轻松引用它,而无需使用 SHA 值。

#!/bin/sh

# 初始化 Git 仓库
git init

# 添加一个包含一些内容的文件,并将其提交到仓库。
echo "Text" > file.txt
git add file.txt
git commit -m "Initial commit."

# 标记这个提交。
git tag -a "1.0" -m "1.0"

# 进行几次提交。
echo "More text" >> file.txt
git add file.txt
git commit -m "Commit 2"

echo "More text" >> file.txt
git add file.txt
git commit -m "Commit 3"

echo "More text" >> file.txt
git add file.txt
git commit -m "Commit 4"

# 创建我们的“坏”提交。
echo "A bad commit oooooooooooooooooo" >> file.txt
git add file.txt
git commit -m "Commit 5"

# 继续进行一些提交。
echo "Even more text" >> file.txt
git add file.txt
git commit -m "Commit 6"

echo "Some text" >> file.txt
git add file.txt
git commit -m "Commit 7"

echo "Some text" >> file.txt
git add file.txt
git commit -m "Commit 8"

这些提交中的一个(提交编号 5)向文件中添加了字符串 “A bad commit oooooooooooooooooo”,但在这之后又添加了更多的提交,所以现在有一个包含问题的文件和一个可供检查的 Git 历史记录。

以下是这个仓库的历史记录表示,标签 “1.0” 在左侧(提交 1 中),HEAD 在右侧。问题就出在这两个点之间的某个位置。

1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> HEAD

为了找到问题,使用 HEAD 作为已知的“坏”提交,“1.0” 标签作为最后一次已知的“好”提交来设置 Git bisect。

git bisect start
git bisect bad
git bisect good 1.0

当运行最后一个命令后,会在命令行中看到以下内容。

Bisecting: 3 revisions left to test after this (roughly 2 steps)
[e2d8209b5ea2e7d1ac1728a1a44ecc12d0b7c3fb] Commit 4

在 Git 历史记录中,指针现在位于提交 4 处。

1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> HEAD
               ^

查看文件后发现,在这个时间点,有问题的字符串还不在文件中,所以将其标记为“好”提交。

git bisect good

然后,Git 会检出提交 6,并在命令行中显示以下内容。选择提交 6 是因为它位于最近已知的“坏”提交(提交 8)和最后标记为“好”的提交(提交 4)之间的中间位置。

Bisecting: 1 revision left to test after this (roughly 1 step)
[8b393238a6f6f4b1dd473d32ad79a285450f980a] Commit 6

在 Git 历史记录中,指针现在位于提交 6 处。

1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> HEAD
                         ^

检查文件后发现,有问题的字符串在这里,所以将其标记为“坏”提交。

git bisect bad

Git 会检出位于这个提交和最后一次已知的“好”提交之间的提交,这意味着 Git 历史记录指针现在指向提交 5。

1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> HEAD
                    ^

已经知道这就是引入更改的提交,但让我们完成 Git bisect 过程。

在命令行中会看到以下内容。

Bisecting: 0 revisions left to test after this (roughly 0 steps)
[bc2d487be1e93f9b5ff0f82df72e6e12a85789c0] Commit 5

Git 在这里表示,之后没有更多的提交需要测试了,所以这一定是引入“坏”代码的提交。查看文件可以发现有问题的文本存在,所以这肯定是要找的问题提交。

然后,可以通过将这个提交标记为“坏”来完成 Git bisect 命令。

git bisect bad

然后,Git 会显示一份报告,确认提交 5 是第一个“坏”提交,同时还会显示提交的 SHA 值和该提交的差异。

bc2d487be1e93f9b5ff0f82df72e6e12a85789c0 is the first bad commit
commit bc2d487be1e93f9b5ff0f82df72e6e12a85789c0
Author: Phil Norton <anon@example.com>
Date:   Sat Mar 30 10:42:17 2024 +0000

    Commit 5

 file.txt | 1 +
 1 file changed, 1 insertion(+)

现在,可以运行 “git bisect reset” 命令,将代码库恢复到原始状态。

五、结论

Git bisect 是 Git 中非常有用的一部分,它能让你更轻松地找出代码库中引入问题的位置。在 Drupal 开发、Drupal 模块开发和 Drupal 升级过程中,使用 Git bisect 可以高效地定位代码问题,尤其对于 Drupal11 这样功能不断更新的版本而言,其作用更为显著。

这个命令的主要优势在于,不一定需要知道问题的根源是什么,只需要知道存在问题,并且问题是在项目历史的某个地方引入的即可。通过运行 Git bisect,可以查看项目过去的快照,逐步缩小问题引入的时间范围,直到找到单个提交。通常情况下,查看单个提交中发生的更改导致的问题,比面对一个包含数千次提交的大型代码库要容易得多。

Git bisect 的优点在于,即使代码库有数千次提交,它也能很好地工作。“run” 子命令可以让这个过程变得非常快速;不过,创建一个适用于大型代码库的自动化测试可能有点挑战。如果无法使用项目内置的构建和测试工具来自动化 bisect 运行,那么设置某种不受项目内部更改影响的外部测试是个好主意。如果这样做,通常最好大致了解可能导致问题的原因,但这并不是必需的。

如何利用 bisect 命令提供的信息取决于你自己。但如果项目管理规范,提交消息应该包含所做更改的详细信息,包括一个工单引用,可以使用这个引用来查看更多关于该更改的历史记录。现在最重要的收获是知道了引入破坏性功能的更改是在什么时候发生的。

这里只是简单介绍了 Git bisect 命令(这是一篇入门指南),还有很多其他选项可以传递给 Git bisect,从而改变它的运行方式。可以在 Git 文档页面上了解更多关于 Git bisect 命令的信息。