'rebase master' 和 'rebase --onto master' 之间的区别来自从 master 分支派生的分支

Difference between 'rebase master' and 'rebase --onto master' from a branch derived from a branch of master

给定以下分支结构:

  *------*---*
Master        \
               *---*--*------*
               A       \
                        *-----*-----*
                        B         (HEAD)

如果我想将我的 B 更改(并且 我的 B 更改,没有 A 更改)合并到 master 中,这两组命令之间有什么区别?

>(B)      git rebase master
>(B)      git checkout master
>(master) git merge B

>(B)      git rebase --onto master A B
>(B)      git checkout master
>(master) git merge B

我主要想了解如果我使用第一种方式,分支 A 的代码是否可以成为 master。

差异:

第一组

  • (乙)git rebase master

    *---*---* [master]
             \
              *---*---*---* [A]
                       \
                        *---*---* [B](HEAD)
    

什么都没发生。自 B 分支创建以来 master 分支中没有新提交。

  • (B) git checkout master

    *---*---* [master](HEAD)
             \
              *---*---*---* [A]
                       \
                        *---*---* [B]
    
  • (硕士)git merge B

    *---*---*-----------------------* [Master](HEAD)
             \                     /
              *---*---*---* [A]   /
                       \         /
                        *---*---* [B]
    

第二组

  • (乙)git rebase --onto master A B

    *---*---*-- [master]
            |\
            | *---*---*---* [A]
            |
            *---*---* [B](HEAD)
    
  • (乙)git checkout master

    *---*---*-- [master](HEAD)
            |\
            | *---*---*---* [A]
            |
            *---*---* [B]
    
  • (硕士)git merge B

    *---*---*----------------------* [master](HEAD)
            |\                    /
            | *---*---*---* [A]  /
            |                   /  
            *---*--------------* [B]
    

I want to merge my B changes (and only my B changes, no A changes) into master

请注意您对 "only my B changes" 的理解。

在第一组中,B 分支是(在最终合并之前):

 *---*---*
          \
           *---*---*
                    \
                     *---*---* [B]

而在第二组中,您的 B 分支是:

*---*---*
        |
        |
        |
        *---*---* [B]

如果我没理解错的话,你想要的只是不在A分支中的B提交。所以,第二套是你合并前的正确选择。

在进行任何给定操作之前,您的存储库如下所示

           o---o---o---o---o  master
                \
                 x---x---x---x---x  A
                                  \
                                   o---o---o  B

标准变基后(没有--onto master)结构将是:

           o---o---o---o---o  master
               |            \
               |             x'--x'--x'--x'--x'--o'--o'--o'  B
                \
                 x---x---x---x---x  A

...其中 x' 是来自 A 分支的提交。 (请注意它们现在是如何在分支 B 的底部复制的。)

相反,使用 --onto master 的变基将创建以下更清晰、更简单的结构:

           o---o---o---o---o  master
               |            \
               |             o'--o'--o'  B
                \
                 x---x---x---x---x  A

你可以自己试试看。您可以创建一个本地 git 存储库来玩:

#! /bin/bash
set -e
mkdir repo
cd repo

git init
touch file
git add file
git commit -m 'init'

echo a > file0
git add file0
git commit -m 'added a to file'

git checkout -b A
echo b >> fileA
git add fileA
git commit -m 'b added to file'
echo c >> fileA
git add fileA
git commit -m 'c added to file'

git checkout -b B
echo x >> fileB
git add fileB
git commit -m 'x added to file'
echo y >> fileB
git add fileB
git commit -m 'y added to file'
cd ..

git clone repo rebase
cd rebase
git checkout master
git checkout A
git checkout B
git rebase master
cd ..

git clone repo onto
cd onto
git checkout master
git checkout A
git checkout B
git rebase --onto master A B
cd ..

diff <(cd rebase; git log --graph --all) <(cd onto; git log --graph --all)

在我按要求回答问题之前,请耐心等待一段时间。一个较早的答案是正确的,但存在标签和其他相对较小(但可能令人困惑)的问题,所以我想从 b运行ch 图纸和 b运行ch 标签开始。此外,来自其他系统的人,或者甚至可能只是版本控制和 git 的新手,通常认为 b运行ches 是 "lines of development" 而不是 "traces of history"(git 将它们实现为后者,而不是前者,因此提交不一定在 any 特定 "line of development").

首先,您绘制图表的方式存在一个小问题:

  *------*---*
Master        \
               *---*--*------*
               A       \
                        *-----*-----*
                        B         (HEAD)

