Git - 在更深的阶段重新合并

9

合并与变基的主要缺点在于合并会导致树形结构混乱。如果主分支经常更新,那么每次有重要更新时合并主分支将会创建大量不必要的提交记录。实际上,大多数情况下并不需要这样做。考虑以下仓库:

Master A\--B--C\--D\---------E
Branch   l--m---n---o--p--q

这里的n是一个合并操作,我们不想再次解决大量冲突。我们希望将E合并回来,而不创建新的合并提交。

因此,我们返回到o,合并E,并在其上挑选p和q:

Master A\--B--C\--D\--------E\
Branch   l--m---n\--o--p--q   \
Tmp               -------------o'-p-q

如果这个没有错误,我们就可以删除旧的分支。
如果我是第一个想到这种工作流程的人,那我会很惊讶。有没有可以自动化这个过程的git扩展程序?
我已经开始编写一个脚本来完成这个任务,但主要问题是选择o。": / ^ Merge" - 在大多数情况下将选择o,但如果不依赖于不以Merge开头的提交消息,那么一定会有更好的方法。

@ErixJiang:抱歉,我的图表有误。我已经修改了图表,以便避免再次处理合并n的情况。 - Casebash
@PaulHicks:是的,但当你变基时,它会让你再次解决它们。 - Casebash
注意:git rerere(例如在https://dev59.com/4Wkw5IYBdhLWcg3wUI_x#10018939中)可能有助于记录合并决策并避免在下一次rebase中出现相同的冲突。 - VonC
我仍然不清楚与rebase相关的问题。我建议你只需进行rebase操作。这样可以避免重新出现旧冲突的整个问题,并保持你的历史记录干净整洁。你总是可以进行rebase,然后在主题分支中进行非快进式合并,以保留合并的事实。 - eddiemoya
在合并 E 到 p q 之后或之前,很难找到一种情况会改变具有冲突的情况。 - Learath2
显示剩余5条评论
3个回答

7
编辑:要回溯启用rerere,请参见此处。本答案中的脚本不会启用rerere,而且在E的历史(如下所示)中对n...D冲突的块进行了更改,则会与rerere的行为有所不同,以解决o中发生的冲突。 rerere执行n...E合并,并应用仍然有效的任何n...D分辨率,而此脚本执行标准的o...E合并(但将父节点o替换为o

复制图表供参考:

      before                                 desired after
Master A\--B--C\--D\....E             \  Master A\--B--C\-D...E\
Branch   l--m---n---o--p--q         /    Branch   l--m---n------o'--p'--q'

因此,rerere将绕过E-reverted ND冲突,而脚本将把reversion视为进一步的更改。这两个脚本都生成上面所示的“desired after”图形,但它们通过不同的路径到达那里。

一个小时的尝试并没有产生比仅仅观察图表和推导出更多易于理解的文本描述。

我不清楚rerere或脚本的行为哪一个总是更好。


当我执行

返回o,合并E

使用:

git checkout o
git merge E

我理解

Master A\--B--C\--D\---------E
Branch   l--m---n---o\-p--q   \
HEAD                  `--------o'

使用 o 作为 o' 的父节点。你需要保留 o' 的内容(以保存你已经在 o 中应用的任何冲突解决方���),但是你希望它的父节点是 nE,而不是 oE。这实际上相当容易。
因此,完整的程序如下:
#!/bin/sh

# 1. identify the most recent merge on $1
# 2. verify that it's from $2
# 3. update the merge to $2's current state, 
# 4. preserving any conflict resolutions that were already done
# 5. and cherry-pick everything since.

# most recent merge in $1
update=`git rev-list -1 --min-parents=2 $1`

# verify most recent merge is from correct merge base
test "`git merge-base $1 $2`" = "`git rev-parse $update^2`" \
|| { echo "most recent merge isn't of $1 from $2"; exit 1; }

set -e  # exit immediately on error
git checkout $update
git merge $2 
git checkout $(
      # this makes a new commit with HEAD's commit message and tree
      # but with $update^ and $2 as parents
      git cat-file -p HEAD \
      | sed 1,/^$/d \
      | git commit-tree -p $update^ -p $2 HEAD^{tree}
   )
test $update = `git rev-parse $1` || git cherry-pick $update..$1
git checkout -B $1

如果你愿意失去在 o 中完成的任何冲突解决,你可以改为执行以下操作(从 set -e 开始)

set -e
git checkout $update^
git merge $2
test $update = `git rev-parse $1` || git cherry-pick $update..$1
git checkout -B $1

如果在子shell中执行工作,并且从该shell退出以继续工作不会有任何问题,那么您可以通过执行以下操作使其更加稳定:

git merge $2 \
|| { echo "Resolve conflicts and \`exit\`, or \`exit 1\` to not continue"; $SHELL; }

编辑:更新以处理在没有需要cherry-pick的情况下更新合并。


不错的脚本。+1 使用“合并”和“commit-tree”方法可能比变基更容易。 - VonC

3

我之前不知道rebase命令中的“-onto”选项,我得试一下。我也要记住使用imerge命令。 - Casebash

1
您一直遇到同样的冲突,是因为您应该将分支重新基于主干进行变基,而不是将主干的内容合并到分支中(这是错误的合并方式)。
如果点“n”是一个变基,那么它就完成了。l-m-n将被重放在C之上,因此分支将从C开始。在C之前的更改就不必再进行变基了。
当您准备将分支合并到主干时,您需要再次变基,以便分支从主干的HEAD版本开始。此时,分支可以替换主干:您可以检出主干并重置为分支。

反复重新定位一个越来越长的提交序列可能会变得很慢,如果该分支不打算合并回去(比如私有分支,或者交换标签,这是从暂存分支合并),那么重新定位能给你带来什么?看看Git主线的成功示例。 - jthill
@jthill,rebase 操作可以让你的分支得到来自主线的新提交,使得你的分支起点后移,并且之前的提交就不必再考虑了。然而,这也将会是你的分支上的一个非快进合并操作。 - Kaz

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