我可以压缩与其前任之一的合并提交吗?

Can I squash a merge commit with one of its predecessors?

假设我从这样的历史开始:

A---B---C
 \
  \-D---E

然后,我合并了两个分支,解决了过程中的一些冲突:

A---B---C---F
 \         /
  \-D---E-/

但之后,我想重写历史,使其看起来像这样:

A---B---C---EF
 \         /
  \-D-----/

换句话说,我想将更改从分支提交 E 移动到合并提交 F 中。就好像我在 E 中进行了更改,同时手动解决了将 D 合并到 C 的冲突。

(我的实际原因是 E 只是一个合并准备提交,使预期的冲突更容易解决。它本身没有太大价值,我不想它污染了历史。我不想将 E 压缩到 它的 前身,因为它的变化在逻辑上是分开的。)

我试过了:

git rebase -i --rebase-merges HEAD~10

但这给了我:

...
pick 1069500 ...
merge -C 44c0a69 ...

# s, squash <commit> = use commit, but meld into previous commit
# ...
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.

所以看起来提交可以是合并提交, git rebase -i 可以将其压缩到其前身,但不能同时合并。而且 git rebasemake me reapply my manual conflict resolutions,无论如何,这有点臭。

还有其他方法吗?

我会这样做:

git checkout $( git commit-tree -p C -p D -m "some merge" F^{tree} )

你最终会在一个修订版上以分离的 HEAD 结束,就像你所请求的那样(检查 git 日志或 gitk,检查文件)......如果你喜欢它:

git branch-f some-branch
git checkout some-branch

TL;DR

使用 git commit-treeF 的快照和您选择的父级进行新的合并提交,然后强制您的分支名称指向那里:

newcommit=$(git commit-tree -p HEAD^ -p HEAD^2^ HEAD^{tree} -F /tmp/msg)

其中 /tmp/msg 包含您要使用的提交消息。然后使用 git reset 将当前分支/提交移动到 $newcommit,检查此提交是否符合您的要求(例如 git log --graph --oneline $newcommit)。

(这假设一个类 Unix shell,shell 变量设置为 var=value$(...) 到 运行 一个命令,等等.)

提交不存储更改。他们存储快照。

合并提交也是如此:它们的快照是快照。他们以每个文件的合并形式 保存每个文件的完整副本

所以假设你有:

A--B--C--F
 \      /
  D----E

(这是您的原始绘图,我只是将提交内容稍微推到我更喜欢的样式)并且想要:

A--B--C--G
 \      /
  D----'

(我只是在这里使用了一个全新的合并字母G,但这和你的EF是一样的)。合并提交 G 快照 必须与合并提交 F 的现有快照完全匹配。 merge commit G 的第一个 parent 必须是 commit C——与现有 commit F 的第一个 parent 相同——第二个 parent 必须是 commit D,跳过commit E,但最重要的是,Gsnapshot 应该与您已经使用 merge commit F 创建的快照完全相同。

现在,您可以轻松生成这个图:

        ___
       /   \
A--B--C--F  G
 \      /  /
  D----E  /
   \_____/

根据您已有的图表,通过进行新提交 Ggit commit-tree 命令正是这样做的。

您必须向 git commit-tree 提供三样东西:

  • 一组父哈希 ID,或解析为哈希 ID 的名称。您需要提交的 ID CD,您可以分别使用 HEAD^1HEAD^2^1 拼写它们。每个 1 都可以省略。这些是 -p 个参数。

  • 新提交的树哈希 ID。因为 HEAD 是提交 F 并且它的树是你想要的,所以 HEAD^{tree} 指定。

  • 提交日志消息,带有 -m-F 或来自标准输入。

提交树程序写出新的提交对象并打印其哈希 ID,我们希望使用 shell 变量以人类可读的形式捕获它。

一旦完成,我们只需要更新当前的分支名称。由于此分支当前已检出,我们将使用 git reset:

git reset $newcommit

我们可以 git reset --soft 避免更改当前索引,或 git reset --hard 更改索引和工作树,但如果当前索引和工作树与现有合并提交中的快照匹配F,合并 G 中的快照无论如何都完全相同,因此实际上不需要任何一个标志。默认 --mixed 重置将重置索引,使工作树不受干扰。 (不过,如果你仔细安排了一些东西,你可能想要 --soft。)

提交 F 将继续存在,但没有 name 找到它,您将看不到它——最终,一旦保留它的 reflog在过期前后,Git 的垃圾收集器将丢弃提交的 FE.