如何使用onto命令对分支进行git rebase操作?

291
我注意到以下两组git命令的行为不同,但我不明白为什么。 我有一个分叉成一次提交的A和B分支。
---COMMIT--- (A)
\
 --- (B)

我想要将B分支rebase到最新的A上(并保留在B分支上的提交记录)

---COMMIT--- (A)
         \
          --- (B)

我这样做也没有问题:

checkout B
rebase A

但是如果我这么做:

checkout B
rebase --onto B A

完全没有反应,什么都没发生。我不明白为什么这两种行为是不同的。

PhpStorm GIT客户端使用第二种语法,因此似乎完全失效了,这就是我要求解决这个语法问题的原因。


http://git-scm.com/book/ch3-6.html - choroba
在切换到 B 后尝试运行 git rebase --onto A A - Prihex
7
这个问题已经过去很多年了,我认为我们在这个争论中所面临的根本理解问题是语义学上的:"onto"并不意味着"到......上面",而是意味着"在......之上"。 - Xmanoux
8个回答

725
正确的语法在您的情况下使用git rebase --onto将B重新基于A的顶部。
git checkout B
git rebase --onto A B^

或者在B的基础上重新定位到A,从B的父提交开始,用B^B~1引用。

如果你对git rebase <branch>git rebase --onto <branch>之间的区别感兴趣,请继续阅读。

简单快速:git rebase

git rebase <branch>将重新定位当前已检出的分支,由HEAD引用,到从<branch>可达但HEAD不可达的最新提交上。
这是最常见的重新定位情况,也是可能需要最少事先规划的情况。

          Before                           After
    A---B---C---F---G (branch)        A---B---C---F---G (branch)
             \                                         \
              D---E (HEAD)                              D---E (HEAD)

在这个例子中,FG是从branch可达但从HEAD不可达的提交。执行git rebase branch命令将会将D,也就是分支点之后的第一个提交,变基(即更改其父提交)到从branch可达但从HEAD不可达的最新提交G之上。

精确操作:使用两个参数的git rebase --onto

git rebase --onto允许你从一个特定的提交开始进行变基。它让你对正在变基的内容和位置有精确的控制。这适用于需要精确操作的场景。
举个例子,假设我们需要将HEADE精确地变基到F之上。我们只对将F带入我们的工作分支感兴趣,同时我们不想保留D,因为它包含了一些不兼容的更改。
          Before                           After
    A---B---C---F---G (branch)        A---B---C---F---G (branch)
             \                                     \
              D---E---H---I (HEAD)                  E---H---I (HEAD)

在这种情况下,我们会说git rebase --onto F D。这意味着:
重新设置从HEAD可达且父节点为D的提交,放在F之上。
换句话说,将E的父节点从D更改为Fgit rebase --onto的语法是git rebase --onto <newparent> <oldparent>
另一个使用这个命令很方便的场景是,当你想要快速从当前分支中移除一些提交,而不必进行交互式的重新设置时。
          Before                       After
    A---B---C---E---F (HEAD)        A---B---F (HEAD)

在这个例子中,为了从序列中移除C和E,你可以使用git rebase --onto B E,或者将HEAD重新基于B,其中旧的父节点是E。
外科医生:git rebase --onto与3个参数
git rebase --onto在精确性方面可以再进一步。实际上,它允许你在另一个提交的顶部重新基于一个任意范围的提交。
以下是一个例子:
          Before                                     After
    A---B---C---F---G (branch)                A---B---C---F---G (branch)
             \                                             \
              D---E---H---I (HEAD)                          E---H (HEAD)

在这种情况下,我们希望将确切的范围 E---H 重新基于 F,忽略 HEAD 当前指向的位置。我们可以通过执行 git rebase --onto F D H 来实现,这意味着:

将父提交为 DH 的提交范围重新基于 F

git rebase --onto 的语法中,使用一个提交范围,变成了 git rebase --onto <newparent> <oldparent> <until>。这里的诀窍是要记住,<until> 引用的提交是范围的一部分,并且在重新基于完成后将成为新的 HEAD

