git的“rebase --preserve-merges”到底是什么?做(为什么?) [英] What exactly does git's "rebase --preserve-merges" do (and why?)
问题描述
Git的 rebase
命令的文档相当简要说明:
- preserve-merges
而不是忽略合并,尝试重新创建它们。
这在内部使用--interactive机制,但明确地将
与--interactive选项结合使用通常不是一个好主意
,除非您知道自己在做什么(请参阅BUGS)。
那么当你使用时 - 保留 - 合并
?它与默认行为(没有该标志)有什么不同?这意味着什么重新创建合并等。
与正常的git rebase一样,git与 - preserve-merges
首先标识在提交图的一部分中提交的提交列表,然后在另一部分的顶部重新提交这些提交。与 - preserve-merges
的不同之处在于选择了哪些提交用于重播,以及重播如何用于合并提交。
为了更加明确正常和保留合并的rebase之间的主要区别:
- 合并保留的rebase愿意重放(某些)合并提交,而正常的rebase完全忽略了合并提交。
- 因为它愿意重播合并提交,所以merge-preserving rebase必须定义重放它的意义来重放合并提交和处理一些额外的皱纹
- 从概念上讲,最有趣的部分可能在于选择新提交的合并父项应该是什么。 重新提交合并提交也需要明确地检出特定的提交(
git checkout<期望的第一个父元素>
),而正常的rebase doesn'
- 从概念上讲,最有趣的部分可能在于选择新提交的合并父项应该是什么。 重新提交合并提交也需要明确地检出特定的提交(
- 合并保留的rebase考虑一组较重的提交提交:
- 特别是,它只会考虑重放自最近合并基础以来所做的提交 - 即两个分支最近一次分支的时间 - ,而正常的rebase可能会重新提交提交到第一个第一个第二个分支分支的时间。
- 我认为这是临时的和不清楚的,筛选出重放已经合并到合并提交中的旧提交。
首先,我将尝试描述足够精确,并且会有一些示例。我们当然可以从这些例子开始,如果这看起来更有用的话。
Brief中的算法
如果您想真正进入杂草,请下载git源文件并浏览文件
git-rebase--interactive.sh
。 (Rebase不是Git的C核的一部分,而是用bash编写的,而且在幕后,它与交互式rebase共享代码。)但是这里我会画出我认为是其本质的东西。为了减少需要思考的事情,我采取了一些自由。 (例如,我不会试图以100%的精度捕捉计算发生的精确顺序,并忽略一些不那么重要的主题,例如,如何处理已在分支机构间进行挑选的提交) p>
首先,请注意,非合并保留的rebase非常简单。它或多或少:
在B上查找所有提交,但不在A上(git log A..B)
将B重置为A(git reset --hard A)
将所有这些提交按顺序重播到B。
Rebase
- preserve-merges
是比较复杂。以下是我所能做到的简单而不会丢失看起来相当重要的事情:找到提交重播:
首先找到A和B的合并基(即最近的共同祖先)
这个(这些)合并基将作为变基。
特别的,我们将把它们的子集放在新父母的头顶
现在我们可以定义C,这是提交重放的集合。尤其是,这些提交:
1)可从B到达但不是A(如在正常的rebase中)和ALSO
2)合并基础的后代
如果我们忽略樱桃挑选和其他聪明保存合并做的,它或多或少:
git log A..B - 不$(git merge-base --all AB)
重播提交:
创建一个分支B_new,在其上重放我们的提交。
切换到B_new(例如,git checkout B_new)
继续在子节点之前的父母(--topo-order),在C_B_new之上重播每个提交c:
如果它是一个非合并提交,像往常一样挑选樱桃(即git cherry-pick c)
否则它是一个合并提交,我们将构建一个等效合并提交c':
To创建一个合并提交,其父母必须存在,我们必须知道他们是什么。
首先,通过引用c的父母,找出哪些父母用于c':
对于parents_of(c)中的每个父亲p_i:
如果p_i是合并中的一个上面提到的基数:
#p_i是我们不再想用作父母的边界提交之一
对于新提交的第i个父节点(p_i'),使用B_new的HEAD。
否则,如果p_i是被重写的提交之一(即,如果p_i在R中):
#注意:因为我们正在移动父母之前的孩子,重写版本
# p_i必须已经存在。因此重用它:
对于新提交的第i个父节点(p_i'),使用p_i的重写版本。
否则:
#p_i是*不可重写的提交之一。所以不要重写它
对于新提交的第i个父节点(p_i'),使用p_i,即旧提交的第i个父节点。
其次,实际创建新提交c':
转到p_1'。 (即git checkout p_1',p_1'是我们为新提交所需的第一个父级)
在其他父级合并:
对于典型的双亲合并,它是只是git merge p_2'。
对于章鱼合并,它是git merge p_2'p_3'p_4'...。
将B_new切换到当前提交(即HEAD)(如果它尚未存在)
将标签B更改为应用于此新分支,而不是旧分支。 (即git reset --hard B)
使用
重新编译 - 在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在主(C)之上重新定义了主题(H)。 (例如, checkout topic; rebase master 。)在这种情况下,git会选择
以下提交重播:
- 选择D
- 选择E
- 选择F
-
- 选择H
,然后像下面这样更新提交图:
B --- C < - master
/ \
A D'--- E'--- F '--- G'--- H'< - topic
(D'是重播相当于D等。)
请注意,合并提交m未被选中用于重播。
如果我们改为在C之上做一个
- 保留 - 合并
重新绑定H(例如,结束主题; rebase --preserve-merges master <在这个新的例子中,git会选择以下提交重播:
- 选择D
- 选择E
- 选择F(到'subtopic'分支中的D')
- 选择G(到F'in
- 选择合并分支'subtopic'进入主题
- 选择H
现在选择m 进行重播。还要注意合并父母E和G在合并提交m之前选择了
。
下面是结果提交图:
B --- C < - master
/ \
A D'----- E' - - m'---- H'< - topic
\ /
F'------- G'
再一次,D'是一个樱桃选择的(即重新创建的)D.版本的Same for E'等等。每一个不在master上的提交都被重播。 E和G(m的合并父母)被重新创建为E'和G'作为m'的父母(在重新绑定之后,树历史依然保持不变)。
示例2 与正常的rebase不同,merge-preserving rebase可以创建多个
的上游头部。
例如,考虑:
B --- C < - master
/
A ------- D ------ E --- m ---- H < - topic
\\ \\ |
------- F ----- G - /
如果我们在C(master)上重新设置H(topic),那么选择rebase的提交是:
- pick D
- 选择E
- 选择F
- 选择G
- / li>
- 选择H
结果如下所示:
B --- C < - master
/ | \
A | D'---- E'--- m'---- H'< - topic
\ |
F'---- G'--- /
示例3在上面的例子中,合并提交和它的两个父代都是重播的提交,而不是原始合并提交具有的原始父代。但是,在其他重定位中,重播的合并提交最终可能会在合并之前已经在提交图中的父代。
例如,请考虑以下内容:$ / $>
B - C --- D < - master
/ \
A --- E-- m ------ F < - topic
如果我们将主题放在master上(保留合并),那么提交重放将是
- 选择合并提交m
- 选择F
重写的提交图如下所示:
B - C - D < - master
/ \
A ----- E --- m' - F'; < - topic
这里重播的合并提交m'获取预先存在于提交图中的父母,即D(master的HEAD)和E(原始合并提交m的父代之一)。
示例4 p>
在某些空白提交情况下,合并保留重新分配可能会感到困惑。至少这只是一些老版本的git(例如1.7.8)。
拿这个提交图:
A -------- B ----- C ----- m2 --- D < - master
\ \\ \\ /
E --- F - \ - G ---- /
\ \
--- m1 - H < - topic
请注意,提交m1和m2应该包含来自B和F的所有更改。
如果我们试图在D(master)上执行H(主题)的
git rebase --preserve-merges
,那么下面的提交是选择重播:
- 选择m1
- 选择H
请注意,m1中的变化(B,F)应该已经合并到D中。(这些变化应该已经合并到m2中,因为m2会合并B和F.)因此,从概念上讲,重放m1 o D的顶部应该可能是空操作或创建一个空的提交(即,其中一个是连续修订之间的差异是空的)。然而,git可能会拒绝在m之上重播m1的尝试。你可以得到这样的错误:
错误:提交90caf85是合并,但没有给出-m选项。
致命:樱桃挑选失败
看起来好像忘了把标志传给git ,但潜在的问题是,git不喜欢创建空的提交。
Git's documentation for the
rebase
command is quite brief:--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).
So what actually happens when you use
--preserve-merges
? How does it differ from the default behavior (without that flag)? What does it mean to "recreate" a merge, etc.解决方案As with a normal git rebase, git with
--preserve-merges
first identifies a list of commits made in one part of the commit graph, and then replays those commits on top of another part. The differences with--preserve-merges
concern which commits are selected for replay and how that replaying works for merge commits.To be more explicit about the main differences between normal and merge-preserving rebase:
- Merge-preserving rebase is willing to replay (some) merge commits, whereas normal rebase completely ignores merge commits.
- Because it's willing to replay merge commits, merge-preserving rebase has to define what it means to replay a merge commit, and deal with some extra wrinkles
- The most interesting part, conceptually, is perhaps in picking what the new commit's merge parents should be.
- Replaying merge commits also require explicitly checking out particular commits (
git checkout <desired first parent>
), whereas normal rebase doesn't have to worry about that.
- Merge-preserving rebase considers a shallower set of commits for replay:
- In particular, it will only consider replaying commits made since the most recent merge base(s) -- i.e. the most recent time the two branches diverged --, whereas normal rebase might replay commits going back to the first time the two branches diverged.
- To be provisional and unclear, I believe this is ultimately a means to screen out replaying "old commits" that have already been "incorporated into" a merge commit.
First I will try to describe "sufficiently exactly" what rebase
--preserve-merges
does, and then there will be some examples. One can of course start with the examples, if that seems more useful.The Algorithm in "Brief"
If you want to really get into the weeds, download the git source and explore the file
git-rebase--interactive.sh
. (Rebase is not part of Git's C core, but rather is written in bash. And, behind the scenes, it shares code with "interactive rebase".)But here I will sketch what I think is the essence of it. In order to reduce the number of things to think about, I have taken a few liberties. (e.g. I don't try to capture with 100% accuracy the precise order in which computations take place, and ignore some less central-seeming topics, e.g. what to do about commits that have already been cherry-picked between branches).
First, note that a non-merge-preserving rebase is rather simple. It's more or 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
is comparatively complicated. Here's as simple as I've been able to make it without losing things that seem pretty important: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")
Rebase with an
--onto C
argument should be very similar. Just instead of starting commit playback at the HEAD of B, you start commit playback at the HEAD of C instead. (And use C_new instead of B_new.)Example 1
For example, take commit graph
B---C <-- master / A-------D------E----m----H <-- topic \ / F-------G
m is a merge commit with parents E and G.
Suppose we rebased topic (H) on top of master (C) using a normal, non-merge-preserving rebase. (For example, checkout topic; rebase master.) In that case, git would select the following commits for replay:
- pick D
- pick E
- pick F
- pick G
- pick H
and then update the commit graph like so:
B---C <-- master / \ A D'---E'---F'---G'---H' <-- topic
(D' is the replayed equivalent of D, etc..)
Note that merge commit m is not selected for replay.
If we instead did a
--preserve-merges
rebase of H on top of C. (For example, checkout topic; rebase --preserve-merges master.) In this new case, git would select the following commits for replay:- pick D
- pick E
- pick F (onto D' in the 'subtopic' branch)
- pick G (onto F' in the 'subtopic' branch)
- pick Merge branch 'subtopic' into topic
- pick H
Now m was chosen for replay. Also note that merge parents E and G were picked for inclusion before merge commit m.
Here is the resulting commit graph:
B---C <-- master / \ A D'-----E'----m'----H' <-- topic \ / F'-------G'
Again, D' is a cherry-picked (i.e. recreated) version of D. Same for E', etc.. Every commit not on master has been replayed. Both E and G (the merge parents of m) have been recreated as E' and G' to serve as the parents of m' (after rebase, the tree history still remains the same).
Example 2
Unlike with normal rebase, merge-preserving rebase can create multiple children of the upstream head.
For example, consider:
B---C <-- master / A-------D------E---m----H <-- topic \ | ------- F-----G--/
If we rebase H (topic) on top of C (master), then the commits chosen for rebase are:
- pick D
- pick E
- pick F
- pick G
- pick m
- pick H
And the result is like so:
B---C <-- master / | \ A | D'----E'---m'----H' <-- topic \ | F'----G'---/
Example 3
In the above examples, both the merge commit and its two parents are replayed commits, rather than the original parents that the original merge commit have. However, in other rebases a replayed merge commit can end up with parents that were already in the commit graph before the merge.
For example, consider:
B--C---D <-- master / \ A---E--m------F <-- topic
If we rebase topic onto master (preserving merges), then the commits to replay will be
- pick merge commit m
- pick F
The rewritten commit graph will look like so:
B--C--D <-- master / \ A-----E---m'--F'; <-- topic
Here replayed merge commit m' gets parents that pre-existed in the commit graph, namely D (the HEAD of master) and E (one of the parents of the original merge commit m).
Example 4
Merge-preserving rebase can get confused in certain "empty commit" cases. At least this is true only some older versions of git (e.g. 1.7.8.)
Take this commit graph:
A--------B-----C-----m2---D <-- master \ \ / E--- F--\--G----/ \ \ ---m1--H <--topic
Note that both commit m1 and m2 should have incorporated all the changes from B and F.
If we try to do
git rebase --preserve-merges
of H (topic) onto D (master), then the following commits are chosen for replay:- pick m1
- pick H
Note that the changes (B, F) united in m1 should already be incorporated into D. (Those changes should already be incorporated into m2, because m2 merges together the children of B and F.) Therefore, conceptually, replaying m1 on top of D should probably either be a no-op or create an empty commit (i.e. one where the diff between successive revisions is empty).
Instead, however, git may reject the attempt to replay m1 on top of D. You can get an error like so:
error: Commit 90caf85 is a merge but no -m option was given. fatal: cherry-pick failed
It looks like one forgot to pass a flag to git, but the underlying problem is that git dislikes creating empty commits.
这篇关于git的“rebase --preserve-merges”到底是什么?做(为什么?)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!