Git: 如何将现有的 `merge` 转换为 `merge --squash`?

Git: How to convert an existing `merge` to a `merge --squash`?

我执行了多次 merge 提交,但它们应该是 merge --squash。解决冲突花了一天多的时间,所以我不能手动重做合并。

有没有办法将 merge 转换为 merge --squash

这里值得注意的是git mergegit 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 添加了一个新提交,这个新提交是一个 merge 提交 :它指向前面的提示main,然后返回(在本例中,也是向下)到 feature 的当前尖端。 (名称 feature 继续指向与之前相同的提交。)

git merge --squash所做的就是修改最后一步。它没有进行合并提交,而是完全抑制提交——没有明显的原因——并迫使你 运行 git commit。当您 do 运行 git commit 时,这会生成 ordinary 提交,而不是 merge commit,结果如下:

...--o--*--o--o---o   <-- main
         \
          o--o--o   <-- feature

这里有两个关键项:

  • 新提交的内容相同。两个新的提交都是 从索引 做出的(索引 是 Git 对 "what goes into the next commit you make" 的术语)。该索引由 merge-as-a-verb 过程设置。 first 父级是 "main-line" 提交,来自我们进行合并时所在的分支。 second parent 是另一个提交,我们刚刚合并的那个。

  • 新提交的父链接不同。一个真正的合并有两个个以前的提交作为它的父项,但是"squash merge"只有一个个以前的提交作为它的父项——"main-line" 提交。这意味着它不会——它可以不会——记住哪个提交被合并了。

在发生冲突(但真实)合并的情况下,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 文件,一旦你用它的消息编写了你的​​提交。在那之前你可以使用它 作为 消息,或者作为它的起点。)git commit 步骤将不再知道进行合并提交,而是进行普通提交 - 瞧,你已经进行了挤压合并。


如果您已经进行了真正的合并提交,这个过程可能会更难。特别是,如果您已经发布了合并,您现在必须让所有获得此合并的人都收回。否则真正的合并会 return (他们很可能会再次合并),或者给他们制造问题,甚至两者兼而有之。但是,如果你能做到这一点,或者如果它 没有 被发布,你只需要 "shove the merge aside",就像它一样,并放入一个假的合并提交。

让我们重新绘制已有的内容,腾出一些空间。这是与之前相同的图表,只是分布在更多行上:

...--o--*----o----o 
         \         \
          \         o   <-- main
           \       /
            o--o--o   <-- feature

如果我们可以将 main 移回顶行,然后进行新的提交会怎么样?那么,我们会得到如下图:

...--o--*----o----o--o   <-- main
         \         \
          \         o   [abandoned]
           \       /
            o--o--o   <-- feature

请注意,所有箭头——包括提交用于记录历史的内部提交箭头(此处未显示,因为它们太难在文本图中生成)——指向向左,所以在任何顶行提交中都不知道它们下面的任何提交:这些都是 "to their right"。只有底线提交,加上我们将要放弃的提交,才知道顶线提交。

此外,如果我们要完全放弃中间线提交,我们就停止绘制它,以及它的两个合并箭头:

...--o--*----o----o--o   <-- main
         \
          \
           \
            o--o--o   <-- feature

看看那个:我们刚刚为其中一个假南瓜 "merge" 绘制了我们想要的那种提交图。这正是我们想要的,只要我们在新提交中获得正确的内容

但是——这是一种练习;在开始之前看看您是否知道答案—新提交的内容来自哪里?答案在上面的第一个要点中以粗体显示。 (如果您忘记了,请返回查看。)


既然您知道 git commit 使用索引进行新提交,让我们考虑一下我们现在拥有的真正的合并提交。它有内容。 (所有提交都有内容。)它们来自哪里?他们来自索引!如果我们能以某种方式将这些内容 返回到 索引中,我们就大功告成了。事实上,我们可以:我们所要做的就是检查那个提交,git checkout,或者已经把它作为当前提交.

由于我们刚刚进行了新的合并提交,我们已经将合并提交作为当前提交。索引是干净的——如果我们 运行 git status,它说没有什么可提交的。所以现在我们使用 git reset --soft 重置 分支指针 main 后退一步,以获得我们的中间绘图。 --soft 参数告诉 Git: "move the branch pointer, but don't change the index and work-tree." 所以我们仍然会有来自原始合并的索引(和工作树)。现在我们只需 运行 git commit 进行普通提交,提供一些适当的提交消息,我们就完成了:我们有一个压缩合并。原来的合并现在被放弃了,最终——默认情况下 30 天后的某个时间——Git 会注意到它不再被使用并将删除它。

(要后退一步,您可以使用 HEAD~1HEAD^;两者意思相同。因此命令序列只是 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

最后一次提交的最终结果在哪里?如果你想要 two fake-squashes,你将需要保留两个原始合并提交足够长的时间以使它们进入索引并从中进行两个普通提交。如果你只想要最终合并的一个大假南瓜,那只需要两个 Git 命令:

$ git reset --soft HEAD~2 && git commit

因为我们将保留 second 合并的索引,向后移动 two 步骤,然后进行推到一边的新提交 两者合并。

同样,重要的是 真实 合并中的 none 已经发布——或者,如果它们已发布,您要说服所有选择它们的人, 停止使用它们。