与普通的git rebase一样,git with --preserve-merges
首先标识在提交图的一部分中进行的提交的列表,然后在另一部分的顶部重播这些提交。--preserve-merges
选择要重播的提交与合并提交的重播如何工作有关的差异。
要更明确地了解普通和保留合并基准之间的主要区别:
- 保留合并的rebase愿意重播(某些)合并提交,而普通的rebase则完全忽略合并提交。
- 因为它愿意重播合并提交,所以保留合并的rebase必须定义它的含义重播合并提交的,并处理一些额外的问题
- 从概念上讲,最有趣的部分可能是在选择新提交的合并父母应该是什么。
- 重播合并提交还需要显式签出特定的提交(
git checkout <desired first parent>
),而正常的重新设置不必为此担心。
- 保留合并的rebase考虑要重播的较浅的提交集:
- 特别是,它将仅考虑重播自最近合并基础以来的重做提交(即,两个分支最近的时间),而正常的重定基础可能重播提交,可以追溯到两个分支的第一次背离。
- 暂时不明确的是,我相信这最终是一种筛选出重放已“合并到”合并提交中的“旧提交”的方法。
首先,我将尝试“足够准确地”描述rebase的--preserve-merges
功能,然后将提供一些示例。如果看起来更有用,那么当然可以从示例开始。
“简要”中的算法
如果您真的想深入研究杂草,请下载git源并浏览文件git-rebase--interactive.sh
。(Rebase不是Git C核心的一部分,而是用bash编写的。而且,在后台,它与“交互式rebase”共享代码。)
但是在这里,我将概述我认为其实质的内容。为了减少需要考虑的事情,我采取了一些自由措施。(例如,我不会尝试以100%的准确度捕获进行计算的精确顺序,而忽略一些不太集中的话题,例如,如何处理分支之间已经挑剔的提交)。
首先,请注意,非保留的变基相当简单。或多或少:
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.
底垫--preserve-merges
较为复杂。这就像我能够做到的一样简单,而且不会丢失看起来很重要的东西:
Find the commits to replay:
First find the merge-base(s) of A and B (i.e. 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 A B)
Replay the commits:
Create a branch B_new, on which to replay our commits.
Switch to B_new (i.e. "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 (i.e. "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 (i.e. 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, i.e. the old commit's ith parent.
Second, actually create the new commit c':
Go to p_1'. (i.e. "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 (i.e. "git reset") B_new to the current commit (i.e. HEAD), if it's not already there
Change the label B to apply to this new branch, rather than the old one. (i.e. "git reset --hard B")
用--onto C
参数重新设置应该非常相似。与其在B的HEAD上开始提交回放,不如在C的HEAD上开始提交回放。(并使用C_new而不是B_new。)
例子1
例如,以提交图
B---C <-- master
/
A-------D------E----m----H <-- topic
\ /
F-------G
m是与父母E和G的合并提交。
假设我们使用正常的不保留合并的基准在主数据库(C)上重新建立主题(H)。(例如,checkout主题; rebase master。)在这种情况下,git将选择以下提交进行重播:
然后像这样更新提交图:
B---C <-- master
/ \
A D'---E'---F'---G'---H' <-- topic
(D'是D的重播等,等等。)
请注意,未选择合并提交m进行重播。
如果我们改为在--preserve-merges
C的基础上对H进行重新基准(例如,checkout主题; rebase --preserve-merges master。)在这种新情况下,git将选择以下提交进行重播:
- 选择D
- 选择E
- 选择F(在“ subtopic”分支中的D上)
- 选择G(在“子主题”分支中的F上)
- 选择合并分支“子主题”为主题
- 选择H
现在我是选中进行重放。另请注意,在合并提交m之前已选择合并父E和G进行包含。
这是生成的提交图:
B---C <-- master
/ \
A D'-----E'----m'----H' <-- topic
\ /
F'-------G'
同样,D'是D的精选版本(即重新创建的版本)。E'相同,依此类推。每次重播不在master上的提交。E和G(m的合并父级)都已重新创建为E'和G',以用作m'的父级(重新设置基数后,树的历史仍保持不变)。
例子2
与普通的rebase不同,保留合并的rebase可以创建上游头的多个子代。
例如,考虑:
B---C <-- master
/
A-------D------E---m----H <-- topic
\ |
------- F-----G--/
如果我们将H(主题)基于C(主)作为基础,那么选择用于基础的提交是:
结果是这样的:
B---C <-- master
/ | \
A | D'----E'---m'----H' <-- topic
\ |
F'----G'---/
例子3
在以上示例中,合并提交及其两个父级都是重播的提交,而不是原始合并提交所具有的原始父级。但是,在其他基准库中,重播的合并提交可能以合并之前已经存在于提交图中的父级结束。
例如,考虑:
B--C---D <-- master
/ \
A---E--m------F <-- topic
如果我们将主题重新设置为主主题(保留合并),则重播的提交将为
重写的提交图将如下所示:
B--C--D <-- master
/ \
A-----E---m'--F'; <-- topic
在这里重播的合并提交m'获得了在提交图中预先存在的父级,即D(主控的HEAD)和E(原始合并提交m的父级之一)。
例子4
在某些“空提交”情况下,保留合并的rebase可能会造成混乱。至少只有某些旧版本的git(例如1.7.8)是正确的。
采取以下提交图:
A--------B-----C-----m2---D <-- master
\ \ /
E--- F--\--G----/
\ \
---m1--H <--topic
请注意,提交m1和m2都应合并了B和F的所有更改。
如果我们尝试将git rebase --preserve-merges
H(主题)执行到D(母版),则选择以下提交进行重播:
请注意,在m1中合并的更改(B,F)应该已经合并到D中。(这些更改应该已经合并到m2中,因为m2将B和F的子级合并在一起。)因此,从概念上讲,在m1的顶部重播m1 D可能应该是空操作或创建一个空的提交(即,连续修订之间的差异为空的提交)。
但是,相反,git可能会拒绝尝试在D顶部重播m1的尝试。您可能会得到如下错误:
error: Commit 90caf85 is a merge but no -m option was given.
fatal: cherry-pick failed
看起来好像忘记了将标志传递给git,但是潜在的问题是git不喜欢创建空的提交。
git --rebase-merges
最终将取代旧版本git --preserve-merges
。请参阅下面的答案