合并和变基是根本不同的操作。我所指的合并——也就是通过git merge
创建一个新的合并提交——确实会在提交记录中搜索最近的公共提交:
...--o--*--o--o--A <-- mainbr
\
B--C--D--E <-- sidebr
在这里,最近的共同提交是*
。合并过程将会比较(如git diff
)提交*
和提交A
,以找出“我们做了什么”,并将*
与提交E
进行比较,以找出“他们做了什么”。然后它创建一个新的单一合并提交,有两个父提交:
...--o--*--o--o--A---M <-- mainbr
\ /
B--C--D--E <-- sidebr
这里的操作将两个历史记录合并起来,结合了“我们做了什么”和“他们做了什么”,因此diff *
vs M
会得到“每个更改中的一个”。
请注意,在这里您无法选择合并基础:Git会自动解决这个问题。
另一方面,rebase可以单独指定要复制的提交和复制它们的位置。默认情况下,它会再次找到提交*
1,然后使用git cherry-pick
或等效方法逐个复制原始提交;最后,它会将分支标签移动到指向最后一个复制的提交。
...--o--*--o--o--A <-- mainbr
\ \
\ B'-C'-D'-E' <-- sidebr
\
B--C--D--E
原始的提交链(在这种情况下是B-C-D-E
)仍然存在于仓库中,可以通过哈希 ID 和 sidebr
的 reflog 找到它们。如果任何其他分支或标签名称使它们可达,则它们仍然可以通过该名称访问。
git merge --squash
做的是略微修改合并流程:Git 不再创建合并提交M
,而是像往常一样通过合并机制进行,比较基准提交(您不能选择)与当前提交和另一个提交之间的差异,并将索引和工作树中的更改组合起来。然后出于没有明显的原因2停止并让您运行git commit
,以便提交结果,当您这样做时,它是一个普通的、非合并提交,因此整个图形片段看起来像这样:
...--o--*--o--o--A--F <-- mainbr
\
B--C--D--E <-- sidebr
现在,合并所产生的快照树
内容(提交的内容)与我们进行真正合并时提交的内容相同,这甚至是再次平滑化时提交的内容的相同之处。
此外,假设侧支sidebr上只有一个提交(B)。现在,merge, merge--squash 和 rebase 都将给您提供以以下方式绘制的图片:
<code><code><code>...--o--*--o--o--A--B' <-- ???
\ ?
B????????? <-- ???
</code></code></code>
对于git merge
、git merge --squash
和git rebase
,它们的最终快照B'都是相同的。然而,对于git merge
,新提交将在分支mainbr
上,并指向提交A
和B
,而sidebr
将指向B
;对于git merge --squash
,新提交将在mainbr
上,并只指向A
;对于git rebase
,新提交将在sidebr
上,没有明显的指向B
的内容,我们可以这样表示:
...--o--*--o--o--A <-- mainbr
\ \
B B' <-- sidebr
由于mainbr
将继续指向提交A
,因此最终看起来更像是合并而不是变基。(但是,如果它根本不被称为“压缩合并”,我会更高兴。)
1Git查找*
的方法略有不同: 它实际上不是单个提交,而是一组(通常非常大的)提交中的最后一个,即所有可从<upstream>
参数到达的提交,这是git rebase
的参数。(令人困惑的是,合并基也可以是一组提交,但它是一个更受限制的集合。最好先不要深入研究图论。:-))
2如果我们希望它停止,我们可以像对待常规的非压缩git merge
一样使用--no-commit
。那么为什么它会自动停止呢?
merge --squash
仅将分支中的所有提交压缩为一个提交。它不会修改历史记录。 - Andy Ray