我可以用其中一个前身压缩合并提交吗?

4

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

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

然后,我合并了这两个分支,在此过程中解决了一些冲突:
A---B---C---F
 \         /
  \-D---E-/

但是之后,我想重写历史,使它看起来像这样:
A---B---C---EF
 \         /
  \-D-----/

换句话说,我想将分支提交 E 中的更改移动到合并提交 F 中。就像我在手动解决从将 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 rebase会使我重新应用手动冲突解决方法,这有点烦人。还有其他方法吗?
2个回答

3

简短概述

使用git commit-tree创建一个新的合并提交(commit),其中包含F的快照(snapshot)和您选择的父提交(parents),然后强制使您的分支(branch)指向该提交(commit):

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,用$(...)运行命令等)

Long

提交并不会存储更改。它们会存储快照。

这对于合并提交也是适用的:它们的快照就是快照。它们将每个文件的完整和完全的副本保存为每个文件的合并形式。

所以假设您有:

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

(这是您的原始绘图,我只是稍微调整了提交的风格,使其更符合我的喜好),并且希望有:
A--B--C--G
 \      /
  D----'

我刚在这里使用了一个全新的字母G来进行合并,但与您的EF相同。合并提交G的快照必须完全匹配现有合并提交F的快照。合并提交G的第一个父提交必须是提交C,与现有提交F的第一个父提交相同,并且第二个父提交必须是提交D,跳过提交E,但最重要的是,G的快照应该与您已经用合并提交F制作的快照完全相同。
现在,您可以轻松地生成图形:
        ___
       /   \
A--B--C--F  G
 \      /  /
  D----E  /
   \_____/

从您已经有的图表中,通过创建新的提交Ggit commit-tree命令可以完成此操作。

您需要向 git commit-tree 提供三个内容:

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

  • 新提交的树哈希ID。由于HEAD是提交F,其树是您想要的,因此HEAD^{tree}指定了它。

  • 提交日志消息,使用-m-F或从stdin获取。

commit-tree程序会写出新的提交对象并打印出其哈希ID,我们希望使用shell变量捕获其可读形式。

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

git reset $newcommit

我们可以使用 git reset --soft 来避免更改当前索引,或者使用 git reset --hard 来更改索引和工作树,但是如果当前索引和工作树与现有合并提交F中的快照匹配,那么合并 G 中的快照将完全相同,因此实际上没有必要使用任何标志。默认的 --mixed 重置将重置索引,保留工作树不受影响。(不过,如果你仔细地暂存了一些东西,那么你可能需要使用 --soft )。
提交F将继续存在,但是没有一个名称可以找到它,你将看不到它--最终,一旦保留它的reflog过期,Git的垃圾收集器将丢弃提交FE

为了确保,F^{tree}或者HEAD^{tree}仅指快照(仅文件),而F指整个提交(文件、信息、作者等),因此我们应该在git commit-tree命令后有${newcommit}^{tree} == F^{tree}。这样是正确的吗?您是一个非常好的解释者,谢谢您。 - leogama
1
@leogama:是的,就是这样:^{<type>} 后缀告诉修订解析代码,在定位插入符号之前的任何内容的哈希 ID 后,它应该尽力找到适合给定类型的哈希 ID。有些类型不会转换 - 例如,如果 ^{tree} 左侧的哈希是 blob 哈希,则 ^{tree} 会产生错误。但是提交始终只有一个树,因此当 F 是提交时,F^{tree} 每次都有效。 - torek
请注意,一些(主要是“瓷器”)Git命令会为您执行等效的操作,例如git diff,给定提交哈希指示符,将提交哈希转换为树哈希以进行差异比较。其他(主要是“管道”)Git命令则不会这样做,因此您需要一些语法或代码来获取正确类型的对象。 - torek

2
我会这样做:

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

如果你喜欢的话,你将会以分离头指针的形式停留在一个修订版本上(使用git log或gitk检查文件),就像你所请求的那样。

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

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