`git rebase --onto --preserve-merges` 重现我已经在合并中解决的冲突

`git rebase --onto --preserve-merges` reproduces the conflicts I already resolved in a merge

我有一个类似于以下的 git 历史记录,它试图使旧功能与 master 保持同步。

* (origin/master, origin/HEAD, master) commit B'
* commit Y
* commit X
| * (HEAD -> feature) commit D
| *   commit C
| |\  
| | * commit B
| |/  
|/|   
* | commit A 
  * commit OLD

Master 指的是提交 A,我不得不添加一个大的修复程序,这会与旧的非集成功能混淆很多。我将该修复(一开始不用担心该功能)提交到提交 B。然后我将 master 合并到旧功能中,有许多冲突需要解决。我在 feature 分支中再次提交,因为我稍微更新了功能。一切正常并已构建,所以我将提交 B 推送到 master。但是(通过 gerrit),该提交是 rebased 超过同时推送的两个新提交 X 和 Y,它给出 "duplicated" 提交 B'.

我尝试使用命令

对提交 B' 的合并进行变基
git rebase --onto master B feature --preserve-merges

但是我仍然有 完全 与我将 a 与提交 C 合并时相同的冲突。做起来很长,我不想重做工作。我应该怎么做才能让我的历史看起来像这样?

  * (HEAD -> feature) commit D'
  *   commit C'
 /|
* | (origin/master, origin/HEAD, master) commit B'
* | commit Y
* | commit X
* | commit A 
  * old commit OLD

感谢您的帮助!

这是自然的,因为 git rebase 必须 copy 提交并且没有复制合并的通用方法(相对于 git cherry-pick 复制的通用方法非合并提交)。所以保留合并 git rebase 实际上重新执行 合并。

如果你能保证三个输入到原始合并——它们是BOLD,以及我们看不到的合并基础提交,它位于显示日志的底部——与新合并的三个输入树相同——这些是B', OLD, 并且可能是相同的合并基础,但我们无法确定 - 那么合并 result 将是相同的。

但是,B' 的树似乎不同于 B 的树,因为 B'XY 在其历史中也是如此。 B'B在他们的历史中都有A,所以如果BA的进化,B'可能是[的进化=33=] 这可能是 X 的演变,也可能是 A 的演变,使得 B' 具有 XY 的变化在里面。所以最终的合并结果应该是而不是原来的合并结果。事实上,它可能或多或少应该是 "what's in C, but evolved by applying the evolutions in X and Y, as if by cherry-pick."(最后一点是您可以在此处使用的一种策略的线索。)

冲突可能完全相同,合并每个冲突的结果可能与上次完全相同时间。如果是这样的话,git rerere 会做你想做的事。

此处有两条路径可用

我们刚刚提到了 Git 可以帮助您的两件事。不过,由于不同的原因,两者都有点棘手。

最好的可能是 git rerere。这个 re-use recorded resolutions 东西,内置于 Git ,在 git merge 失败时保存 conflicts,然后当您修复冲突和 git add 已解决的版本以及 git commit 结果时,它也保存他们的决议。 (这两个步骤实际上发生在失败的合并之后,并在最终的 git commit 期间发生。)然后,在后续的 git merge 上——例如 git rebase --preserve 调用的新 git merge — 有冲突,Git 会查看这些冲突是否有已保存、记录的解决方案。如果是这样,Git 对这些冲突应用这些解决方案,只留下 新的 冲突需要手动解决。

美中不足的是 git rerere 必须在发生冲突时启用 ,以便记录冲突,并且在添加和添加时仍然启用提交决议。显然你当时没有开启这个,不然你也不会问这个问题。 :-)

我有一个想法,我从未测试过,但在逻辑上应该可以解决这个问题。或者,我们可以使用 second 路径,我现在将介绍它。然后我会回到路径 1 描述这个想法,你可以测试一下。

路径 2:使用之前的结果,然后进行挑选

避免重新执行所有冲突解决方案的第二条途径是从提交 B 中获取树。假设你有 运行 git rebase -p 并且现在正处于合并冲突中,如果你 运行 git status 你会看到一些未合并的文件和一些成功合并的文件。您需要解决冲突并提交——所以让我们通过删除每个文件来立即解决所有冲突:

git rm -r -- .         # assumes you are in your top level dir

相当激烈,嗯? :-) 但是现在,在不提交的情况下,让我们提取 每个文件,完全按照提交时的格式 C:

git checkout <hash-of-C> -- .

请注意,我们还没有提交任何内容。我们现在已经完全按照 C 中的方式设置了索引和工作树。不过,我们缺少提交 XY 的效果,所以现在让我们使用 git cherry-pick -n:

添加那些,同样不提交
git cherry-pick -n <hash-of-X>
git cherry-pick -n <hash-of-Y>

我们现在在索引和工作树中有一个匹配 C 的树,除了它还具有 X 变化 和添加了 Y 变化 。我们可以 git diff HEAD 这个,确保它看起来不错,然后 git commit 结果。

路径 1:git rerere

git rerere的问题是在最初的合并过程中没有开启,所以Git没有记录冲突和解决方法.但是稍等一下:在上面,我们刚刚看到了一种方法,我们通过该方法重新执行 合并与之前 完全一样。我们所要做的就是重复原来的合并并提交它原来的结果!

因此,我们首先通过结束正在进行的、失败的 rebase 来结束正在进行的、失败的合并:

git rebase --abort

