与普通的 git rebase 一样,git with--preserve-merges
首先识别在提交图的一部分中进行的提交列表,然后在另一部分的顶部重播这些提交。--preserve-merges
选择哪些提交进行重播以及重播如何用于合并提交的差异。
为了更明确地了解正常和保留合并的 rebase 之间的主要区别:
- 保留合并的 rebase 愿意重播(一些)合并提交,而普通的 rebase 完全忽略合并提交。
- 因为它愿意重放合并提交,保留合并的 rebase 必须定义重放合并提交的含义,并处理一些额外的皱纹
- 从概念上讲,最有趣的部分可能是选择新提交的合并父项应该是什么。
- 重放合并提交还需要显式检查特定的提交 (
git checkout <desired first parent>
),而普通的 rebase 不必担心这一点。
- 保留合并的 rebase 考虑了一组较浅的重放提交:
- 特别是,它只会考虑重放自最近的合并基础以来所做的提交——即最近一次两个分支分歧的时间——而正常的 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.
Rebase--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 的合并提交。
假设我们使用正常的、非保留合并的 rebase 在 master (C) 之上 rebase 主题 (H)。(例如,结帐主题;rebase master。)在这种情况下,git 会选择以下提交进行重播:
然后像这样更新提交图:
B---C <-- master
/ \
A D'---E'---F'---G'---H' <-- topic
(D' 是 D 的重放等价物。)
请注意,未选择合并提交 m 进行重播。
如果我们改为在--preserve-merges
C 之上对 H 进行 rebase(例如,checkout topic; rebase --preserve-merges master。)在这种新情况下,git 将选择以下提交进行重播:
- 选D
- 选E
- 选择 F(在“子主题”分支中选择 D')
- 选择 G(到“子主题”分支中的 F')
- 选择将分支“子主题”合并到主题中
- 选H
现在选择 m进行重播。另请注意,在合并提交 m 之前选择了合并父级 E 和 G 进行包含。
这是生成的提交图:
B---C <-- master
/ \
A D'-----E'----m'----H' <-- topic
\ /
F'-------G'
同样,D' 是 D 的精选(即重新创建)版本。对于 E' 等也是如此。每个不在 master 上的提交都已重播。E 和 G(m 的合并父级)都被重新创建为 E' 和 G' 以充当 m' 的父级(在 rebase 之后,树历史仍然保持不变)。
示例 2
与普通 rebase 不同,保留合并的 rebase 可以创建上游头部的多个子级。
例如,考虑:
B---C <-- master
/
A-------D------E---m----H <-- topic
\ |
------- F-----G--/
如果我们在 C(master)之上 rebase H(topic),那么为 rebase 选择的提交是:
结果是这样的:
B---C <-- master
/ | \
A | D'----E'---m'----H' <-- topic
\ |
F'----G'---/
示例 3
在上面的示例中,合并提交及其两个父级都是重放提交,而不是原始合并提交具有的原始父级。但是,在其他变基中,重放的合并提交可能会以合并之前已经在提交图中的父级结束。
例如,考虑:
B--C---D <-- master
/ \
A---E--m------F <-- topic
如果我们将主题 rebase 到 master(保留合并),那么重播的提交将是
重写的提交图将如下所示:
B--C--D <-- master
/ \
A-----E---m'--F'; <-- topic
这里重放的合并提交 m' 获取提交图中预先存在的父级,即 D(master 的 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 的子级合并在一起。)因此,从概念上讲,在D 可能应该是无操作或创建一个空提交(即连续修订之间的差异为空的提交)。
然而,git 可能会拒绝在 D 之上重放 m1 的尝试。您可能会收到如下错误:
error: Commit 90caf85 is a merge but no -m option was given.
fatal: cherry-pick failed
看起来好像忘记向 git 传递一个标志,但潜在的问题是 git 不喜欢创建空提交。