git的“rebase –preserve-merges”究竟干什么(为什么?)

Git的rebase命令的文档非常简短:

 --preserve-merges Instead of ignoring merges, try to recreate them. This uses the --interactive machinery internally, but combining it with the --interactive option explicitly is generally not a good idea unless you know what you are doing (see BUGS below). 

那么当你使用--preserve-merges时会发生什么呢? 它与默认行为有什么不同(没有这个标志)? 什么意思是“重新创build”合并等

和普通的git rebase一样,具有--preserve-merges git首先标识在提交图的一部分中提交的提交列表,然后在另一部分的顶部重放这些提交。 与--preserve-merges的不同之处在于select哪个提交用于重放,以及重放提交的工作方式。

为了更加明确正常和保留合并的rebase之间的主要区别:

  • 合并保留的rebase愿意重播(一些)合并提交,而正常的rebase则完全忽略合并提交。
  • 因为它愿意重播合并提交,合并保留重定义必须定义重播合并提交的意义,并处理一些额外的皱纹
    • 从概念上讲,最有意思的部分或许就在于select新的合并父母应该是什么。
    • 重放合并提交也需要明确地检出特定的提交( git checkout <desired first parent> ),而正常的rebase不必担心这一点。
  • 保留合并的rebase考虑重新提交一组较小的提交:
    • 特别是,它只考虑重播自最近合并基础(即两个分支最近一次分叉)以来所做的提交,而正常的重新分配可能会重新提交两个分支首次分离的提交。
    • 暂时性的和不清楚的,我认为这最终是一个筛选出已经“合并到”合并承诺中的“旧承诺”的重放手段。

首先,我将尝试描述“足够确切”,即“ --preserve-merges ”,然后会有一些例子。 当然,从这些例子开始,如果这看起来更有用。

“Brief”中的algorithm

如果你想真正进入杂草,请下载git源文件并浏览git-rebase--interactive.sh文件。 (Rebase不是Git的C核心的一部分,而是用bash写的,而且在后台,它和“interactive rebase”共享代码)

但在这里,我将画出我认为是其本质的东西。 为了减less一些事情的思考,我采取了一些自由。 (例如,我不试图100%准确地捕捉计算发生的精确顺序,忽略一些不那么重要的主题,例如,如何处理已经在分支之间挑选的提交)。

首先,请注意,不保留合并的rebase是相当简单的。 这或多或less是:

 Find all commits on B but not on A ("git log A..B") Reset B to A ("git reset --hard A") Replay all those commits onto B one at a time in order. 

Rebase – --preserve-merges比较复杂。 这样做很简单,我已经能够做到这一点,而不会丢失那些看起来相当重要的东西:

 Find the commits to replay: First find the merge-base(s) of A and B (ie the most recent common ancestor(s)) This (these) merge base(s) will serve as a root/boundary for the rebase. In particular, we'll take its (their) descendants and replay them on top of new parents Now we can define C, the set of commits to replay. In particular, it's those commits: 1) reachable from B but not A (as in a normal rebase), and ALSO 2) descendants of the merge base(s) If we ignore cherry-picks and other cleverness preserve-merges does, it's more or less: git log A..B --not $(git merge-base --all AB) Replay the commits: Create a branch B_new, on which to replay our commits. Switch to B_new (ie "git checkout B_new") Proceeding parents-before-children (--topo-order), replay each commit c in C on top of B_new: If it's a non-merge commit, cherry-pick as usual (ie "git cherry-pick c") Otherwise it's a merge commit, and we'll construct an "equivalent" merge commit c': To create a merge commit, its parents must exist and we must know what they are. So first, figure out which parents to use for c', by reference to the parents of c: For each parent p_i in parents_of(c): If p_i is one of the merge bases mentioned above: # p_i is one of the "boundary commits" that we no longer want to use as parents For the new commit's ith parent (p_i'), use the HEAD of B_new. Else if p_i is one of the commits being rewritten (ie if p_i is in R): # Note: Because we're moving parents-before-children, a rewritten version # of p_i must already exist. So reuse it: For the new commit's ith parent (p_i'), use the rewritten version of p_i. Otherwise: # p_i is one of the commits that's *not* slated for rewrite. So don't rewrite it For the new commit's ith parent (p_i'), use p_i, ie the old commit's ith parent. Second, actually create the new commit c': Go to p_1'. (ie "git checkout p_1'", p_1' being the "first parent" we want for our new commit) Merge in the other parent(s): For a typical two-parent merge, it's just "git merge p_2'". For an octopus merge, it's "git merge p_2' p_3' p_4' ...". Switch (ie "git reset") B_new to the current commit (ie HEAD), if it's not already there Change the label B to apply to this new branch, rather than the old one. (ie "git reset --hard B") 

