git 的“rebase --preserve-merges"究竟是做什么的?做(为什么?) [英] What exactly does git's "rebase --preserve-merges" do (and why?)

查看:101
本文介绍了git 的“rebase --preserve-merges"究竟是做什么的?做(为什么?)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

--preserve-merges不要忽略合并,而是尝试重新创建它们.这在内部使用 --interactive 机制,但将其结合起来明确地使用 --interactive 选项通常不是一个好主意除非您知道自己在做什么(请参阅下面的错误).

那么当您使用 --preserve-merges 时实际发生了什么?它与默认行为(没有该标志)有何不同?重新创建"合并等是什么意思

解决方案

与普通的 git rebase 一样,带有 --preserve-merges 的 git 首先标识在其中一个部分进行的提交列表提交图,然后在另一部分之上重放这些提交.与 --preserve-merges 的不同之处在于选择哪些提交进行重放以及重放如何用于合并提交.

更明确地说明正常变基和合并保留变基之间的主要区别:

  • 保留合并的 rebase 愿意重放(一些)合并提交,而普通的 rebase 完全忽略合并提交.
  • 因为它愿意重放合并提交,所以保留合并的 rebase 必须定义重放合并提交的意义,并处理一些额外的问题
    • 从概念上讲,最有趣的部分可能是选择新提交的合并父项应该是什么.
    • 重放合并提交还需要明确检查特定提交(git checkout ),而正常的 rebase 不必担心这一点.
  • 保留合并的 rebase 考虑用于重放的一组较浅的提交:
    • 特别是,它只会考虑重播自最近的合并基础以来所做的提交——即两个分支分歧的最近时间——,而正常的 rebase 可能会重播正在进行的提交回到第一次这两个分支的分歧.
    • 暂时且不清楚,我相信这最终是一种筛选已合并到"合并提交中的旧提交"的重放方式.

首先我将尝试完全准确地"描述 rebase --preserve-merges 的作用,然后会有一些例子.如果这看起来更有用的话,当然可以从示例开始.

Brief"中的算法