3
不错的答案。对于一般情况,只需添加一个小细节:如果范围的两个部分位于不同的分支上,则<oldparent>名称会失效。通常是这样的:“包括从<until>可达的每个提交,但排除从<oldparent>可达的每个提交。” - musiKk
130
“git rebase --onto <newparent> <oldparent>” 是我所见过的最好的关于 --onto 行为的解释! - ronkot
5
谢谢!我有些难以理解--onto选项,但是这次翻译把它讲得非常清楚!我甚至不明白以前为什么不能理解它:D 感谢你提供的出色“教程” :-) - grongor
6
虽然这个答案很好,但我觉得它没有涵盖所有可能的情况。最后一种语法形式也可以用来表达一种更微妙的变基类型。以《Pro Git》(第二版)中的例子为例,D不一定是H的祖先。相反,D和H也可以是具有共同祖先的提交——在这种情况下,Git会找出它们的共同祖先,并从那个祖先开始重演到F上的H。 - Pastafarian
2
总的来说,解释得非常好,不过提一下可能会有帮助,即在三个参数的例子(git rebase --onto D F H)中,如果你最初处于分离 HEAD 状态,则后续状态的图表看起来只是这样。如果初始状态中有一个指向 I 的分支,则你将拥有两个 E 和 H 提交。对于知道该如何查找的人来说,你的图表确实看起来像是在每种情况下都参考了分离 HEAD,但对于不知道的人来说并不明显,而且这在第三个例子中有很大的区别。 - De Novo
显示剩余21条评论

125

这是你需要知道的所有内容,以理解 --onto

git rebase --onto <newparent> <oldparent>

您正在切换提交的父级,但未提供提交的sha,只提供了当前(旧)父级的sha。


4
简短易懂。实际上,我花了最长时间才意识到在变基操作中,我需要提供要变基的那个提交的父提交而不是那个提交本身。 - Antoniossss
1
重要的细节是,您从当前分支选择旧父级的子级,因为一个提交可以是多个提交的父级,但当您限制自己在当前分支时,提交只能是一个提交的父级。换句话说,在分支上,父关系是唯一的,但如果您不指定分支,则不必如此。 - Trismegistos
3
请注意:您需要在分支中运行此命令,或将分支名称作为第三个参数添加进来。 git rebase --onto <newparent> <oldparent> <feature-branch> - Jason Portnoy
1
这个回答很棒,直戳本质,正如本帖所需。 - John Culviner

30
为了更好地理解git rebasegit rebase --onto之间的区别,了解这两个命令的可能行为是很有帮助的。git rebase可以让我们将提交移动到选定分支的顶部。就像这样:

To better understand difference between git rebase and git rebase --onto it is good to know what are the possible behaviors for both commands. git rebase allow us to move our commits on top of the selected branch. Like here:

git rebase master

结果是:

Before                              After
A---B---C---F---G (master)          A---B---C---F---G (master)
         \                                           \
          D---E (HEAD next-feature)                   D'---E' (HEAD next-feature)

git rebase --onto更加精确。它允许我们选择特定的提交点来开始,并确定要结束的位置。就像这样:

git rebase --onto更加精准。它允许我们选择特定的提交点作为起点和终点。就像这里:

git rebase --onto F D

结果是:

Before                                    After
A---B---C---F---G (branch)                A---B---C---F---G (branch)
         \                                             \
          D---E---H---I (HEAD my-branch)                E'---H'---I' (HEAD my-branch)

我建议您查看我的有关git rebase --onto概述的文章,以获取更多详细信息。


@Makyen 当然,我会记住的 :) - womanonrails
2
那么,我们可以将 git rebase --onto F D 理解为 将 D 的父节点设置为 F 的子节点,对吧? - Prihex
@Prihex 是的,我也这么认为 :) - womanonrails
不管是 rebase 到特定的 tip 还是直接 rebase,如果旧的树已经创建在 'remotes/origin/branchname' 中,你都必须强制 push。 - Ross Bu
@Prihex 那看起来确实正确,但我认为坚持真正的定义是万全之策。"<newbase> <oldbase>" 是我发现理解 rebase 的最佳方式...你有一个旧的 base,想要使用一个新的 base... 这个 base 是一个提交哈希值。 所以 git rebase --onto F D 表示 "F" 是新的 base, "D" 是旧的 base... 所以你可以看到 "F" 被 "D" 替代了。 字面上就是拔出(pluck)根(D)上的分支,并将其(D)精确地放在 "F" 上。我从未因为这个定义而失败过。 - masonCherry

26

简单来说,假设:

      Before rebase                             After rebase
A---B---C---F---G (branch)                A---B---C---F---G (branch)
         \                                         \   \
          D---E---H---I (HEAD)                      \   E'---H' (HEAD)
                                                     \
                                                      D---E---H---I

git rebase --onto F D H

这与使用--onto参数效果相同(因为该参数只接受一个参数):

git rebase D H --onto F

将范围在(D,H]之间的提交(不包括D)变基在F之上。 注意该范围是左开右闭的。 之所以选择左开右闭是因为可以通过输入例如branchgit查找从branch分叉出来的第一个提交,即D,它导致了H

