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

时间:2017-04-14 13:56:05

标签: git merge rebase git-rebase

我有一个类似于以下内容的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提交给主人。然而(通过gerrit),提交是重新设置两个新提交,X和Y,同时被推送,并且它提供了#34;重复"提交B'。

我尝试在提交B'上重新合并。用命令

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

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

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

感谢您的帮助!

3 个答案:

答案 0 :(得分:2)

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

如果您保证三个输入到原始合并 - 那些是BOLD和合并基础提交我们看不到显示日志的底部 - 与新合并的三个输入树相同 - 这些是B'OLD,可能是相同的合并基础,但我们无法确定 - 然后合并结果将是相同的。

然而,B' 的树似乎与B树不同,因为B'X和{ {1}}在其历史中也是如此。 YB'在其历史记录中都有B,因此如果AB的演变,则A可能是B'的演变{1}}这可能是Y的演变,也可能是X的演变,使得A具有B'X的变化在里面。因此,最终合并结果可能与原始合并结果相同。事实上,它可能或多或少地与C中的内容有关,但是通过应用X和Y中的演变而演变,就好像通过樱桃选择一样。" (最后一点是你可以在这里使用的一种策略的线索。)

冲突可能完全相同,并且合并每个冲突的结果可能与上次完全相同。如果是这样的话,Y会做你想要的。

这里有两条路径

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

最好的一个是git rerere。这个重新 - 使用重新有线重新解决方案,它内置于Git中,可以保存冲突 git rerere失败,然后在您解决冲突和git merge已解决的版本以及git add结果时,它也会保存其解决方案。 (这两个步骤实际上是在合并失败后和最终git commit期间发生的。)然后,在随后的git commit上 - 例如git merge调用的新git merge - 有冲突,Git会查看这些冲突是否已保存,记录了解决方案。如果是这样,Git会对这些冲突应用这些解决方案,只留下新的冲突。

此特定软件中的问题是,当发生冲突时必须启用git rebase --preserve ,以便记录冲突,并在添加和提交分辨率时仍然启用。显然你当时没有打开这个,或者你不会问这个问题。 : - )

我有一个想法,我从未测试过,但在逻辑上应该可以解决这个问题。或者,我们可以使用第二个路径,我现在将对此进行描述。然后我将回到路径1并描述您可以测试的想法。

路径2:使用之前的结果,然后采摘樱桃

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

git status

相当激烈,嗯? :-)但是现在,在没有提交的情况下,让提取每个文件的格式与它在提交git rm -r -- . # assumes you are in your top level dir 中的形式完全相同:

C

请注意,我们仍未提交任何内容。我们现在已经完全按照git checkout <hash-of-C> -- . 中的方式设置索引和工作树。我们已经错过了提交CX的影响,所以现在让我们使用Y来添加这些内容,也无需提交:

git cherry-pick -n

我们现在在索引和工作树中有一个匹配git cherry-pick -n <hash-of-X> git cherry-pick -n <hash-of-Y> 的树,除了它还有来自C更改并且更改来自X添加到其中。我们Y可以确保它看起来不错,然后git diff HEAD结果。

路径1:git commit

git rerere的问题是在原始合并期间没有启用,因此Git没有记录冲突及其解决方案。但请稍等一下:上面,我们刚刚看到了一种方法,我们重新完成完全一样的。我们所要做的就是重复原始合并并提交其原始结果!

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

git rerere

我们现在有一个干净的索引和工作树,甚至还没有启动rebase,这很好,因为git rebase --abort 将在以后重复所有这些。我们还没有做过任何需要仔细考虑的事情,我们只运行了一些简单的Git命令。

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

OLD

它为我们提供了指向提交git checkout feature^^ 的第一个父级的分离HEAD(请记住,C指向提交feature,因此D指向提交{{1}所以feature^指向C的第一个父母)。现在确保已启用feature^^

C

并开始合并:

rerere

由于git config rerere.enabled true 是提交git merge feature^^2 feature^是第二个父提交,因此会尝试合并其他提交(如果我们合并C我们合并{ {1}},如果我们在^2,我们合并B)。此合并失败,但由于启用了OLD时间,Git会记录冲突。

然后,我们提取结果。我们可能甚至不需要OLD(仅当合并期间有一些文件消失时才会这样),但仅仅为了它,我们使用一个管道命令将合并B的索引读入我们的索引并更新我们的工作树,我们也可以在路径2中使用它来代替两部分rereregit rm -r .:< / p>

C

和以前一样,git rm名称提交C,所以我们刚刚提取了git checkout的索引。 git read-tree --reset -u feature^ 部分意味着&#34;抛弃我们未合并的条目&#34;并且feature^部分意味着&#34;更新工作树以匹配&#34;。

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

C

这记录了我们的决议,当然这些决议与我们之前提出的决议相同。 (实际上,我们甚至不需要提交;我们也可以运行--reset然后运行-u。但是提交似乎更简单,不知何故。)

现在我们抛弃了这个合并结果 - 我们只为git commit -m 'dummy merge for rerere' 做了所有这些 - 然后回到我们的分支上:

git rerere

我们准备重复保留合并rebase。这一次,我们启用了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机制来制作私有regit rerere,那就更好了:它最终无需恢复git worktree,而且不会惹恼(因此需要保留)任何现有的HEAD

答案 1 :(得分:1)

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

如果导致最终合并冲突的更改在提交链中引入相当晚,那么

git rerere将是非常好的帮助。如果冲突发生的时间要早​​得多,那就完全没有用了。

rerere缓存的工作原理与广告完全相同:它通过在其通常的内容哈希下存储版本A,B和已解析的文件C来缓存各个冲突解决方案的解析。

如果您将一长串提交重新绑定到最终在合并时最终导致冲突的问题,则会出现问题。那就是&#34;某事&#34;实际上更改了文件,它也改变了它们的哈希值,因此rerere缓存不再知道新的哈希值了。

因此。简而言之:rerere非常棒,特别是如果在开发人员之间手动共享缓存,特别是如果您倾向于经常重放合并。它工作得很完美,并且可以手动地用它进行黑客攻击(它由没有管理开销的普通文件组成)。但是,Rebase会导致缓存未命中,因为新的rebase父级会更改这些文件。

答案 2 :(得分:0)

我最后尝试使用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,以便跳过git diff的前导a//b文件夹:

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

哦,我需要删除合并而不解决任何冲突......:

git branch -D non-merge