--onto C参数的重新configuration应该非常相似。 而不是在B的HEAD处开始落实播放,而是开始在C的HEAD处提交播放。 (并使用C_new而不是B_new。)

例1

例如,采取提交图

  B---C <-- master / A-------D------E----m----H <-- topic \ / F-------G 

m是父母E和G的合并提交。

假设我们使用一个正常的,不合并保留的rebase在主(C)之上重新定义了主题(H)。 (例如, checkout topic; rebase master 。)在这种情况下,git会select以下提交进行重播:

  • 选D.
  • 挑E
  • 选F
  • 选G
  • 选H

然后像这样更新提交图:

  B---C <-- master / \ A D'---E'---F'---G'---H' <-- topic 

(D'是D的重播等价物。)

请注意,合并提交m未被选中用于重播。

如果我们在C之上做了一个“ --preserve-merges H的--preserve-merges (例如, checkout topic; rebase –preserve-merges master) 。在这种新的情况下,git会select以下提交进行重放:

  • 选D.
  • 挑E
  • 选F(到'subtopic'分支的D'上)
  • 选G(到'subtopic'分支的F'上)
  • select合并分支'subtopic'为主题
  • 选H

现在m 选中重播。 还要注意合并父母E和G在合并提交之前被选中包含在内。

这是由此产生的提交图:

  B---C <-- master / \ A D'-----E'----m'----H' <-- topic \ / F'-------G' 

再次,D'是一个樱桃采摘(即重新创build)的版本D.同样的E'等。每个提交不在主人已经重播。 E和G(m的合并父母)被重新创build为E'和G'作为m'的父母(在重build之后,树的历史依然保持不变)。

例2

不同于正常的重新组合,合并保留重新组合可以创build上游头部的多个孩子。

例如,考虑:

  B---C <-- master / A-------D------E---m----H <-- topic \ | ------- F-----G--/ 

如果我们在C(master)之上重新设置H(主题),那么为rebaseselect的提交是:

  • 选D.
  • 挑E
  • 选F
  • 选G
  • selectm
  • 选H

结果是这样的:

  B---C <-- master / | \ A | D'----E'---m'----H' <-- topic \ | F'----G'---/ 

例3

在上面的例子中,合并提交和它的两个父母都是重放的提交,而不是原来的合并提交拥有的原始父母。 但是,在其他的rebase中,重播的合并提交最终可能会在合并之前已经在提交图中的父代。

例如,考虑:

  B--C---D <-- master / \ A---E--m------F <-- topic 

如果我们将主题redirect到master(保留合并),那么提交重放将会是

  • select合并提交m
  • 选F

重写的提交图如下所示:

  B--C--D <-- master / \ A-----E---m'--F'; <-- topic 

这里重播合并提交m'得到提交图中预先存在的父母,即D(master的HEAD)和E(原合并提交m的父代之一)。

例4

在某些“空白提交”的情况下,合并保留的rebase会变得混乱。 至less这只是一些老版本的git(例如1.7.8)。

拿这个提交图:

  A--------B-----C-----m2---D <-- master \ \ / E--- F--\--G----/ \ \ ---m1--H <--topic 

请注意,提交m1和m2应该包含来自B和F的所有更改。

如果我们试图做H(主题)到D(主)的保留git rebase --preserve-merges ,那么下面的提交被select重播:

  • 选m1
  • 选H

请注意,m1中的变化(B,F)应该已经被纳入到D.(这些变化应该已经被合并到m2中,因为m2将B和F的孩子合并在一起)。因此,从概念上讲,重放m1 D应该可能是一个没有操作或创build一个空的提交(即连续修订之间的差异是空的)。

不过,git可能会拒绝在m之上重放m1的尝试。你可能会得到这样的错误:

 error: Commit 90caf85 is a merge but no -m option was given. fatal: cherry-pick failed 

它看起来像忘了通过一个标志git,但潜在的问题是,git不喜欢创build空提交。