“git rebase”使用什么算法?

6

我找不到有关git如何在内部执行rebase的解释。

最常见的答案是基于一个基础提交应用补丁,但我不明白这是如何实现的,因为没有办法正确地将补丁应用于(可能已完全更改的)文件。三方合并被提及,但没有明确说明合并了什么。

2个回答

13
如果你深入研究Git代码,你会发现实际上有多个不同的内部(或“后端”)算法用于rebase。这已经随着时间的推移而演变:Git 2.26(2020年3月)将默认设置从名为git-rebase--am的内部程序切换到git-rebase--interactive,而Git 2.12(2017年2月)将交互式变量从使用shell脚本切换到Git称之为其“序列器”的东西。因此,对于每个git rebase调用或每个Git版本,并不存在适用的单一正确答案。请注意,即使在非常旧的Git版本中,git rebase -i也使用git-rebase--interactive后端。

无论如何,可以将每个提交复制为似乎使用git cherry-pickgit format-patchgit am。使用旧的基于git am的算法时有一些细微差别-例如它根本不处理文件重命名;这就是它被淘汰的主要原因-而在现代Git的情况下,我们大多数情况下使用git cherry-pick,但是可能更容易考虑这个问题是“获取并应用补丁”。

那么,更有趣的事情实际上是git am和/或git cherry-pick如何完成其工作。长答案很冗长无聊,最好缩短为“去看源代码”,但简短的答案是:git am首先尝试将更改应用为补丁,仅在失败时才会回退到完整的三方合并。在git am中查看-3标志的简要说明。与此同时,git cherry-pick只进行直接的三方合并。

在这里使用的合并基础通常并不是很有用。考虑这个初始DAGlet和一个git rebase,它打算将A复制到A',将B复制到B',并将它们附加到提交D之上:

       A--B        <-- branch
      /
...--*------C--D   <-- origin/branch

第一个 cherry-pick 操作是选择 A,因此它会将提交 A 与两个分支的合并基础提交 * 进行比较。使用 git am 方法时,Git 尝试将其作为补丁应用到 D 上。如果失败了,或者你正在使用交互式 rebase 导致使用 git cherry-pick,则该提交(如果是 cherry-pick)或每个失败的文件(如果使用 git am)将通过三方合并过程运行。

实际上这非常合理:我们正在尝试在处理 *-to-D 后重新播放从 *A 的提交记录。最终结果是 A'

       A--B        <-- branch
      /
...--*------C--D   <-- origin/branch
                \
                 A'    [detached HEAD]

现在我们要复制 B,这对于 git am 意味着生成从 AB 的补丁。对于那些可以应用的补丁部分,我们只需将它们应用即可。对于存在冲突的文件,这一次我们针对每个无法应用补丁的文件,使用从 A-vs-BA-vs-A' 中得到的差异进行低级别文件合并。A 在这里是一种奇怪的合并基础,但它肯定比没有更好,并且通常能够完美地工作。

对于实际的 git cherry-pick 变基操作,Git 使用整个提交 A 作为合并基础,并在整个树上执行正常的三方合并。每个文件的合并基础都是来自提交 A 的该文件版本。

(如果我们有超过 B 的更多提交,这将继续进行。)

很容易看出,这对于非交互式操作来说必须是这样的:

git format-patch -k --stdout --full-index --cherry-pick --right-only \
    --src-prefix=a/ --dst-prefix=b/ --no-renames --no-cover-letter \
    "$revisions" ${restrict_revision+^$restrict_revision} \
    >"$GIT_DIR/rebased-patches"
...
git am $git_am_opt --rebasing --resolvemsg="$resolvemsg" \
    ${gpg_sign_opt:+"$gpg_sign_opt"} <"$GIT_DIR/rebased-patches"

因为git am仅获取Index:行以用于构建基础文件,所以在代码中很容易定位。但在git rebase代码中,关键部分深藏在sequencer.c中,难以找到。 在旧版Git中,您可以查看shell脚本并了解其如何运行git cherry-pick


1
谢谢@torek!你为什么说"git cherry-pick只是直接进行三方合并"?文档说它应用更改。我认为它是通过首先应用差异<remote-commit>^..<remote-commit>来实现的。 - Romain Pellerin
1
如果你深入挖掘源代码,你会发现 git cherry-pick 调用了序列器,序列器选择适当的父节点作为合并基础,然后根据需要运行 do_recursive_mergetry_merge_command。文档中的解释是一个善意的谎言,旨在更易于理解。 - torek
2
请注意,这与git rebase不同,后者可能(根据参数)调用git format-patchgit am。如果补丁可以干净地应用,则git am步骤可以应用补丁而无需经过三方合并。否则,由于使用了--3way和索引行(使用--full-index生成),它将进行三方合并。 - torek
2
@RomainPellerin:确实如此:如果没有合并基础,就无法在合并的三个点之间识别“相同”的文件。(它还可以更好地处理一些冲突变更情况。) - torek
2
@RomainPellerin:我的意思和你一样:你无法找到重命名的文件(即,识别修订版B中的文件foo与修订版L中的文件bar和修订版R中的文件baz是“相同的文件”)。换句话说,文件的身份不一定是其名称,它可能是其他某些特性,决定了两个文件是否是“相同的”文件。 - torek
显示剩余5条评论

4
最常见的回答是在基础提交上应用补丁,但这不可能是真实的。没有办法正确地将补丁应用于(可能)完全更改的文件。
这是真的 - 'rebase'本质上正在执行“ cherry-pick ”加一些其他的簿记工作。
但您的第二个句子也是正确的 - 如果文件已完全更改,则在尝试rebase时可能会遇到冲突。

那么这个三路合并发生在哪里?我指的不是冲突情况,而是自动合并。旧版 Git rebase 消息说:使用索引信息重建基础树...回退到修补基础和三路合并...我该如何理解这个? - listerreg
@listerreg:请查看https://dev59.com/Tl7Va4cB1Zd3GeqPIk9C。每个补丁都是从一对连续的提交自动生成的,然后应用于第三个提交。因此,“三方”。 - Oliver Charlesworth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接