“git merge --squash”不就是“git rebase -squash”吗?

3
尝试理解命令的原因:
git merge --squash mybranch

不叫做

git rebase -squash mybranch

看起来它所做的操作更像是变基而不是合并。我的理解是,它会在提交树中向后搜索,直到找到当前分支和mybranch的共同基础提交。然后,它将从该基础节点到mybranch的头部的所有提交重新应用(变基)到当前分支的头部。但是,它会将这些更改应用到索引/工作区中,以便可以将其作为单个提交应用。完成后,没有合并节点,就像正常合并中显示了被合并的两个分支一样。我的理解正确吗?


1
merge --squash 仅将分支中的所有提交压缩为一个提交。它不会修改历史记录。 - Andy Ray
Andy的意思是,在合并之前进行压缩,因此从目标分支的角度来看,它仍然只看到一个合并。 - Tim Biegeleisen
1
我的观点是,在常规合并之后,图表显示被合并的两个分支合并为一个合并节点,但在合并--squash & commit之后,图表仍然显示两个分支是分开的,但是来自mybranch的更改已经压缩成单个提交到当前分支上。对我来说,这更像是将rebase压缩成单个提交。但我真正问的问题是,我的merge --squash的概念是否基本正确,通过@torek的扩展答案,我得出结论,尽管还有更复杂的细节需要学习。 - JonN
2个回答

4

合并和变基是根本不同的操作。我所指的合并——也就是通过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 mergegit merge --squashgit rebase,它们的最终快照B'都是相同的。然而,对于git merge,新提交将在分支mainbr上,并指向提交AB,而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。那么为什么它会自动停止呢?


0

概念上:有点像

历史上:不是

看起来,git merge --squash 是在提交 7d0c68871a (git-merge --squash, 2006-06-23) 中引入的。如果你检出这个提交并查看 git-rebase(1) 的文档,你会发现它没有提到交互式 rebase,包括“压缩”等操作;显然 git-rebase(1) 只用于在其他分支或提交之上重放提交,而不是重新排序、操作和压缩更改。

有点像

当我使用 git-rebase(1) 时,我的期望是某个分支将被更新,可能以一些非快进的方式。但是,git merge --squash 在概念上创建了一个新的提交,其中包含一个“压缩”的提交消息和一个与要包含的分支顶部相等的树。因此,你不会以某种非快进的方式更改任何分支;你保持其他分支不变,并创建一个新的提交,从概念上讲,它是挑选到应该合并到的分支上的。

注释

提交消息很好地激励了转换:
git-merge --squash
有些人倾向于在主题分支上做许多小提交,记录所有的尝试和错误,当主题相当成熟时,他们希望将系列的净效果作为一个提交记录到主线之上,从历史记录中删除无用信息。然后该主题被放弃或者再次从主线的那个点分叉。
核心 git 工具自带的裸骨 porcelainish 并不正式支持这种操作,但是当这样的主题分支不是严格的主线超集时,你可以通过使用 "git pull --no-merge" 来模拟它,就像这样:
git checkout mainline git pull --no-commit . that-topic-branch : 如果有冲突,请修复 rm -f .git/MERGE_HEAD git commit -a -m 'consolidated commit log message' git branch -f that-topic-branch ;# 现在完全合并
但是当主题分支是主线的快进时,这种方法不起作用,因为普通的 "git pull" 在这种情况下永远不会创建合并提交,而且 --no-commit 也无法开始任何特殊操作。
此补丁引入了一个新选项 --squash,以正式支持这种工作流程,在快进情况和真正的合并情况下都是如此。在两种情况下,用户级操作都是相同的:
git checkout mainline git pull --squash . that-topic-branch : 如果有冲突,请修复 - 自然地,如果快进,则不会有冲突。 git commit -a -m 'consolidated commit log message' git branch -f that-topic-branch ;# 现在完全合并
当当前分支已经与其他分支保持最新时,确实没有什么可做的,因此新选项没有任何效果。
这是最近在 #git IRC 频道中提出的问题。
签名:Junio C Hamano junkio@cox.net

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接