回溯移植和冲突解决¶
- 作者:
Vegard Nossum <vegard.nossum@oracle.com>
简介¶
一些开发人员可能在日常工作中永远不必处理回溯移植补丁、合并分支或解决冲突,因此当出现合并冲突时,可能会感到不知所措。幸运的是,解决冲突是一种与其他技能相同的技能,并且您可以使用许多有用的技术来使过程更顺畅并增强您对结果的信心。
本文档旨在成为回溯移植和冲突解决的全面、循序渐进的指南。
将补丁应用于树¶
有时,您正在回溯移植的补丁已经作为 git 提交存在,在这种情况下,您只需使用 git cherry-pick
直接选取它。但是,如果补丁来自电子邮件(就像 Linux 内核中经常发生的那样),您需要使用 git am
将其应用于树。
如果您曾经使用过 git am
,您可能已经知道它对补丁完美应用于您的源代码树非常挑剔。事实上,您可能已经做过关于 .rej
文件并尝试编辑补丁以使其应用的噩梦。
强烈建议您找到一个合适的基版本,该补丁可以干净地应用,然后 将其樱桃挑选到您的目标树,因为这将使 git 输出冲突标记,并让您借助 git 和您可能更喜欢的任何其他冲突解决工具来解决冲突。例如,如果您想将刚刚在 LKML 上收到的补丁应用于较旧的稳定内核,您可以将其应用于最新的主线内核,然后将其樱桃挑选到较旧的稳定分支。
通常最好使用与生成补丁的基版本完全相同的基版本,但只要它能干净地应用并且与原始基版本不太远,实际上就没有那么重要。将补丁应用于“错误”基版本的唯一问题是,当将其樱桃挑选到较旧的分支时,它可能会在差异的上下文中引入更多不相关的更改。
更喜欢 git cherry-pick
而不是 git am
的一个好理由是,git 知道现有提交的精确历史,因此它会知道何时代码被移动并更改了行号;这反过来使得它不太可能将补丁应用到错误的位置(这可能会导致无声的错误或混乱的冲突)。
如果您正在使用 b4。并且您正在直接从电子邮件应用补丁,您可以使用选项 -g
/--guess-base
和 -3
/--prep-3way
使用 b4 am
自动执行其中一些操作(有关更多信息,请参见 b4 演示文稿)。但是,本文的其余部分将假定您正在执行普通的 git cherry-pick
。
将补丁放入 git 后,您可以继续将其樱桃挑选到您的源代码树中。如果您想要记录补丁的来源,请不要忘记使用 -x
进行樱桃挑选!
请注意,如果您正在为稳定版提交补丁,则格式略有不同;主题行之后的第一行需要是
commit <upstream commit> upstream
或者
[ Upstream commit <upstream commit> ]
解决冲突¶
哎呀;樱桃挑选失败,并显示了一条含糊不清的威胁性消息
CONFLICT (content): Merge conflict
现在该怎么办?
一般来说,当补丁的上下文(即,被更改的行和/或更改周围的行)与您尝试将补丁应用到的树中的内容不匹配时,会出现冲突。
对于回溯移植,可能发生的情况是,您正在从中回溯移植的分支包含您正在回溯移植到的分支中没有的补丁。但是,反过来也是可能的。在任何情况下,结果都是需要解决的冲突。
如果您的樱桃挑选尝试失败并出现冲突,git 会自动编辑文件以包含所谓的冲突标记,向您显示冲突发生的位置以及两个分支如何分歧。解决冲突通常意味着以某种方式编辑最终结果,使其考虑到这些其他提交。
可以使用常规文本编辑器手动解决冲突,也可以使用专门的冲突解决工具解决冲突。
许多人喜欢使用常规文本编辑器并直接编辑冲突,因为这可能更容易理解您正在做什么并控制最终结果。每种方法都有其优点和缺点,有时同时使用这两种方法都有价值。
除了提供一些指向您可以使用的各种工具的指针之外,我们不会在这里介绍使用专门的合并工具
要配置 git 以使用这些,请参阅 git mergetool --help
或官方 git-mergetool 文档。
先决条件补丁¶
大多数冲突发生的原因是,您正在回溯移植到的分支与您正在回溯移植自的分支相比缺少一些补丁。在更一般的情况下(例如合并两个独立的分支),开发可能发生在任一分支上,或者分支只是分歧了 - 也许您的旧分支应用了一些其他回溯移植,这些回溯移植本身需要解决冲突,从而导致分歧。
始终识别导致冲突的提交或提交非常重要,否则您无法确信您的解决方案的正确性。此外,特别是如果补丁在您不熟悉的区域中,这些提交的更改日志通常会为您提供理解代码以及潜在问题或冲突解决的陷阱的上下文。
git log¶
一个好的第一步是查看存在冲突的文件的 git log
-- 当该文件没有太多补丁时,这通常足够了,但如果文件很大且经常被打补丁,则可能会让人感到困惑。您应该在当前检出的分支(HEAD
)和您要挑选的补丁的父级(<commit>
)之间的提交范围内运行 git log
,即
git log HEAD..<commit>^ -- <path>
更好的是,如果您想将此输出限制为单个函数(因为这是出现冲突的地方),则可以使用以下语法
git log -L:'\<function\>':<path> HEAD..<commit>^
注意
函数名称周围的 \<
和 \>
确保匹配项锚定在单词边界上。这很重要,因为这部分实际上是一个正则表达式,并且 git 只遵循第一个匹配项,因此如果您使用 -L:thread_stack:kernel/fork.c
,它可能只为您提供函数 try_release_thread_stack_to_cache
的结果,即使该文件中还有许多其他函数在其名称中包含字符串 thread_stack
。
git log
的另一个有用选项是 -G
,它允许您根据您正在列出的提交的差异中出现的某些字符串进行过滤
git log -G'regex' HEAD..<commit>^ -- <path>
这也可以快速找到何时更改、添加或删除某些内容(例如函数调用或变量)的便捷方法。搜索字符串是一个正则表达式,这意味着您可能会搜索更具体的内容,例如对特定结构成员的赋值
git log -G'\->index\>.*='
git blame¶
查找先决条件提交的另一种方法(尽管只是针对给定冲突的最新提交)是运行 git blame
。在这种情况下,您需要针对您正在樱桃挑选的补丁的父提交和出现冲突的文件运行它,即
git blame <commit>^ -- <path>
此命令也接受 -L
参数(用于将输出限制为单个函数),但在这种情况下,您像往常一样在命令末尾指定文件名
git blame -L:'\<function\>' <commit>^ -- <path>
导航到发生冲突的位置。blame 输出的第一列是添加给定代码行的补丁的提交 ID。
查看这些提交并确认它们是否可能是冲突的根源,这可能是个好主意。 有时会有多个这样的提交,要么是因为多个提交更改了同一冲突区域的不同行,或者是因为多个后续补丁多次更改了同一行(或多行)。 在后一种情况下,您可能需要再次运行 git blame
并指定文件的旧版本,以便更深入地挖掘文件的历史记录。
先决条件补丁与偶然补丁¶
找到导致冲突的补丁后,您需要确定它是您正在回溯的补丁的先决条件,还是只是偶然的,可以跳过。 偶然的补丁是指与您正在回溯的补丁相同的代码,但在任何实质性的方式上都没有改变代码的语义。 例如,空白清理补丁是完全偶然的 -- 同样,简单地重命名函数或变量的补丁也是偶然的。 另一方面,如果被更改的函数在您当前的分支中甚至不存在,那么这就不是偶然的,您需要仔细考虑是否应该先 cherry-pick 添加该函数的补丁。
如果您发现存在必要的先决条件补丁,那么您需要停止并 cherry-pick 该补丁。 如果您已经在另一个文件中解决了一些冲突,并且不想再次执行此操作,则可以创建该文件的临时副本。
要中止当前的 cherry-pick,请运行 git cherry-pick --abort
,然后使用先决条件补丁的提交 ID 重新开始 cherry-pick 过程。
理解冲突标记¶
组合差异¶
假设您已决定不选择(或还原)其他补丁,而只想解决冲突。 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
>>>>>>> <commit>... title
如果您在编辑器中打开该文件,您会看到这样的内容。 但是,如果您在不带任何参数的情况下运行 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
++>>>>>>>> <commit>... title
当您解决冲突时,git diff
的行为与其正常行为不同。 请注意两列差异标记,而不是通常的一列; 这是一种所谓的“组合差异”,这里显示的是以下两者之间的 3 向差异(或差异的差异):
当前分支(在 cherry-pick 之前)和当前工作目录,以及
当前分支(在 cherry-pick 之前)和应用原始补丁后的文件外观。
更好的差异¶
3 向组合差异包括在您当前分支和您正在进行 cherry-pick 的分支之间对文件发生的所有其他更改。 虽然这对于发现您需要考虑的其他更改很有用,但这也会使 git diff
的输出有些令人生畏且难以阅读。 您可能更喜欢运行 git diff HEAD
(或 git diff --ours
),它仅显示 cherry-pick 之前的当前分支和当前工作目录之间的差异。 它看起来像这样
$ 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
+>>>>>>>> <commit>... title
如您所见,它的读取方式与任何其他差异一样,并且清楚地表明哪些行位于当前分支中,哪些行由于是合并冲突或正在 cherry-pick 的补丁的一部分而被添加。
合并样式和 diff3¶
上面显示的默认冲突标记样式称为 merge
样式。 还有另一种样式可用,称为 diff3
样式,它看起来像这样
<<<<<<< HEAD
this is what is in your current tree before cherry-picking
||||||| parent of <commit> (title)
this is what the patch expected to find there
=======
this is what the patch wants it to be after being applied
>>>>>>> <commit> (title)
如您所见,它有 3 个部分而不是 2 个部分,并且包括 git 期望在那里找到但没有找到的内容。 强烈建议使用这种冲突样式,因为它更清楚地表明了补丁实际更改的内容; 即,它允许您比较您正在 cherry-pick 的提交的文件的前后版本。 这使您可以更好地决定如何解决冲突。
要更改冲突标记样式,您可以使用以下命令
git config merge.conflictStyle diff3
在 Git 2.35 中引入了第三个选项 zdiff3
,它具有与 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
并提交结果),重新开始尝试 cherry-pick 补丁,将文件重命名回来(git mv
并再次提交),最后使用 git rebase -i
压缩结果(请参阅 rebase 教程),以便在完成后显示为单个提交。
陷阱¶
函数参数¶
注意更改函数参数! 很容易忽略细节,并认为两行是相同的,但实际上它们在某些小细节上有所不同,例如哪个变量作为参数传递(尤其是如果两个变量都是看起来相同的单个字符,例如 i 和 j)。
错误处理¶
如果您 cherry-pick 包含 goto
语句(通常用于错误处理)的补丁,则绝对必须仔细检查目标标签在您正在回溯到的分支中是否仍然正确。 添加的 return
、break
和 continue
语句也是如此。
错误处理通常位于函数的底部,因此即使可能已被其他补丁更改,也可能不属于冲突的一部分。
确保您审查错误路径的一个好方法是始终在检查更改时使用 git diff -W
和 git show -W
(又名 --function-context
)。 对于 C 代码,这将向您显示补丁中正在更改的整个函数。 在回溯期间经常出错的一件事是,在您正在回溯的分支的任一分支上,函数中的其他内容发生了更改。 通过在差异中包含整个函数,您可以获得更多上下文,并且可以更容易地发现可能被忽略的问题。
重构的代码¶
经常发生的事情是,代码被重构为通过将公共代码序列或模式“分解”为帮助器函数。 当将补丁回溯到已进行此类重构的区域时,您在回溯时实际上需要反向操作:对单个位置的补丁可能需要在回溯版本中应用到多个位置。 (这种情况的一个迹象是函数被重命名了 -- 但情况并非总是如此。)
为了避免不完整的反向移植,值得尝试找出该补丁是否修复了在多个地方出现的错误。一种方法是使用 git grep
。(实际上,这是一个普遍适用的好主意,而不仅仅是针对反向移植。)如果您发现同一类型的修复适用于其他地方,也值得看看这些地方是否存在于上游——如果不存在,则很可能需要调整该补丁。git log
是您了解这些区域发生了什么的好帮手,因为 git blame
不会显示已被删除的代码。
如果您在上游树中找到同一模式的其他实例,并且不确定它是否也是一个错误,那么可以询问该补丁的作者。在反向移植过程中发现新的错误并不罕见!
验证结果¶
colordiff¶
在提交了一个无冲突的新补丁后,您现在可以将您的补丁与原始补丁进行比较。强烈建议您使用诸如 colordiff 之类的工具,它可以并排显示两个文件,并根据它们之间的更改进行颜色标记。
colordiff -yw -W 200 <(git diff -W <upstream commit>^-) <(git diff -W HEAD^-) | less -SR
在这里,-y
表示进行并排比较;-w
忽略空格,而 -W 200
设置输出的宽度(否则默认情况下将使用 130,这通常有点太少)。
rev^-
语法是 rev^..rev
的一个方便的简写,本质上只为您提供该单个提交的差异;另请参阅官方的 git rev-parse 文档。
再次注意 git diff
中包含 -W
;这确保您将看到任何已更改的函数的完整函数。
colordiff 一个非常重要的功能是突出显示不同的行。例如,如果原始补丁和反向移植补丁之间错误处理 goto
的标签已更改,则 colordiff 将并排显示这些标签,但以不同的颜色突出显示。因此,很容易看出两个 goto
语句跳转到不同的标签。同样,未被任何补丁修改但在上下文中不同的行也将被突出显示,从而在人工检查期间脱颖而出。
当然,这只是一个视觉检查;真正的测试是构建和运行已打补丁的内核(或程序)。
构建测试¶
我们在这里不讨论运行时测试,但仅构建补丁修改的文件作为快速的健全性检查是一个好主意。对于 Linux 内核,您可以像这样构建单个文件,假设您已正确设置了 .config
和构建环境
make path/to/file.o
请注意,这不会发现链接器错误,因此您仍然应该在验证单个文件编译后进行完整构建。通过首先编译单个文件,您可以避免在您更改的任何文件中出现编译器错误的情况下等待完整构建。
运行时测试¶
即使成功的构建或启动测试也不足以排除某处缺少依赖项。即使可能性很小,也可能存在代码更改,其中对同一文件的两个独立更改不会导致冲突,没有编译时错误,并且仅在特殊情况下才会出现运行时错误。
一个具体的例子是对系统调用入口代码的一对补丁,其中第一个补丁保存/恢复一个寄存器,而后面的补丁在该序列的中间某个位置使用了相同的寄存器。由于更改之间没有重叠,因此可以挑选第二个补丁,没有冲突,并认为一切都很好,而实际上代码现在正在覆盖一个未保存的寄存器。
尽管绝大多数错误将在编译期间或通过表面上运行代码来捕获,但真正验证反向移植的唯一方法是使用与您(或应该)给其他任何补丁相同的审查级别来审查最终补丁。拥有单元测试和回归测试或其他类型的自动测试可以帮助增加对反向移植正确性的信心。
向 stable 提交反向移植¶
当 stable 维护者尝试将主线修复挑选到他们的 stable 内核上时,他们在遇到冲突时可能会发送电子邮件请求反向移植,例如,请参阅 <https://lore.kernel.org/stable/2023101528-jawed-shelving-071a@gregkh/>。这些电子邮件通常包含您需要将补丁挑选到正确树并提交补丁的确切步骤。
要确保的一件事是您的变更日志符合预期的格式
<original patch title>
[ Upstream commit <mainline rev> ]
<rest of the original changelog>
[ <summary of the conflicts and their resolutions> ]
Signed-off-by: <your name and email>
“上游提交”行有时会根据 stable 版本略有不同。较旧的版本使用这种格式
commit <mainline rev> upstream.
最常见的是在电子邮件主题行中指示补丁适用的内核版本(例如,使用 git send-email --subject-prefix='PATCH 6.1.y'
),但您也可以将其放在 Signed-off-by: 区域或 ---
行下方。
stable 维护者希望为每个活动的 stable 版本单独提交,并且每个提交也应单独进行测试。
一些最后的建议¶
以谦虚的态度对待反向移植过程。
理解您要反向移植的补丁;这意味着阅读变更日志和代码。
在提交补丁时,如实说明您对结果的信心。
向相关维护者请求明确的 ack。
示例¶
以上大致显示了反向移植补丁的理想化过程。有关更具体的示例,请参阅此视频教程,其中两个补丁从主线反向移植到 stable:反向移植 Linux 内核补丁。