这里值得注意的是git merge和git merge --squash密切相关,但git merge --squash不会创建合并。
这里的措辞很重要,尤其是“merge”前面的冠词“a”:“a merge”是名词,“to merge”是动词。两个命令都执行合并操作。区别在于如何保存结果。
还值得以提交图的形式快速提醒一下合并的外观。这里的每一轮o 节点代表一个提交,较早的提交指向左侧。分支名称是指向一个特定提交的标签(分支的 tip 提交)。例如,您可以从这个开始:
...--o--*--o--o <-- main
\
o--o--o <-- feature
然后您决定将一个特定的 feature 分支合并回 main 分支,因此您运行:
$ git checkout main && git merge feature
这会合并(动词)和合并(名词),结果如下:
...--o--*--o--o---o <-- main
\ /
o--o--o <-- feature
Git 向main 添加了一个新的提交,这个新的提交是一个合并提交:它既指向main 的前一个提示,也指向(在这个情况下,也向下)到feature 的当前提示。 (名称feature 继续指向与之前相同的提交。)
git merge --squash 所做的是修改最后一步。它没有进行合并提交,而是完全禁止提交——没有明显的原因——并强制你运行git commit。当您确实运行git commit时,这会进行普通提交,而不是合并提交,因此结果如下所示:
...--o--*--o--o---o <-- main
\
o--o--o <-- feature
这里有两个关键项:
新提交的内容是一样的。两个新的提交都是由索引生成的(索引是 Git 的术语,意思是“你下一次提交的内容”)。该索引是由作为动词的合并过程设置的。 first 父级是“主线”提交,来自我们在进行合并时所在的分支。 second 父级是另一个提交,即我们刚刚合并的那个。
新提交的父链接不同。真正的合并有 both 个之前的提交作为其父级,但“壁球合并”只有 一个 之前的提交作为其父级 - “主线”提交。这意味着它不会——它不能——记住合并了哪个提交。
在发生冲突(但真实的)合并的情况下,git merge 无法自行进行新提交,因此它会停止并强制您解决冲突。解决完这些冲突后,您必须手动运行 git commit,就像您必须始终(没有明显原因)对 git merge --squash 及其虚假合并执行的操作一样。您还可以使用git merge --no-commit 请求任何真正的合并停止并让您检查结果。
这导致了一种简单的方法将真正的合并变成假的(壁球)合并,只要真正的合并尚未提交:
要让git commit 知道进行合并,它依赖于冲突(或--no-commit)合并留下的文件。此文件名为.git/MERGE_HEAD。 (它还会留下一个名为 .git/MERGE_MSG 的文件,尽管这个额外的文件是无害的。)
因此,您可以简单地删除 .git/MERGE_HEAD 并运行git commit。 (您可能还想删除 MERGE_MSG 文件,一旦您用它的消息编写了提交。在此之前,您可以将它作为消息使用,或者作为它的起点。)@987654348 @step 将不再知道进行合并提交,而是进行普通提交——瞧,你已经进行了壁球合并。
如果您已经进行了真正的合并提交,该过程可能会更难。特别是,如果您已发布该合并,您现在必须让所有获得此合并的人将其收回。否则真正的合并将返回(他们可能会再次合并),或者给他们带来问题,甚至两者兼而有之。但是,如果您可以做到这一点,或者如果它没有被发布,那么您只需要“将合并推到一边”,然后放入一个虚假的合并提交。
让我们重新绘制已有的内容,腾出一些空间。这是和以前一样的图表,只是展开到更多的线上:
...--o--*----o----o
\ \
\ o <-- main
\ /
o--o--o <-- feature
如果我们可以将main 移回顶行,然后进行新的提交怎么办?好吧,我们会得到下面的图:
...--o--*----o----o--o <-- main
\ \
\ o [abandoned]
\ /
o--o--o <-- feature
请注意,所有的箭头——包括提交用来记录历史的内部提交箭头(此处未显示,因为它们太难在文本图中生成)——指向向左,所以有在任何最重要的提交中,都不知道它们下面的任何提交:这些都是“在他们的右边”。只有最底层的提交,加上我们将要放弃的那个,对最顶层的提交一无所知。
此外,如果我们要完全放弃中间线提交,让我们停止绘制它,以及它的两个合并箭头:
...--o--*----o----o--o <-- main
\
\
\
o--o--o <-- feature
看一下:我们刚刚为那些假壁球“合并”之一绘制了我们想要的提交图。这正是我们想要的,只要我们在新的提交中获得正确的内容。
但是——这是一种练习;在开始之前看看你是否知道答案——新提交的内容从何而来?答案在上面的第一个要点中,用粗体字表示。 (如果您忘记了,请返回查看。)
现在您知道git commit 使用索引进行新提交,让我们考虑一下我们现在拥有的真正合并提交。它有内容。 (所有提交都有内容。)它们来自哪里?他们来自索引!如果我们能够以某种方式将这些内容重新放入索引中,那么我们就完美了。事实上,我们可以:我们所要做的就是签出那个提交,git checkout,或者已经把它作为当前提交。
由于我们刚刚进行了新的合并提交,因此我们已经将合并提交作为当前提交。索引是干净的——如果我们运行git status,它表示没有什么可提交的。所以现在我们使用git reset --soft 来重置 分支指针main 后退一步,得到我们的中间绘图。 --soft 参数告诉 Git:“移动分支指针,但 不要 更改索引和工作树。”所以我们仍然会拥有来自原始合并的索引(和工作树)。现在我们只需运行git commit 进行普通提交,提供一些适当的提交消息,我们就完成了:我们有一个壁球合并。最初的合并现在被放弃了,最终——默认情况下,在 30 天后的某个时间——Git 会注意到它不再被使用并将其删除。
(要后退一步,您可以使用HEAD~1 或HEAD^;两者的含义相同。因此命令序列只是git reset --soft HEAD^ && git commit,假设当前提交是您希望替换的真正合并一个虚假的合并。)
即使您进行了多次合并提交,也可以使用上述更长的方法。但是,您必须决定是要多个假合并还是一个大的假合并。例如,假设你有这个:
...--o--o---o--o--o <-- develop
\ \ / /
\ o--o / <-- feature1
\ /
o----o <-- feature2
你想让你的最终照片看起来像:
...--o--o---o--o--o <-- develop
\ \
\ o--o <-- feature1
\
o----o <-- feature2
develop 上的最后 两个 提交是两个假(壁球)合并,还是你只想要一个大的假壁球:
...--o--o---o--o <-- develop
\ \
\ o--o <-- feature1
\
o----o <-- feature2
最后一次提交是最终结果?如果您想要 两个 假压缩,则需要保留两个原始合并提交足够长的时间以将它们放入索引并从中进行两次普通提交。如果你只想要最终合并的一个大的假壁球,那只需要两个 Git 命令:
$ git reset --soft HEAD~2 && git commit
因为我们将保留 second 合并的索引,所以向后移动 两个 步骤,然后进行将 both 合并推到一边的新提交。
同样,重要的是,没有一个真正的合并已经发布——或者,如果它们已经发布,你要说服所有捡到它们的其他人停止使用它们。