OP案例

    o---o (A)
     \
      o (B)(HEAD)

git checkout B
git rebase --onto B A

可以改为单个命令:

git rebase --onto B A B
看起来出错的地方是B的放置位置,这意味着“将导致分支B的某些提交移动到B的顶部”。问题是什么是“某些提交”。如果添加-i标志,则会看到它是由HEAD指向的单个提交。该提交被跳过,因为它已经应用于--onto目标B,因此不会发生任何事情。
在任何重复使用分支名称的情况下,该命令都没有意义。这是因为提交范围将是那个分支中已经存在的一些提交,在重新设置期间,所有这些提交都将被跳过。
进一步解释和适用的git rebase <upstream> <branch> --onto <newbase>用法。 git rebase默认值。
git rebase master

扩展为以下之一:

git rebase --onto master master HEAD
git rebase --onto master master current_branch

重新定义后自动完成结账。

当以标准方式使用时,例如:

git checkout branch
git rebase master

在rebase之后,你不会注意到gitbranch移动到最新的rebase commit,并执行git checkout branch(请参见git reflog历史记录)。当第二个参数是commit hash而不是分支名称时,rebase仍然有效,但没有分支可移动,因此您最终进入"detached HEAD",而不是被检出到已移动的分支。

省略主要分歧提交。

--onto中的master取自第一个git rebase参数。

                   git rebase master
                              /    \
         git rebase --onto master master

实际上它可以是任何其他提交或分支。这样您就可以通过获取最新的提交并保留主要分叉提交来限制rebase提交的数量。

git rebase --onto master HEAD~
git rebase --onto master HEAD~ HEAD  # Expanded.

将由HEAD指向的单个提交变基到master,并最终处于“分离头指针”状态。

避免显式检出。

默认的HEADcurrent_branch参数是从你所在的位置上下文地获取的。这就是为什么大多数人会检出他们想要变基的分支。但是当显式给出第二个变基参数时,您不必先检出再进行变基以隐式方式传递它。

(branch) $ git rebase master
(branch) $ git rebase master branch  # Expanded.
(branch) $ git rebase master $(git rev-parse --abbrev-ref HEAD)  # Kind of what git does.
这意味着您可以从任何位置重新安排提交和分支。 因此,与重新安排后自动检出。一起使用,您无需在重新安排之前或之后单独检出重定基分支。
(master) $ git rebase master branch
(branch) $ # Rebased. Notice checkout.

2
这是最好的解释!我从未理解“git rebase <upstream>”只是“git rebase <upstream> --onto <upstream>”的速记。这篇文章还教会了我:我们总是选择一系列提交--来自'git rebase'文档:“在另一个基础提示上重新应用提交”。 - Jordan

23

Git的术语有点令人困惑,如果你这样假装命令看起来是这样的,可能会有所帮助:

git rebase --onto=<新基地> <旧基地> [<分支>]

如果我们现在在分支上,则可以省略它:

git rebase --onto=<新基地> <旧基地>

如果新基地旧基地相同,我们可以省略--onto参数:

git rebase <新旧基地>

这听起来可能很奇怪:如果旧基地和新基地相同,那么如何进行rebase呢?但是请这样思考,如果您拥有一个功能分支foo,它已经(很可能)基于主分支中的某个提交。通过“重新基于”,我们只是使其基于的提交更加最新。

(实际上,<旧基地>是我们将分支与之进行比较的内容。如果它是一个分支,则git会查找共同祖先(也可以参见--fork-point);如果它是当前分支上的一个提交,则使用该提交之后的提交;如果它是一个与当前分支没有共同祖先的提交,则使用从当前分支开始的所有提交。 <新基地>也可以是一个提交。因此,例如,git rebase --onto HEAD~ HEAD将获取从旧基地HEAD到当前HEAD之间的提交并将其放在HEAD~的顶部,实际上删除了最后一次提交。)


15

简单地说,git rebase --onto 选择一系列提交并将它们变基到给定的提交上。

阅读 git rebase 的手册,搜索 "onto"。示例非常好:

example of --onto option is to rebase part of a branch. If we have the following situation:

                                   H---I---J topicB
                                  /
                         E---F---G  topicA
                        /
           A---B---C---D  master

   then the command

       git rebase --onto master topicA topicB

   would result in:

                        H'--I'--J'  topicB
                       /
                       | E---F---G  topicA
                       |/
           A---B---C---D  master

在这种情况下,您告诉Git将从topicAtopicB的提交在master之上进行变基。


9