这是完全相同的图表,但绘制的标签不同,并且添加了更多 arrow-heads(我已经为下面使用的提交节点编号):

0 <- 1 <- 2         <-------------------- master
           \
            3 <- 4 <- 5 <- 6      <------ A
                       \
                        7 <- 8 <- 9   <-- HEAD=B

为什么这很重要,因为 git 对提交 "on" some b运行ch 的含义相当宽松——或者更好的说法是一些提交是 "contained in" 一些 b运行 的集合。不能移动或更改提交,但 b运行ch labels 可以移动。

更具体地说,像 masterAB 这样的 b运行ch name 指向 一个特定的提交。在这种情况下,master 指向提交 2,A 指向提交 6,B 指向提交 9。前几个提交 0 到 2 包含在所有三个 b运行切;提交 3、4 和 5 包含在 AB 中;提交 6 仅包含在 A 中;并且提交 7 到 9 仅包含在 B 中。 (顺便说一句,多个名称可以指向同一个提交,这在您创建新的 b运行ch 时是正常的。)

在我们继续之前,让我 re-draw 图形的另一种方式:

0
 \
  1
   \
    2     <-- master
     \
      3 - 4 - 5
              |\
              | 6   <-- A
               \
                7
                 \
                  8
                   \
                    9   <-- HEAD=B       

这只是强调重要的不是提交的 水平线 ,而是 parent/child 关系。 b运行ch label 指向开始提交,然后(至少这些图的绘制方式)我们向左移动,也可能根据需要向上或向下移动,找到 parent 提交。


当你变基提交时,你实际上是在复制那些提交。

Git 永远无法更改任何提交

有一个 "true name" 用于任何提交(或者 git 存储库中的任何 object),这是它的 SHA-1:那个 40-hex-digit 字符串例如,您在 git log 中看到的 9f317ce...。 SHA-1 是 object 内容的加密 1 校验和。内容是作者和提交者(姓名和电子邮件)、时间戳、源代码树和 parent 提交列表。提交 #7 的 parent 始终是提交 #5。如果您创建提交 #7 的 mostly-exact 副本,但将其 parent 设置为提交 #2 而不是提交 #5,您将获得具有不同 ID 的不同提交。 (此时我已经从单个 digit 中 运行——通常我使用单个大写字母来表示提交 ID,但是 b运行ches 命名为 AB 我认为这会令人困惑。所以我将在下面调用 #7、#7a 的副本。)

git rebase 的作用

当您要求 git 对提交链进行变基时——例如上面的提交#7-8-9——它必须 复制 它们,至少如果他们将要移动到任何地方(如果他们不移动,则可以将原件留在原处)。它默认从 currently-checked-out b运行ch 复制提交,所以 git rebase 只需要两条额外的信息:

  • 它应该复制哪些提交?
  • 副本应该放在哪里?也就是说,first-copied 提交的目标 parent-ID 是什么? (额外的提交只是指向 first-copied、second-copied,等等。)

当你 运行 git rebase <upstream> 时,你让 git 从一条信息中找出这两个部分。当您使用 --onto 时,您可以分别告诉 git 这两个部分:您仍然提供 upstream 但它不会计算 目标 来自 <upstream>,它仅计算 提交以从 <upstream> 复制 。 (顺便说一句,我认为 <upstream> 不是一个好名字,但它是 rebase 使用的,我没有更好的方法,所以让我们坚持下去。Rebase 调用 target <newbase>,但我认为 target 是一个更好的名字。)

我们先来看看这两个选项。两者都假设您首先在 b运行ch B 上:

  1. git rebase master
  2. git rebase --onto master A

对于第一个命令,rebase<upstream> 参数是 master。第二个是 A.

下面是 git 计算要复制的提交的方式:它将当前的 b运行ch 交给 git rev-list,并且还将 <upstream> 交给 git rev-list,但使用 --not——或者更准确地说,使用等同于 two-dot exclude..include 的表示法。这意味着我们需要知道 git rev-list 是如何工作的。

虽然 git rev-list 极其复杂——大多数git 命令最终使用它;它是 git loggit bisectrebasefilter-branch 等的引擎——这种特殊情况并不难:使用 two-dot 表示法,rev-list 列出从 right-hand 端可访问的每个提交(包括该提交本身),不包括从 left-hand 端可访问的每个提交。

