解决冲突¶
噢不;拣选失败,并出现了一个模糊的威胁性消息:
CONFLICT (content): Merge conflict
现在该怎么办?
一般来说,当补丁的上下文(即被更改的行和/或围绕更改的行)与您尝试应用补丁的树中的内容不匹配时,就会出现冲突。
对于回溯补丁,可能发生的情况是您回溯来源的分支包含了一些您回溯到的分支中没有的补丁。然而,反过来也可能发生。无论如何,结果都是需要解决的冲突。
如果您尝试的拣选因冲突而失败,git 会自动编辑文件以包含所谓的冲突标记,向您展示冲突的位置以及两个分支如何发生了分歧。解决冲突通常意味着以一种考虑这些其他提交的方式来编辑最终结果。
解决冲突可以通过在常规文本编辑器中手动完成,也可以使用专用的冲突解决工具。
许多人更喜欢使用他们的常规文本编辑器直接编辑冲突,因为这样可能更容易理解您正在做什么并控制最终结果。两种方法各有利弊,有时两者结合使用也很有价值。
除了提供一些您可能使用的各种工具的指引外,我们在此不会涵盖专用合并工具的使用:
Emacs Ediff 模式
vimdiff/gvimdiff
KDiff3
TortoiseMerge
Meld
P4Merge
Beyond Compare
IntelliJ
VSCode
要配置 git 以便与这些工具配合使用,请参阅 git mergetool --help 或官方的 git-mergetool 文档。
前置补丁¶
大多数冲突的发生,是因为您回溯到的分支,相较于您回溯自的分支,缺少了一些补丁。在更一般的情况下(例如合并两个独立分支),开发可能发生在任一分支上,或者分支只是简单地分叉了——也许您的旧分支应用了一些其他回溯补丁,这些补丁本身也需要冲突解决,导致了分歧。
识别导致冲突的一个或多个提交始终很重要,否则您无法对解决方案的正确性充满信心。作为额外的好处,特别是当补丁在您不熟悉的领域时,这些提交的变更日志通常会为您提供理解代码以及解决冲突时潜在问题或陷阱的上下文。
git log¶
一个好的第一步是查看冲突文件的 git log — 当文件没有太多补丁时,这通常就足够了,但如果文件很大并且经常打补丁,可能会让人感到困惑。您应该在当前检出的分支(HEAD)和您正在拣选的补丁的父提交(
git log HEAD..
更好的是,如果您想将此输出限制到单个函数(因为冲突出现在那里),您可以使用以下语法:
git log -L:'\
注意
函数名周围的 \< 和 \> 确保匹配是锚定在单词边界上的。这很重要,因为这部分实际上是一个正则表达式,git 只会跟随第一个匹配项,所以如果您使用 -L:thread_stack:kernel/fork.c,它可能只为您提供函数 try_release_thread_stack_to_cache 的结果,尽管该文件中还有许多其他函数包含字符串 thread_stack 在它们的名称中。
另一个有用的 git log 选项是 -G,它允许您根据列表中提交的差异中出现的特定字符串进行过滤:
git log -G'regex' HEAD..
这也可以是一种方便快捷的方式,可以快速查找某些内容(例如函数调用或变量)何时被更改、添加或删除。搜索字符串是一个正则表达式,这意味着您可能可以搜索更具体的内容,例如对特定结构成员的赋值:
git log -G'\->index\>.*='
git blame¶
查找前置提交的另一种方法(尽管只适用于给定冲突的最新提交)是运行 git blame。在这种情况下,您需要针对您正在拣选的补丁的父提交以及发生冲突的文件运行它,即:
git blame
此命令也接受 -L 参数(用于将输出限制到单个函数),但在这种情况下,您像往常一样在命令末尾指定文件名:
git blame -L:'\
导航到冲突发生的地方。blame 输出的第一列是添加给定代码行的补丁的提交 ID。
最好 git show 这些提交,看看它们是否像是冲突的来源。有时会有多个这样的提交,这可能是因为多个提交更改了同一冲突区域的不同行,或者是因为多个后续补丁多次更改了同一行(或多行)。在后一种情况下,您可能需要再次运行 git blame 并指定要查看的文件的旧版本,以便更深入地追溯文件的历史。
前置补丁与偶然补丁¶
找到导致冲突的补丁后,您需要确定它是否是您正在回溯的补丁的前提条件,或者它只是偶然的,可以跳过。偶然补丁是指与您正在回溯的补丁接触相同的代码,但不会以任何实质性方式改变代码语义的补丁。例如,一个空白清理补丁是完全偶然的——同样,一个仅仅重命名函数或变量的补丁也可能是偶然的。另一方面,如果被更改的函数在您当前的分支中甚至不存在,那么这根本不是偶然的,您需要仔细考虑是否应该首先拣选添加该函数的补丁。
如果您发现存在必要的先决补丁,那么您需要停止并拣选该补丁。如果您已经解决了不同文件中的一些冲突,并且不想再做一遍,您可以创建该文件的临时副本。
要中止当前的拣选,请运行 git cherry-pick --abort,然后使用先决补丁的提交 ID 重新开始拣选过程。
理解冲突标记¶
合并差异(Combined diffs)¶
假设您已决定不拣选(或恢复)额外的补丁,只想解决冲突。Git 会在您的文件中插入冲突标记。开箱即用,这看起来会像这样:
<<<<<<< HEAD
this is what's in your current tree before cherry-picking
=======
this is what the patch wants it to be after cherry-picking
>>>>>>>
如果您在编辑器中打开文件,您会看到这样的内容。但是,如果您不带任何参数运行 git diff,输出将看起来像这样:
$ git diff
[...]
++<<<<<<<< HEAD
+this is what's in your current tree before cherry-picking
++========
+ this is what the patch wants it to be after cherry-picking
++>>>>>>>>
当您解决冲突时,git diff 的行为与正常行为不同。注意,这里是两列差异标记,而不是通常的一列;这是一种所谓的“合并差异(combined diff)”,这里显示的是三方差异(或差异的差异)在以下两者之间:
当前分支(拣选前)和当前工作目录,以及
当前分支(拣选前)和应用原始补丁后的文件外观。
更好的差异(Better diffs)¶
三方合并差异包括在您的当前分支和您正在拣选的分支之间对文件发生的所有其他更改。虽然这对于发现您需要考虑的其他更改很有用,但这也使得 git diff 的输出有些令人望而生畏且难以阅读。您可能更喜欢运行 git diff HEAD(或 git diff --ours),它只显示当前分支在拣选之前与当前工作目录之间的差异。它看起来像这样:
$ git diff HEAD
[...]
+<<<<<<<< HEAD
this is what's in your current tree before cherry-picking
+========
+this is what the patch wants it to be after cherry-picking
+>>>>>>>>
如您所见,这读起来就像任何其他差异一样,并清楚地表明了哪些行在当前分支中,以及哪些行是由于合并冲突或正在拣选的补丁而添加的。
合并风格与 diff3¶
上面显示的默认冲突标记样式称为 merge 样式。还有另一种可用的样式,称为 diff3 样式,它看起来像这样:
<<<<<<< HEAD
this is what is in your current tree before cherry-picking
||||||| parent of
this is what the patch expected to find there
=======
this is what the patch wants it to be after being applied
>>>>>>>
如您所见,这有 3 个部分而不是 2 个,并包含了 git 预期会找到但未找到的内容。强烈建议使用这种冲突样式,因为它能更清楚地显示补丁实际更改了什么;即,它允许您比较您正在拣选的提交的文件修改前后版本。这使您能够对如何解决冲突做出更好的决策。
要更改冲突标记样式,您可以使用以下命令:
git config merge.conflictStyle diff3
还有第三个选项,zdiff3,在 Git 2.35 中引入,它具有与 diff3 相同的 3 个部分,但共同的行已被删除,在某些情况下使冲突区域更小。
迭代解决冲突¶
任何冲突解决过程的第一步是理解您正在回溯的补丁。对于 Linux 内核来说,这尤为重要,因为不正确的更改可能导致整个系统崩溃——或者更糟,导致未被检测到的安全漏洞。
理解补丁的难易程度取决于补丁本身、变更日志以及您对被更改代码的熟悉程度。然而,对于每次更改(或补丁的每个代码块),一个很好的问题可能是:“为什么这个代码块会出现在补丁中?”这些问题的答案将指导您的冲突解决。
解决过程¶
有时最简单的方法是只保留冲突的第一部分,使文件基本保持不变,然后手动应用更改。也许补丁正在将函数调用参数从 0 更改为 1,而一个冲突的更改在参数列表的末尾添加了一个全新的(且不重要的)参数;在这种情况下,手动将参数从 0 更改为 1,并保留其他参数就足够简单了。这种手动应用更改的技术主要在冲突引入了大量您不需要关心的不相关上下文时有用。
对于带有许多冲突标记的特别棘手的冲突,您可以使用 git add 或 git add -i 来选择性地暂存您的解决方案,以将其移开;这还允许您使用 git diff HEAD 始终查看还有哪些需要解决,或使用 git diff --cached 查看您的补丁目前看起来如何。
处理文件重命名¶
在回溯补丁时,最令人恼火的事情之一是发现其中一个被修补的文件已被重命名,因为这通常意味着 git 甚至不会放置冲突标记,而只是摊手说(意译): “未合并的路径!你自己搞定吧……”
通常有几种方法可以解决这个问题。如果重命名文件的补丁很小,比如只有一行更改,最简单的办法就是直接手动应用更改并完成。另一方面,如果更改很大或很复杂,您肯定不想手动操作。
作为第一步,您可以尝试这样做,它会将重命名检测阈值降低到 30%(默认情况下,git 使用 50%,这意味着两个文件需要至少有 50% 的共同点,它才会将添加-删除对视为潜在的重命名):
git cherry-pick -strategy=recursive -Xrename-threshold=30
有时,正确的做法是也回溯执行重命名的补丁,但这肯定不是最常见的情况。相反,您可以做的是临时重命名您正在回溯到的分支中的文件(使用 git mv 并提交结果),重新尝试拣选补丁,再将文件重命名回来(再次使用 git mv 并再次提交),最后使用 git rebase -i(参见 rebase 教程)将结果压缩,这样在您完成时它显示为一个单一提交。
陷阱¶
函数参数¶
注意改变函数参数!很容易忽略细节,认为两行相同,但实际上它们在一些小细节上有所不同,比如传递的变量是哪个(特别是如果这两个变量都是一个看起来相同的单字符,比如 i 和 j)。
错误处理¶
如果您拣选的补丁包含 goto 语句(通常用于错误处理),那么绝对有必要仔细检查目标标签在您回溯到的分支中是否仍然正确。对于新增的 return、break 和 continue 语句也是如此。
错误处理通常位于函数的底部,因此即使它可能已被其他补丁更改,它也可能不属于冲突。
确保您检查错误路径的一个好方法是始终在检查您的更改时使用 git diff -W 和 git show -W(即 --function-context)。对于 C 代码,这会显示补丁中被更改的整个函数。在回溯过程中经常出错的一件事是,函数中的其他内容在您回溯的源分支或目标分支上发生了变化。通过在差异中包含整个函数,您可以获得更多上下文,并且更容易发现否则可能被忽视的问题。
重构的代码¶
经常发生的情况是,通过将常见的代码序列或模式“提取”到辅助函数中来重构代码。当回溯补丁到发生此类重构的区域时,您实际上需要在回溯时进行反向操作:对单个位置的补丁可能需要应用于回溯版本中的多个位置。(这种情况的一个线索是函数被重命名了——但这并非总是如此。)
为避免不完整的反向移植,值得尝试弄清楚该补丁是否修复了在多个地方出现的错误。一种方法是使用 git grep。(这实际上通常是个好主意,不只针对反向移植。)如果您确实发现在上游树中存在相同类型的修复会适用于其他地方,那么也值得看看这些地方是否存在于上游——如果不存在,则补丁可能需要调整。git log 是您的朋友,可以帮助您弄清楚这些区域发生了什么,因为 git blame 不会显示已删除的代码。
如果您在上游树中确实发现了相同模式的其他实例,并且不确定它是否也是一个错误,那么值得询问补丁作者。在回溯过程中发现新 bug 并不少见!