git pull --rebase --preserve-merges 重写未修改父项的合并提交

git pull --rebase --preserve-merges rewrites merge commit with unmodified parents

回顾历史

   D  origin/master
  /
 | C  master
 | |\
 | | B
  \|/|
   A :
   |
   :

git pull --rebase --preserve-merges 之后,我希望历史是

   C'  master
   |\
   D |
   | B
   |/|
   A :
   |
   :

但结果是

   C'  master
   |\
   | B'
   |/|
   D :
   |
   A
   |
   :

换句话说,B 提交被重写为 B',即使不需要它,因为两个父项都未修改。我知道我正在变基到 D,但是这是有问题的,因为必须再次解决 B 合并中​​的所有冲突。

当前功能有什么好处吗?

有没有办法获得我想要的功能?

我无法真正回答 "benefit" 问题,但我可以描述为什么 rebase 会按原样运行,以及如何获得您想要的结果。

无论交互与否,Rebase 都采用 "three branch-or-commit-like" 个参数,它调用 newbaseupstream, 和 branch:

git rebase [-i | --interactive] [options] [--exec <em>cmd</em>] [--onto <em>newbase</em>]
                      [upstream [branch]]

如果您省略其中的部分或全部,rebase 仍会使用它们,它只是自行找到它们:

  • branch默认为当前分支,或者HEAD(包括分离的头)。
  • upstream 默认为当前分支的上游,由 git branch --set-upstream-to 或类似设置。无论您在此处指定什么,或默认值,都会提供给 git rev-list 以使其 stop 接受提交,即,将重新定位的提交是由 [=117= 打印的提交]git rev-list upstream..HEAD.
  • newbase 默认为与 upstream.
  • 相同的提交 ID

(如果您在 git 中提供 --fork-point 足以拥有 --fork-point,这些都会有所修改,但这里的总体思路仍然适用:要重新设置基数的提交是默认情况下,根据 "after" 上游的那些选择,直到并包括 HEAD。)

当你让 git pull 运行 git rebase 为你提供时,它提供,作为 upstream,当前分支的实际上游(同样,由 fork 修改-point 计算在较新的 gits 中,但是如果您从中获取的实际上游没有完成自己的变基,这应该没有影响)。 (--onto 通常也是相同的上游。在这种情况下,它等于 git rebase [options] --onto origin/master origin/master master。)

如您所见(通过运行宁git rev-list origin/master..master),这意味着"please rebase commits B and C onto D"。添加 --preserve-merges 仅使用交互式机制并按要求保留合并,在按要求重新定位两个提交之后。

如果你只是 运行 git fetch,你将像往常一样获得提交 D 并且将你绘制的图形作为第一个:

   D  origin/master
  /
 | C  master
 | |\
 | | B
  \|/|
   A :
   |
   :

您现在可以 尝试 到 运行 git rebase -p 手动变基提交 C,指定 B 作为其 "upstream" 以便 rebase 不会复制提交 B,并将 D 指定为 --onto 提交,以便 C 重新基于 D:

$ git rebase -p --onto origin/master master^2 master

不幸的是,这会产生一个新的合并提交,其两个父项是 origin/master(提交 D)和原始的 master^(提交 A)。 (虽然这显然与内部交互保留模式的工作方式有关,但原因并不明显。)

因此,本例中的技巧是进行您自己的合并:

$ git branch -m master old-master
$ git checkout master

(这使得一个新的 master 指向提交 D),然后:

$ git merge old-master^2

(这会重新合并未修改的 B)。

不过,您还提到:

... it is problematic because all conflicts in the B merge have to be resolved again.

不幸的是,您无法真正避免这种情况,因为 D 可能与 A 有很大不同,因此实际的合并冲突解决方案应该 不同。

如果您确定它们(分辨率)不应该(不同),您也可以通过从旧提交 C 中获取合并结果来回避这个问题。例如,假设冲突发生在 dir1/file1dir2/file2 中,而 AD 之间的差异全部在 README.txt 或类似的地方。然后,当您执行上述合并时,您可以 "resolve" 非常简单地解决冲突:

$ git checkout old-master -- dir1/file1 dir2/file2

提取这些文件的提交 C 版本,更新您的索引和工作树以恢复您以前的合并解决方案。您的冲突现已解决(与以前完全一样),您可以 git commit 合并结果。