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

问题描述

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

对于给定的历史:

                /...o      origin/master
o...o...o...o...o...o...o  master
    \...o........../       topic

我有一个已合并到 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

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

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

o...o...o...o...B...N...M'...A'  master
    \...T............../

其中 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

(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默认不这样做?

标签: gitgit-mergegit-rebasegit-rerere

解决方案


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

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

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

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

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

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

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

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

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


推荐阅读