为什么 git 在执行 rebase-merges rebase 时不会自动重新应用冲突解决方案?

Mir*_*ral 5 git git-merge git-rebase git-rerere

(类似于这个问题,但有一些上下文和演示为什么rerere不是答案。)

对于给定的历史:

                /...o      origin/master
o...o...o...o...o...o...o  master
    \...o........../       topic
Run Code Online (Sandbox Code Playgroud)

我有一个主题分支,我已将其合并到 master 中,并进行了一次额外的提交。同时,上游有人对 origin/master 进行了另一次提交,所以我不能再按原样推送我的 master。

我想将我的 master 重新绑定到 origin/master 而不更改主题上的提交 SHA,并且不会丢失已经在 master 上执行的冲突解决方案。(这是迄今为止我想要保留合并提交的最常见情况,所以我很惊讶这显然如此困难。)

rerere启用,git rebase -p 几乎工程-在原来合并的任何冲突,它会记住我做了什么来解决这些问题并重新应用此(虽然它留下标记为冲突的,所以我必须要记住,纪念每一个的文件已经无需重新启动解决文件的冲突解决方案,这在 TortoiseGit 前端有点烦人)。但是,如果在合并提交中也修复了对文件的任何其他更改(例如,纯粹在合并中添加的行没有冲突,但由于其他地方的更改仍需要更正),这些都会丢失。

事情是这样的。在我对合并提交的(可能是有缺陷的)理解中,它们由两个(或更多)父项和一个唯一的变更集(用于存储冲突解决方案,以及在提交合并之前所做的任何其他更改或稍后修改为合并提交)组成。似乎rebase -p重新创建了合并提交,但完全丢弃了这个额外的变更集。

为什么它不重新应用原始合并提交中的变更集?这将使 rerere 变得多余并避免丢失这些额外的更改。如果需要人工确认,它可能会将受影响的文件标记为冲突,但在许多情况下,这种自动解决方案就完全足够了。

换句话说,标记上面的一些提交:

                /...N      origin/master
o...o...o...o...B...M...A  master
    \...T........../       topic

T - the commit on topic
B - the merge-base of origin/master and master
N - the new commit on origin/master
M - the merge between B and T
A - the extra post-merge commit
Run Code Online (Sandbox Code Playgroud)

M 有父节点 B 和 T 以及一个独特的变更集 Mc。创建 M' 时,git 在父 N 和 T 之间执行新的合并,并丢弃 Mc。为什么 git 不能只是重新应用 Mc 而不是丢弃它?

最后,我希望历史看起来像这样:

o...o...o...o...B...N...M'...A'  master
    \...T............../
Run Code Online (Sandbox Code Playgroud)

其中 M' 和 A' 从 rebase 更改 SHA1,但 M' 包含 Mc 变更集,而 T 未更改 SHA1 或父项。现在我可以将 origin/master 快进到 A'。


我还注意到有一个新选项一开始--rebase-merges听起来不错,之后确实会产生正确的图表 - 但就像--preserve-merges仍然因 M' 上的冲突而停止并丢失 Mc 中的任何独特更改,否则不会被 rerere 保存。


该问题的另一种表述可能更有用:

鉴于上面的初始状态,并且刚刚启动了一个现在处于 HEAD1 或 HEAD2 状态的交互式变基:

        /...........(T)
       /               \
      /             /...M'  HEAD2
     /              /...    HEAD1
    /           /...N       origin/master
o...o...o...o...B...M...A   master
    \...T........../        topic
Run Code Online (Sandbox Code Playgroud)

(HEAD1 已经检出 N 但还没有做其他事情;HEAD2 已经创建了一个新的合并,其中 N 作为父级 1,T 作为父级 2 但由于未解决的冲突尚未提交)

是否有一些 rebase 命令和/或 git 命令序列,它们将:

  1. 计算 M 和 B 之间的 diff Mc(选择 B 因为另一个父 T 没有变化)
  2. 将此应用于冲突树 M'(它应该完全解决所有冲突,除非 N 引入新冲突)简单地将其应用于 N 之上(无需先进行任何合并)——这些应该是等效的;第二个可能更容易
  3. 暂停以解决由 N 引入的任何剩余冲突(如果有)。
  4. 提交 M' 作为 N 和 T 之间的合并
  5. 照常继续(在这种情况下,在 M' 之上将 A 变基为 A')

为什么 git 默认不这样做?

tor*_*rek 5

git rerere无法记录非冲突的根本原因是它以一种廉价而肮脏的方式实现:Git 获取每个初始冲突,剥离一些数据以使其更适用(与剥离行号和一些空格的git rerere方式相同)git patch-id),然后将冲突作为 blob 对象保存在数据库中,并获取其存储在rerere目录中的哈希 ID。后来,当你git commit获得结果时,Git 会将一个特定的冲突更改 blob 与其解决方案配对。所以它只“知道”冲突,而不知道任何其他变化。

\n\n

稍后的合并(及其冲突)尝试再次保存冲突,再次获取哈希 ID,并找到配对,因此它使用保存的第二个 blob 作为解决方案。由于不冲突的更改不会保存在此处,因此它们永远不会作为此过程的一部分出现。

\n\n

Git 也许可以节省更多,但事实并非如此。

\n\n
\n

在我对合并提交的理解(可能有缺陷)中,它们由两个(或更多)父级和一个唯一的变更集(用于存储冲突解决方案,以及提交合并之前所做的任何其他更改或稍后修改为合并提交)组成。

\n
\n\n

这是不正确的。 所有提交都只是状态的快照。合并在这里并不特殊\xe2\x80\x94就像非合并提交一样,它们有一个完整的源树。他们的特别之处在于他们有两个(或更多)父母。

\n\n

复制非合并git cherry-pick(并且git rebase通过重复调用来重复执行git cherry-pick,或者做一些不太好但类似的事情),通过使用提交的(一个且唯一的)父级作为合并的合并基础来工作。作为动词的操作。复制合并通常是不可能的,并且 rebase 不会尝试:它只是重新执行合并。

\n\n

(另一方面,git cherry-pick将让您选择合并,使用其-m选项选择一个特定的父级。Git 只是假装这是三向合并操作期间的唯一父级。理论上,变基代码可以做同样的事情:-m 1几乎总是正确的父级,并且总是可以使用低级别git commit-tree进行实际提交,从而使其成为合并提交。但git rebase不这样做。)

\n\n
\n

...如果对合并提交中也已修复的文件有任何其他更改(例如,纯粹在合并中添加的行没有冲突,但由于其他地方的更改仍然需要更正),这些更改都会丢失。

\n
\n\n

是的(出于上面讨论的原因)。这也许是人们将此类事情称为“邪恶合并”的原因之一(尽管使用该短语的另一个可能原因是,至少从未来掌握的所有证据来看,这种变化实际上并不是任何人所要求的)。虽然它对现有合并的目标没有帮助,但我建议不要进行此类更改:相反,在合并之前或之后,在馈入或出合并的普通非合并提交中进行这些更改,以便稍后rebase -p或者rebase --rebase-merges 可以保存它们。

\n