在这种情况下,git rev-list HEAD 找到所有可从 HEAD 访问的提交——也就是说,几乎所有提交:提交 0-5 和 7-9——并且 git rev-list master 找到所有提交可从 master 到达,即提交 #s 0、1 和 2。从 0-5,7-9 减去 0-through-2 留下 3-5,7-9。这些是要复制的候选提交,如 git rev-list master..HEAD.

所列

对于我们的第二个命令,我们有 A..HEAD 而不是 master..HEAD,因此要减去的提交是 0-6。提交 #6 没有出现在 HEAD 集中,但这很好:减去不存在的东西,让它不存在。因此,结果 candidates-to-copy 是 7-9。

这仍然让我们弄清楚 rebase 的 target,即复制的提交应该放在哪里?使用第二个命令,答案是 "the commit identified by the --onto argument"。由于我们说 --onto master,这意味着目标是提交 #2。

变基 #1

git rebase master

但是,对于第一个命令,我们没有直接指定目标,所以 git 使用 <upstream> 标识的提交。我们给出的 <upstream>master,它指向提交 #2,所以目标是提交 #2。

因此,第一个命令将从复制提交 #3 开始,只需要进行任何最小的更改,以便它的 parent 是提交 #2。它的 parent 是 已经 提交 #2。无需更改,因此无需更改,只需 re-uses 现有提交 #3 即可变基。然后它必须复制 #4 以便它的 parent 是 #3,但是 parent 已经是 #3,所以它只是 re-uses #4。同样,#5 已经很好了。它完全忽略了#6(它不在要复制的提交集中);它检查#s 7-9 但它们也都很好,所以整个变基最终只是 re-using 所有原始提交。你可以用 -f 强制复制,但你没有,所以整个 rebase 最终什么都不做。

变基#2

git rebase --onto master A

第二个 rebase 命令使用 --onto 到 select #2 作为其目标,但告诉 git 只复制提交 7-9。提交 #7 的 parent 是提交 #5,所以这个副本确实必须做一些事情。2 所以 git 进行新的提交——我们称之为 #7a——它的 parent 有提交 #2。 rebase 继续提交 #8:副本现在需要 #7a 作为其 parent。最后,rebase 继续提交 #9,它需要 #8a 作为它的 parent。复制所有提交后,rebase 做的最后一件事是移动标签(记住,标签会移动和更改!)。这给出了这样的图表:

          7a - 8a - 9a       <-- HEAD=B
         /
0 - 1 - 2                    <-- master
         \
          3 - 4 - 5 - 6      <-- A
                    \
                     7 - 8 - 9   [abandoned]

好的,但是 git rebase --onto master A B 呢?

这与git rebase --onto master A几乎相同。不同之处在于末尾有额外的 B 。幸运的是,这种区别 非常 简单:如果你给 git rebase 那个额外的参数,它 运行 会先 git checkout 那个参数。3

你原来的命令

在你的第一组命令中,你在 b运行ch B 上 运行 git rebase master。如上所述,这是一个很大的 no-op:因为没有任何东西需要移动,所以 git 根本不复制任何东西(除非你使用 -f / --force,你没有).然后您签出 master 并使用了 git merge B,如果它被告知 4,它会创建一个新的合并提交。因此 ,至少在我看到它的时候,在这里是正确的:合并提交有两个 parents,其中一个是 b运行ch [=17 的提示=],并且 b运行ch 通过 b运行ch A 上的三个提交返回,因此 A 上的一些内容最终被合并到 master.

对于第二个命令序列,您首先检查了 B(您已经在 B 上,所以这是多余的,但它是 git rebase 的一部分)。然后,您对三个提交进行了变基复制,生成了上面的最终图表,其中提交了 7a、8a 和 9a。然后您签出 master 并使用 B 进行了合并提交(再次参见脚注 4)。 Dherik 的回答再次是正确的:唯一缺少的是原始的、被遗弃的提交不是 drawn-in 并且新的 merged-in 提交是副本并不那么明显。


1这很重要,因为要确定特定的校验和非常困难。也就是说,如果某人 you trust 告诉你 "I trust the commit with ID 1234567...",其他人(你可能不太信任的人)几乎不可能提出具有相同的提交ID,但内容不同。意外发生的几率是二分之一160,这比你被闪电击中心脏病发作,被海啸淹死被绑架的几率要小得多space外星人。 :-)

2实际复制是使用git cherry-pick 的等效项:git 将提交的树与其 parent 的树进行比较以获得差异,然后将差异应用于新的 parent 的树。

