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