如果您想真正深入研究,请下载 git 源并浏览文件 git-rebase--interactive.sh.(Rebase 不是 Git 的 C 核心的一部分,而是用 bash 编写的.而且,在幕后,它与交互式 rebase"共享代码.)

但在这里我将勾勒出我认为它的本质.为了减少要考虑的事情的数量,我采取了一些自由.(例如,我不会尝试 100% 准确地捕捉计算发生的精确顺序,并忽略一些不太中心化的主题,例如如何处理已经在分支之间挑选出来的提交).

首先,请注意非合并保留 rebase 相当简单.或多或少:

在 B 上查找所有提交但不在 A 上(git log A..B")将 B 重置为 A ("git reset --hard A")一次一个地重播所有这些提交到 B 上.

Rebase --preserve-merges 比较复杂.这就像我能够做到的那样简单而不会丢失看起来很重要的东西:

找到要重放的提交:首先找到 A 和 B 的合并基(即最近的共同祖先)这个(这些)合并基础将作为 rebase 的根/边界.特别是,我们将取其(他们的)后代并在新父母之上重放它们现在我们可以定义 C,即要重放的提交集.特别是那些提交:1) 可从 B 到达但不能从 A 到达(如在正常变基中),并且还2) 合并基的后代如果我们忽略cherry-picks和其他聪明的preserve-merges,它或多或少是:git log A..B --not $(git merge-base --all A B)重播提交:创建一个分支 B_new,在该分支上重放我们的提交.切换到 B_new(即git checkout B_new")继续父母在孩子之前(--topo-order),在 B_new 之上重放 C 中的每个提交 c:如果是非合并提交,请照常选择(即git cherry-pick c")否则它是一个合并提交,我们将构建一个等效的"合并提交 c':要创建合并提交,其父项必须存在并且我们必须知道它们是什么.所以首先,通过参考 c 的父母,找出 c' 使用哪个父母:对于parents_of(c) 中的每个父p_i:如果 p_i 是上面提到的合并基之一:# p_i 是我们不想再用作父项的边界提交"之一对于新提交的第 i 个父项 (p_i'),使用 B_new 的 HEAD.否则,如果 p_i 是被重写的提交之一(即如果 p_i 在 R 中):# 注意:因为我们正在移动父母在孩子之前,一个重写的版本# of 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' ...".切换(即git reset")B_new 到当前提交(即 HEAD),如果它还没有的话更改标签 B 以应用于这个新分支,而不是旧分支.(即git reset --hard B")

带有 --onto C 参数的 Rebase 应该非常相似.只是不是在 B 的 HEAD 处开始提交回放,而是在 C 的 HEAD 处开始提交回放.(并使用 C_new 而不是 B_new.)

示例 1

以提交图为例

 B---C <-- master/A-------D------E----m----H <-- 主题/F-------G

m 是与父 E 和 G 的合并提交.

假设我们使用正常的、非合并保留的主题 (H) 在主 (C) 之上重新定位变基.(例如,checkout topic; rebase master.)在这种情况下,git 会选择以下提交用于重播:

  • 选择 D
  • 选择E
  • 选择 F
  • 选择G
  • 选择H

然后像这样更新提交图:

 B---C <-- master/A D'---E'---F'---G'---H' <--主题

(D' 是重放的 D 等价物,等等.)

请注意,未选择合并提交 m 进行重放.

如果我们改为在 C 之上对 H 进行 --preserve-merges rebase.(例如,checkout topic; rebase --preserve-merges master.) 在这种新情况下,git 将选择以下提交进行重播:

  • 选择 D
  • 选择E
  • 选择 F(到副主题"分支中的 D')
  • 选择 G(到子主题"分支中的 F')
  • 选择将分支子主题"合并到主题中
  • 选择H

现在我被选择进行重播.还要注意合并父母 E 和 G 是在合并提交 m 之前选择包含.

这是生成的提交图:

 B---C <-- master/A D'-----E'----m'----H' <-- 主题/F'-------G'

同样,D' 是 D 的精选(即重新创建)版本. E' 等也是如此.每个不在 master 上的提交都已重播.E 和 G(m 的合并父节点)都被重新创建为 E' 和 G' 作为 m' 的父节点(rebase 后,树历史仍然保持不变).

示例 2

与普通的 rebase 不同,merge-preserving rebase 可以创建多个上游头的孩子.

例如,考虑:

 B---C <-- master/A-------D------E---m----H <-- 主题 |------- F-----G--/

如果我们在 C(主)之上 rebase H(主题),那么为 rebase 选择的提交是:

  • 选择 D
  • 选择E
  • 选择 F
  • 选择G
  • 选米
  • 选择H

结果是这样的:

 B---C <-- master/|一个 |D'----E'---m'----H' <-- 主题 |F'----G'---/

示例 3

在上面的例子中,合并提交及其两个父项都是重播提交,而不是原始合并提交所具有的原始父项.但是,在其他变基中,重放的合并提交可能会以合并前已在提交图中的父项结束.

例如,考虑:

 B--C---D <-- 大师/A---E--m------F <-- 主题

如果我们将主题变基到主节点(保留合并),那么重播的提交将是

  • 选择合并提交 m
  • 选择 F

重写后的提交图如下所示:

 B--C--D <-- 大师/A-----E---m'--F';<-- 话题

这里重放的合并提交 m' 获取提交图中预先存在的父项,即 D(主节点的 HEAD)和 E(原始合并提交 m 的父项之一).

示例 4

在某些空提交"情况下,保留合并的 rebase 可能会混淆.至少这仅适用于某些较旧版本的 git(例如 1.7.8.)

以这个提交图为例:

 A--------B-----C-----m2---D <-- master /E--- F-----G----/ ---m1--H <--主题

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

如果我们尝试将 H(主题)的 git rebase --preserve-merges 操作到 D(主)上,则选择以下提交进行重放:

  • 选择 m1
  • 选择H

请注意,m1 中合并的更改 (B, F) 应该已经合并到 D 中.(这些更改应该已经合并到 m2 中,因为 m2 合并了 B 和 F 的孩子.)因此,从概念上讲,重放 m1在 D 之上应该可能是一个无操作或创建一个空提交(即连续修订之间的差异为空的提交).

然而,git 可能会拒绝在 D 之上重放 m1 的尝试.您可能会收到如下错误:

error: Commit 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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