'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 可以移动。
更具体地说,像 master
、A
或 B
这样的 b运行ch name 指向 一个特定的提交。在这种情况下,master
指向提交 2,A
指向提交 6,B
指向提交 9。前几个提交 0 到 2 包含在所有三个 b运行切;提交 3、4 和 5 包含在 A
和 B
中;提交 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 命名为 A
和B
我认为这会令人困惑。所以我将在下面调用 #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
上:
git rebase master
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 log
、git bisect
、rebase
、filter-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 的新提交; 7c0e241
、40b105d
和 5b0bda1
。分支 master
和 A
不变。
(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
给定以下分支结构:
*------*---*
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 可以移动。
更具体地说,像 master
、A
或 B
这样的 b运行ch name 指向 一个特定的提交。在这种情况下,master
指向提交 2,A
指向提交 6,B
指向提交 9。前几个提交 0 到 2 包含在所有三个 b运行切;提交 3、4 和 5 包含在 A
和 B
中;提交 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 命名为 A
和B
我认为这会令人困惑。所以我将在下面调用 #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
上:
git rebase master
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 log
、git bisect
、rebase
、filter-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,它会创建一个新的合并提交。因此 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 的新提交; 7c0e241
、40b105d
和 5b0bda1
。分支 master
和 A
不变。
(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