我们现在有一个干净的索引和工作树,甚至还没有开始变基,这很好,因为 git rebase -p 稍后会重复所有这些。我们还没有做任何需要仔细考虑的事情,我们只有 运行 几个简单的 Git 命令。

现在我们需要进入一个临时分支,其 tip 提交是合并提交 C 的两个父级之一,即提交 B 或提交 OLD。最简单的方法是:

git checkout feature^^

这让我们有一个分离的 HEAD 指向提交 C 的第一个父级(记住,feature 指向提交 D,所以 feature^ 指向提交 C,所以 feature^^ 指向 C 的第一个父级)。现在确保 rerere 已启用:

git config rerere.enabled true

并开始合并:

git merge feature^^2

因为 feature^ 是提交 C^2 是第二个父提交,这会尝试合并另一个提交(如果我们在 B 我们合并 OLD,如果我们在 OLD 上,我们合并 B)。此合并失败,但由于 rerere 启用, 次,Git 记录冲突。

然后,我们提取结果。我们可能甚至不需要 git rm -r .(只有在合并过程中有一些文件消失的情况下),但只是为了它,让我们使用一个管道命令来读取索引以进行合并 C 到我们的索引中并更新我们的工作树,我们也可以在路径 2 中使用它来代替两部分 git rmgit checkout:

git read-tree --reset -u feature^

和以前一样,feature^ 命名提交 C,所以我们刚刚提取了 C 的索引。 --reset 部分表示 "throw out our unmerged entries" 而 -u 部分表示 "update the work-tree to match".

我们现在已经准备好提交索引和工作树,所以让我们在匿名分支分离的 HEAD 上进行新的合并提交:

git commit -m 'dummy merge for rerere'

这记录了我们的决心,当然和我们之前的决心是一样的。 (实际上,我们甚至不需要 提交 ;我们也可以 运行 git rerere 然后 git merge --abort。但是提交似乎更简单,不知何故.)

现在我们丢弃这个合并结果——我们只是为了 git rerere 做所有这些——然后回到我们的分支:

git checkout feature

我们准备好重复保留-合并变基。这次,我们启用了 rerere,并记录了冲突及其解决方案,因此自动合并应该重新使用我们记录的解决方案:

git rebase --onto master --preserve-merges B

这一切应该都能正常工作。

最后的想法:git rererere

如果它像宣传的那样工作(我从未测试过),则六个命令序列:

git config rererere.enabled true
git checkout --detach <parent1>
git merge <parent2>
git read-tree --reset -u <merge>
git commit -m dummy
git checkout <where we were>

应该被打包为一个新的 git rererere 命令,其中新的 re 代表 repeat。 :-) 它只需要一个参数,它应该验证它是一个合并提交。理想情况下,它也应该与临时索引和工作树一起工作,这样它就可以 运行 而不必打乱任何正在进行的工作。作为一个打包的脚本,它可能应该只是 运行 git rerere 然后清除临时索引和工作树并合并状态。如果它使用 git worktree 机制来创建私有的 HEADMERGE_HEAD,那就更好了:它消除了最后恢复 HEAD 的需要,并且它不会弄乱(因此需要保留)任何现有的 MERGE_HEAD.

我结束了尝试通过至少使用 git 修复合并来复制补丁,使用常规补丁。

我先重做了合并,没有修复任何东西:

git checkout B
git checkout -b non-merge
git merge OLD
git add -u .
git commit -m 'Temporary commit to delete'

然后我从我的固定合并和这个无效合并创建了一个补丁

git diff non-merge..C > my_merge_modifs.patch

我想手动重做合并,所以我将功能分支重置为合并前的状态 C

git checkout feature
git reset --hard OLD

然后我与 master 的最新提交合并,B' :

git merge master

编辑补丁并确保用搜索替换正确的提交散列并从 B 替换为 B'。

我应用补丁,但没有使用 git,确保使用选项 -p1,以便 [=55= 的前导 a//b 文件夹] diff 被跳过:

patch -p1 < my_merge_modifs.patch
git add -u .
git commit -m "This is commit C'"

现在我可以将提交 D 变基到最后一次提交 C' :

git checkout D
git rebase C'

瞧瞧! master 指向 B' 并且 feature 指向 D' 这是在提交 C' 之上合并 B'OLD 而不是比 BOLD.

哦,我需要在没有解决任何冲突的情况下删除合并...:[=​​33=]

git branch -D non-merge

由于可用且优秀的答案提到了rerere,但说作者从未使用过它,所以让我详细说明一下:

git rerere 如果导致最终合并冲突的更改是在提交链中相当晚的时候引入的,那么这里将是极好的帮助。如果冲突早得多地发生,即使有帮助也无济于事。

rerere 缓存的工作原理与宣传的完全一样:它通过将版本 A、B 和已解决的文件 C 存储在其通常的内容哈希下来缓存各个冲突解决方案的解决方案。

如果您将一长串提交变基到最终导致稍后合并时发生冲突的内容,则会出现此问题。由于 "something" 实际上改变了文件,它也改变了它们的哈希值,因此 rerere 缓存不再知道新的哈希值。

所以。简而言之:rerere 很棒,尤其是当缓存在开发人员之间手动共享时,尤其是当您倾向于经常重放合并时。它工作得很好,并且很容易手动修改它(它由没有管理开销的普通文件组成)。但是,对于那些由于新的变基父级而更改的文件,变基将导致缓存未命中。