3此时,这实际上是真的:git rebase 是一个 shell 脚本,它解析您的选项,然后决定哪种内部变基为 运行:non-interactive git-rebase--am 或交互式 git-rebase--interactive。在计算出所有参数后,如果有一个 left-over b运行ch name 参数,脚本会在开始内部 rebase 之前执行 git checkout <branch-name>

4因为 master 指向提交 2 并且提交 2 是提交 9 的祖先,所以这通常不会进行合并提交,而是执行Git 所谓的 fast-forward 操作。您可以指示 Git 不要使用 git merge --no-ff 执行这些 fast-forward。某些界面,例如 GitHub 的 Web 界面和一些 GUI,可能会分离不同类型的操作,因此它们的 "merge" 会像这样强制进行真正的合并。

通过 fast-forward 合并,第一种情况的最终图表是:

0 <- 1 <- 2         [master used to be here]
           \
            3 <- 4 <- 5 <- 6      <------ A
                       \
                        7 <- 8 <- 9   <-- master, HEAD=B

无论哪种情况,提交 1 到 9 现在都在 both b运行ches,master and B。与真正的合并相比,不同之处在于,从图表中,您可以看到包含合并的历史记录。

换句话说,fast-forward 合并的优点是它不会留下任何痕迹,否则这是一个微不足道的操作。 fast-forward 合并的缺点是,它不会留下任何痕迹。因此,是否允许 fast-forward 的问题实际上是您是否 想要 在提交形成的历史记录中留下显式合并的问题。

git log --graph --decorate --oneline A B master(或等效的 GUI 工具)可以在每个 git 命令之后使用以可视化更改。

这是存储库的初始状态,B 为当前分支。

(B) git log --graph --oneline --decorate A B master
* 5a84c72 (A) C6
| * 9a90b7c (HEAD -> B) C9
| * 2968483 C8
| * 187c9c8 C7
|/  
* 769014a C5
* 6b8147c C4
* 9166c60 C3
* 0aaf90b (master) C2
* 8c46dcd C1
* 4d74b57 C0

这是一个在这种状态下创建存储库的脚本。

#!/bin/bash

commit () {
    for i in $(seq  ); do
        echo article $i > $i
        git add $i
        git commit -m C$i
    done
}

git init
commit 0 2

git checkout -b A
commit 3 6

git checkout -b B HEAD~
commit 7 9

第一个 rebase 命令什么都不做。

(B) git rebase master
Current branch B is up to date.

签出 master 并合并 B 只是将 master 指向与 B 相同的提交(即 9a90b7c)。没有创建新的提交。

(B) git checkout master
Switched to branch 'master'

(master) git merge B
Updating 0aaf90b..9a90b7c
Fast-forward
<... snipped diffstat ...>

(master) git log --graph --oneline --decorate A B master
* 5a84c72 (A) C6
| * 9a90b7c (HEAD -> master, B) C9
| * 2968483 C8
| * 187c9c8 C7
|/  
* 769014a C5
* 6b8147c C4
* 9166c60 C3
* 0aaf90b C2
* 8c46dcd C1
* 4d74b57 C0

第二个 rebase 命令复制 A..B 范围内的提交并将它们指向 master。此范围内的三个提交是 9a90b7c C9, 2968483 C8, and 187c9c8 C7。这些副本是具有自己的提交 ID 的新提交; 7c0e24140b105d5b0bda1。分支 masterA 不变。

(B) git rebase --onto master A B
First, rewinding head to replay your work on top of it...
Applying: C7
Applying: C8
Applying: C9

(B) log --graph --oneline --decorate A B master
* 7c0e241 (HEAD -> B) C9
* 40b105d C8
* 5b0bda1 C7
| * 5a84c72 (A) C6
| * 769014a C5
| * 6b8147c C4
| * 9166c60 C3
|/  
* 0aaf90b (master) C2
* 8c46dcd C1
* 4d74b57 C0

和以前一样,签出 master 并合并 B 只是将 master 指向与 B 相同的提交(即 7c0e241)。没有创建新的提交。

B 指向的原始提交链仍然存在。

git log --graph --oneline --decorate A B master 9a90b7c
* 7c0e241 (HEAD -> master, B) C9
* 40b105d C8
* 5b0bda1 C7
| * 5a84c72 (A) C6
| | * 9a90b7c C9    <- NOTE: This is what B used to be
| | * 2968483 C8
| | * 187c9c8 C7
| |/  
| * 769014a C5
| * 6b8147c C4
| * 9166c60 C3
|/  
* 0aaf90b C2
* 8c46dcd C1
* 4d74b57 C0