章节 ▾ 第二版

7.10 Git 工具 - 使用 Git 进行调试

使用 Git 进行调试

除了作为版本控制的主要工具外,Git 还提供了一些命令来帮助你调试源代码项目。由于 Git 的设计可以处理几乎任何类型的内容,因此这些工具都相当通用,但它们在出现问题时通常可以帮助你查找 bug 或罪魁祸首。

文件注解

如果你发现了代码中的一个 bug,想知道它是何时以及为何被引入的,文件注解通常是你最好的工具。它可以显示修改每一行代码的最新提交。因此,如果你发现代码中的某个方法有 bug,你可以使用 git blame 命令来注解该文件,从而确定引入该行代码的提交。

下面的示例使用 git blame 命令来确定 Linux 内核顶层 Makefile 文件中各行的修改者和提交者,并使用 -L 选项将注解范围限制在该文件第 69 行至第 82 行。

$ git blame -L 69,82 Makefile
b8b0618cf6fab (Cheng Renquan  2009-05-26 16:03:07 +0800 69) ifeq ("$(origin V)", "command line")
b8b0618cf6fab (Cheng Renquan  2009-05-26 16:03:07 +0800 70)   KBUILD_VERBOSE = $(V)
^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 71) endif
^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 72) ifndef KBUILD_VERBOSE
^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 73)   KBUILD_VERBOSE = 0
^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 74) endif
^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 75)
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 76) ifeq ($(KBUILD_VERBOSE),1)
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 77)   quiet =
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 78)   Q =
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 79) else
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 80)   quiet=quiet_
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 81)   Q = @
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 82) endif

注意,第一字段是最后修改该行的提交的 SHA-1 摘要。接下来的两个字段是从该提交中提取的值——提交者的姓名和提交日期——因此你可以轻松地看到是谁以及何时修改了该行。之后是行号和文件内容。还要注意 ^1da177e4c3f4 提交行,其中 ^ 前缀表示该行在仓库的初始提交中被引入,并且自那时以来一直未被修改。这有点令人困惑,因为你现在至少看到了 Git 使用 ^ 修饰提交 SHA-1 的三种不同方式,但在这里它的意思就是这样。

Git 的另一个有趣之处在于它不显式跟踪文件重命名。它记录快照,然后尝试在事后隐式地找出哪些文件被重命名了。其中一个有趣的特性是,你可以要求它找出各种代码移动。如果你向 git blame 传递 -C 选项,Git 会分析你正在注解的文件,并尝试找出其中代码片段最初的来源(如果它们是从别处复制过来的)。例如,假设你正在将一个名为 GITServerHandler.m 的文件重构为多个文件,其中一个文件是 GITPackUpload.m。通过使用 -C 选项来 blame GITPackUpload.m,你可以看到代码段最初的来源。

$ git blame -C -L 141,153 GITPackUpload.m
f344f58d GITServerHandler.m (Scott 2009-01-04 141)
f344f58d GITServerHandler.m (Scott 2009-01-04 142) - (void) gatherObjectShasFromC
f344f58d GITServerHandler.m (Scott 2009-01-04 143) {
70befddd GITServerHandler.m (Scott 2009-03-22 144)         //NSLog(@"GATHER COMMI
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 145)
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 146)         NSString *parentSha;
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 147)         GITCommit *commit = [g
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 148)
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 149)         //NSLog(@"GATHER COMMI
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 150)
56ef2caf GITServerHandler.m (Scott 2009-01-05 151)         if(commit) {
56ef2caf GITServerHandler.m (Scott 2009-01-05 152)                 [refDict setOb
56ef2caf GITServerHandler.m (Scott 2009-01-05 153)

这非常有用。通常情况下,你看到的原始提交是你将代码复制过来的那个提交,因为那是你第一次修改该文件中的这些行。Git 会告诉你你编写这些行的原始提交,即使它是在另一个文件中。

如果你一开始就知道问题出在哪里,文件注解会很有帮助。如果你不知道是什么导致了问题,并且自上次代码正常工作的提交以来已经有几十甚至几百个提交,你可能会求助于 git bisectbisect 命令会在你的提交历史中进行二分查找,以帮助你尽可能快地找出引入问题的提交。

假设你刚刚将你的代码发布到了生产环境,你收到了关于一个在开发环境中不存在的 bug 的报告,而你无法想象为什么代码会这样做。你回到你的代码,发现你可以重现该问题,但你无法弄清楚哪里出了问题。你可以对代码进行二分查找来找出原因。首先,运行 git bisect start 来启动,然后使用 git bisect bad 来告诉系统你当前所在的提交是有问题的。接下来,你必须告诉 bisect 上一个已知的正常状态是什么,使用 git bisect good <good_commit> 命令。

$ git bisect start
$ git bisect bad
$ git bisect good v1.0
Bisecting: 6 revisions left to test after this
[ecb6e1bc347ccecc5f9350d878ce677feb13d3b2] Error handling on repo

Git 发现,在你标记为最后一个正常提交(v1.0)和当前有问题的版本之间大约有 12 个提交,它为你检出了中间那个。此时,你可以运行你的测试,看看是否仍然存在这个问题。如果存在,那么它是在此中间提交之前引入的;如果不存在,那么问题是在此中间提交之后引入的。结果发现这里没有问题,你通过输入 git bisect good 来告诉 Git,并继续你的旅程。

$ git bisect good
Bisecting: 3 revisions left to test after this
[b047b02ea83310a70fd603dc8cd7a6cd13d15c04] Secure this thing

现在你位于另一个提交上,这个提交是你刚刚测试的提交和你的有问题的提交之间的中间点。你再次运行测试,发现这个提交是有问题的,所以你通过 git bisect bad 来告诉 Git。

$ git bisect bad
Bisecting: 1 revisions left to test after this
[f71ce38690acf49c1f3c9bea38e09d82a5ce6014] Drop exceptions table

这个提交没问题,现在 Git 拥有了确定问题引入位置所需的所有信息。它会告诉你第一个有问题的提交的 SHA-1 摘要,并显示该提交的一些信息以及该提交中修改过的文件,以便你弄清楚是什么可能引入了这个 bug。

$ git bisect good
b047b02ea83310a70fd603dc8cd7a6cd13d15c04 is first bad commit
commit b047b02ea83310a70fd603dc8cd7a6cd13d15c04
Author: PJ Hyett <pjhyett@example.com>
Date:   Tue Jan 27 14:48:32 2009 -0800

    Secure this thing

:040000 040000 40ee3e7821b895e52c1695092db9bdc4c61d1730
f24d3c6ebcfc639b1a3814550e62d60b8e68a8e4 M  config

完成后,你应该运行 git bisect reset 来将你的 HEAD 重置到开始之前的位置,否则你将处于一个奇怪的状态。

$ git bisect reset

这是一个强大的工具,可以帮助你在几分钟内检查数百个提交以查找引入的 bug。事实上,如果你有一个脚本,在项目正常时退出 0,在项目有问题时退出非 0,那么你可以完全自动化 git bisect。首先,你再次通过提供已知的坏提交和好提交来告诉它二分查找的范围。如果你愿意,可以通过 bisect start 命令列出它们,先列出已知的坏提交,再列出已知的正确提交。

$ git bisect start HEAD v1.0
$ git bisect run test-error.sh

这样做会自动在每个检出的提交上运行 test-error.sh,直到 Git 找到第一个有问题的提交。你也可以运行类似 makemake tests 或任何其他运行自动化测试的命令。