对于onto,您需要两个额外的分支。使用该命令,您可以将基于branchAbranchB提交应用于另一个分支,例如master。在下面的示例中,branchB基于branchA,您想要将branchB的更改应用于master而不应用branchA的更改。

o---o (master)
     \
      o---o---o---o (branchA)
                   \
                    o---o (branchB)

通过使用以下命令:
checkout branchB
rebase --onto master branchA 

您将获得以下提交层次结构。
      o'---o' (branchB)
     /
o---o (master)
     \
      o---o---o---o (branchA)

1
请问您能否再解释一下,如果我们想要将当前分支变基到主分支上,为什么它会成为当前分支?如果您执行 rebase --onto branchA branchB,那么是否会将整个主分支放在 branchA 的头部? - Polymerase
8
这句话的意思是“这不应该是 checkout branchB: rebase --onto master branchA 吗?”。我会将其翻译为“难道不应该使用 checkout branchB: rebase --onto master branchA 命令吗?”。 - goldenratio
5
为什么这个被点赞了?它并没有做它所说的事情。 - De Novo
1
我编辑并修复了答案,这样人们就不必先破坏他们的仓库分支,然后再来阅读评论了... - Kamafeather

2

还有一种情况,git rebase --onto 很难理解:当您将其变基到由对称差异选择器(三个点'...')生成的提交时。

Git 2.24(2019年第四季度)在处理这种情况时做得更好:

请参见提交记录414d924, 提交记录4effc5b, 提交记录c0efb4c, 提交记录2b318aa(2019年8月27日)以及提交记录793ac7e, 提交记录359eceb(2019年8月25日),作者为Denton Liu (Denton-L)
协助者包括:Eric Sunshine (sunshineco), Junio C Hamano (gitster), Ævar Arnfjörð Bjarmason (avar), 以及 Johannes Schindelin (dscho)
请参见提交记录6330209, 提交记录c9efc21(2019年8月27日),以及提交记录4336d36 (2019年8月25日),作者为Ævar Arnfjörð Bjarmason (avar)
协助者包括:Eric Sunshine (sunshineco), Junio C Hamano (gitster), Ævar Arnfjörð Bjarmason (avar), 以及 Johannes Schindelin (dscho)
(由Junio C Hamano -- gitster --提交记录640f9cd合并,2019年9月30日)

rebase: fast-forward --onto in more cases

Before, when we had the following graph,

A---B---C (master)
     \
      D (side)

running 'git rebase --onto master... master side' would result in D being always rebased, no matter what.

此时,请阅读 "Git diff提交范围中双点“..”和三点“…”之间的区别是什么?"。

https://sphinx.mythic-beasts.com/~mark/git-diff-help.png

这里的 "master..." 指的是 master...HEAD,也就是 B:HEAD 是当前检出的 HEAD,你正在将其变基到 B
你在进行什么样的变基?任何不在 master 分支中但可以从 side 分支到达的提交:只有一个符合该描述的提交:D... 它已经在 B 的顶部了!
再次提醒,在 Git 2.24 之前,这样的 rebase --onto 将会导致无论如何都要对 D 进行变基。

然而,期望的行为是,变基应该注意到这是可快进的,并执行快进操作。

这类似于 OP 的 rebase --onto B A,什么也没做。

Add detection to can_fast_forward so that this case can be detected and a fast-forward will be performed.
First of all, rewrite the function to use gotos which simplifies the logic.
Next, since the

options.upstream &&
!oidcmp(&options.upstream->object.oid, &options.onto->object.oid)

conditions were removed in cmd_rebase, we reintroduce a substitute in can_fast_forward.
In particular, checking the merge bases of upstream and head fixes a failing case in t3416.

The abbreviated graph for t3416 is as follows:

        F---G topic
       /
  A---B---C---D---E master

而且失败的命令是:
git rebase --onto master...topic F topic

在此之前,Git会发现只有一个合并基地(C,即master...topic的结果),合并和转移是相同的,因此它会错误地返回1,表示我们可以快进。这将导致重新定位的图形为'ABCFG',而我们期望的是'ABCG'。 rebase --onto C F topic意味着任何提交F之后,通过topic HEAD可达:G仅限于此,不包括F本身。
在这种情况下进行快速转发将在重新定位的分支中包含F,这是错误的。
使用额外的逻辑,我们检测到上游和头部的合并基为F。由于onto不是F,这意味着我们没有将完整的提交集从master..topic变基。
由于我们排除了一些提交,因此无法执行快进操作,因此我们正确地返回0。
在测试用例中添加“-f”,因为由于此更改而失败,它们不希望进行快速转发,因此需要强制进行变基。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接