给定以下分支结构:
*------*---*
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的代码可以使它成为主。
在我回答问题之前,请耐心等待一段时间。一个较早的答案是正确的,但有标签和其他相对较小(但可能令人困惑)的问题,所以我想从分支图纸和分支标签开始。此外,来自其他系统的人,或者甚至可能只是修改控制和git的新手,通常认为分支是“发展线”而不是“历史痕迹”(git将它们实现为后者,而不是前者,所以提交不一定是任何特定的“开发线”)。
首先,绘制图表的方式存在一个小问题:
*------*---*
Master \
*---*--*------*
A \
*-----*-----*
B (HEAD)
这是完全相同的图形,但标签以不同方式绘制,并添加了一些更多的箭头(我在下面使用了提交节点编号):
0 <- 1 <- 2 <-------------------- master
\
3 <- 4 <- 5 <- 6 <------ A
\
7 <- 8 <- 9 <-- HEAD=B
为什么这很重要的是git对于提交“在某个分支上”的含义是相当松散的 - 或者更好的一句话就是说某些提交“包含在”某些分支中。提交不能移动或更改,但分支标签可以并且确实移动。
更具体地说,像master
,A
或B
这样的分支名称指向一个特定的提交。在这种情况下,master
指向提交2,A
指向提交6,B
指向提交9.前几个提交0到2包含在所有三个分支中;提交3,4和5包含在A
和B
中; commit 6仅包含在A
中;并且提交7到9仅包含在B
中。 (顺便提一下,多个名称可以指向相同的提交,这在您创建新分支时是正常的。)
在我们继续之前,让我再一次重新绘制图形:
0
\
1
\
2 <-- master
\
3 - 4 - 5
|\
| 6 <-- A
\
7
\
8
\
9 <-- HEAD=B
这只是强调它不是一个重要的提交水平线,而是父/子关系。分支标签指向一个开始提交,然后(至少绘制这些图形的方式)我们向左移动,也可能根据需要向上或向下移动,以查找父提交。
重新提交提交时,实际上是在复制这些提交。
对于任何提交(或者实际上是git存储库中的任何对象),都有一个“真实的名称”,即SHA-1:例如9f317ce...
中的git log
这样的40-hex-digit字符串。 SHA-1是对象内容的加密1校验和。内容是作者和提交者(姓名和电子邮件),时间戳,源树和父提交列表。提交#7的父代始终是#5。如果您制作了提交#7的大致精确副本,但将其父级设置为提交#2而不是提交#5,则会获得具有不同ID的不同提交。 (此时我用完了一位数 - 通常我使用单个大写字母来表示提交ID,但是使用名为A
和B
的分支我认为这会让人感到困惑。所以我会打电话给#7的副本,# 7a,下面。)
git rebase
does当你要求git重新提交一系列提交时 - 例如上面的提交#7-8-9-它必须复制它们,至少如果它们要移动到任何地方(如果它们不移动它可以只是离开原件到位)。它默认从当前签出的分支复制提交,因此git rebase
只需要两条额外的信息:
当你运行git rebase <upstream>
时,你让git从一条信息中找出这两个部分。当你使用--onto
时,你可以分别告诉git这两个部分:你仍然提供upstream
,但它不会从<upstream>
计算目标,它只计算从<upstream>
复制的提交。 (顺便说一句,我认为<upstream>
不是一个好名字,但它是rebase使用的,我没有更好的方法,所以让我们坚持下去.Rebase调用目标<newbase>
,但我认为目标是一个更好的名字。)
我们先来看看这两个选项。两者都假设你首先在分支机构B
上:
git rebase master
git rebase --onto master A
在第一个命令中,<upstream>
的rebase
论证是master
。第二个是A
。
以下是git计算哪些提交要复制:它将当前分支交给git rev-list
,它还将<upstream>
交给git rev-list
,但是使用--not
- 或者更准确地说,使用相当于两点exclude..include
表示法。这意味着我们需要知道git rev-list
是如何工作的。
虽然git rev-list
非常复杂 - 大多数git命令最终都使用它;它是git log
,git bisect
,rebase
,filter-branch
等的引擎 - 这个特殊情况并不太难:使用双点表示法,rev-list
列出了从右侧可以访问的每个提交(包括该提交本身),不包括每个提交都可以从左侧到达。
在这种情况下,git rev-list HEAD
发现所有可以从HEAD
访问的提交 - 即几乎所有提交:提交0-5和7-9-并且git rev-list master
查找从master
可以访问的所有提交,即提交#0,1和2。减去从0-5,7-9的0到2离开3-5,7-9。这些是git rev-list master..HEAD
列出的复制候选提交。
对于我们的第二个命令,我们有A..HEAD
而不是master..HEAD
,所以减去的提交是0-6。提交#6没有出现在HEAD
集中,但是没关系:减去那些不在那里的东西,不在那里。因此,所得到的候选人复制品是7-9。
这仍然让我们弄清楚了rebase的目标,即复制提交应该在哪里登陆?使用第二个命令,答案是“由--onto
参数标识的提交”。由于我们说--onto master
,这意味着目标是提交#2。
git rebase master
但是,使用第一个命令,我们没有直接指定目标,因此git使用<upstream>
标识的提交。我们给的<upstream>
是master
,指向提交#2,所以目标是提交#2。
因此,第一个命令将从复制commit#3开始,只需要进行任何最小的更改,以便其父进程为#2。它的父母已经提交#2。没有什么必须改变,所以没有任何改变,并且rebase只是重新使用现有的#3提交。然后必须复制#4以使其父级为#3,但父级已经是#3,所以它只是重新使用#4。同样,#5已经很好了。它完全忽略#6(不在复制的提交集中);它会检查#s 7-9,但它们都很好,所以整个rebase最终只是重新使用所有原始提交。你可以使用-f
强制复制,但你没有,所以整个rebase最终什么都不做。
git rebase --onto master A
第二个rebase命令使用--onto
选择#2作为其目标,但告诉git只复制提交7-9。提交#7的父级是提交#5,所以这个副本实际上必须做某事.2所以git做了一个新的提交 - 让我们调用#7a - 它将#2作为其父提交。 rebase继续提交#8:副本现在需要#7a作为其父级。最后,rebase继续提交#9,需要#8a作为其父级。复制完所有提交后,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
。
在第一组命令中,您在分支git rebase master
上运行B
。如上所述,这是一个很大的无操作:因为没有什么需要移动,git根本没有复制(除非你使用-f
/ --force
,你没有)。然后你检查了master
并使用了git merge B
,如果它被告知4 - 用合并创建一个新的提交。因此,Dherik's answer,至少在我看到它的时候,在这里是正确的:合并提交有两个父母,其中一个是分支B
的尖端,并且该分支通过三个提交回到分支A
,因此一些关于A
的内容将被合并到master
。
使用你的第二个命令序列,你首先检查了B
(你已经在B
所以这是多余的,但是git rebase
的一部分)。然后你使用rebase复制三次提交,生成上面的最终图,提交7a,8a和9a。然后你检查了master
并与B
进行了合并提交(再次参见脚注4)。再次Dherik的回答是正确的:唯一缺少的是原始的,被抛弃的提交没有被引入,并且新的合并提交不是明显的。
1这只是因为定位特定校验和非常困难。也就是说,如果你信任的人告诉你“我信任ID 1234567的提交...”,对于其他人 - 你可能不信任的人 - 几乎不可能提出具有相同ID的提交,但是有不同的内容。偶然发生这种情况的可能性是2160年1,这比你在被海啸袭击时被海啸淹没时受到闪电击中而心脏病发作的可能性要小得多。 :-)
2使用等效的git cherry-pick
进行实际复制:git将提交树与其父树进行比较以获得差异,然后将差异应用于新父树。
3实际上,此时确实如此:git rebase
是一个解析您的选项的shell脚本,然后决定运行哪种内部rebase:非交互式git-rebase--am
或交互式git-rebase--interactive
。在找出所有参数后,如果有一个剩余的分支名称参数,脚本在启动内部rebase之前执行git checkout <branch-name>
。
4既然master
指向提交2并且提交2是提交9的祖先,那么通常不会进行合并提交,而是执行Git所谓的快进操作。你可以指示Git不使用git merge --no-ff
快速前进。一些接口,例如GitHub的Web界面和可能的一些GUI,可能会分离不同类型的操作,因此它们的“合并”会强制实现这样的真正合并。
通过快进合并,第一种情况的最终图表是:
0 <- 1 <- 2 [master used to be here]
\
3 <- 4 <- 5 <- 6 <------ A
\
7 <- 8 <- 9 <-- master, HEAD=B
在任何一种情况下,提交1到9现在都在两个分支上,master
和B
。与真正的合并相比,差异在于,从图表中,您可以看到包含合并的历史记录。
换句话说,快进合并的优点是它不会留下什么是微不足道的操作的痕迹。快进合并的缺点是它没有留下任何痕迹。因此,是否允许快进的问题实际上是一个问题,即是否要在提交形成的历史中留下明确的合并。
在任何给定的操作之前,您的存储库看起来像这样
o---o---o---o---o master
\
x---x---x---x---x A
\
o---o---o B
在标准的rebase(没有--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
的rebase将创建以下更清晰,更简单的结构:
o---o---o---o---o master
| \
| o'--o'--o' B
\
x---x---x---x---x A
差异:
git rebase master
*---*---* [master]
\
*---*---*---* [A]
\
*---*---* [B](HEAD)
没啥事儿。自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]
我想将我的B更改(并且只有我的B更改,没有A更改)合并到master中
小心你对“只有我的B改变”的理解。
在第一组中,B
分支是(在最终合并之前):
*---*---*
\
*---*---*
\
*---*---* [B]
在第二组中你的B分支是:
*---*---*
|
|
|
*---*---* [B]
如果我理解正确,你想要的只是不在A分支中的B提交。因此,第二组是合并前的正确选择。
在每个git命令之后可以使用git log --graph --decorate --oneline A B master
(或等效的GUI工具)来可视化更改。
这是存储库的初始状态,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 $1 $2); 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.
检查qazxsw poi并合并qazxsw poi只需将qazxsw poi指向master
(即B
)。没有创建新的提交。
master
第二个rebase命令复制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
。副本是具有自己的提交ID的新提交; A..B
,master
和9a90b7c C9, 2968483 C8, and 187c9c8 C7
。分支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
你可以亲自试试看看。您可以创建一个本地git存储库来玩:
7c0e241