有没有一种方法可以在不产生新提交的情况下与“ours”策略合并?

4
我需要做的是关闭一个分支,并让另一个分支的尖端(最后一次提交)看起来像与该分支合并,但实际上不改变其内容。我尝试过。
git merge -s ours other_branch --squash

但是什么也没发生(在我读了squash的实际作用之后就有意义了)

也就是说,在命令之前:

  * other_branch
 / 
*---* HEAD

命令执行后的预期结果

  * other_branch
 / \
*---* HEAD

注意:在这里所说的“内容”是指已提交的东西:由于我要执行的操作实际上是向提交添加一个父项,因此元数据会发生变化;我知道这会修改历史记录,并且至少会影响shasum。

1
你能添加一张图吗?也许这是一个不好的问题,但您能告诉我们为什么普通合并无法产生期望的结果吗? - Tim Biegeleisen
目标不是创建两个完全相同的提交。 - ilmirons
2
git reset --soft $(git log --format=%B -n 1 | git commit-tree HEAD^{tree} -p HEAD^ -p other_branch) - user4003407
1个回答

3

PetSerAl的评论提供了一个(或者至少是)你可以使用的命令来实现你想要的。唯一缺少的是关于为什么的解释:

git reset --soft $(git log --format=%B -n 1 | \
    git commit-tree HEAD^{tree} -p HEAD^ -p other_branch)

工作。稍后我会详细解释,但首先...

一个直接的回答

然而,根据您提出的问题,直接回答是“不可以”。由于主题行可以被编辑掉,让我引用一下:

有没有一种方法可以在不产生新提交的情况下与“ours”策略合并?

在Git中,“合并”这个词可以指两件事情:合并的行为或以前合并行为的结果。第一个是动词,“合并”;第二个是形容词,“合并提交”,甚至是名词,“合并”。合并提交是具有两个父级(或更多,但我们只关心两个)的提交。

运行git merge --squash告诉Git执行合并即动词部分,但在最后创建一个普通的非合并提交即取消合并作为形容词的效果。1这样做的目的是当您有一系列提交时,例如以“分支”方式存在,即不仅仅是“指向特定提交的名称”(请参见我们所说的“分支”究竟是什么?)。
运行git merge -s ours告诉Git执行虚假合并操作,即假装执行合并即动词,但实际上不会做任何事情,从而产生合并作为形容词/名词的提交。由于动词形式消失了,唯一剩下的就是后效。这就是在此处使用--squash无用的原因:您建议消除动词和名词,没有留下任何东西。

1出于没有特别好的原因,git merge --squash 的副作用是同时设置了 --no-commit 参数,因此您必须在最后手动运行 git commit。请注意,真正的合并(生成合并提交)也可以使用 --no-commit 运行,这样您也必须手动运行 git commit;或者它可能因冲突而停止,这时您必须手动完成提交。Git 的最新版本添加了 git merge --continue 以使此过程感觉不那么尴尬,但它只是运行了 git commit


Git有--amend,但它并不能完全满足我们的需求

这张图展示了你想要的内容——如果Git支持的话,可能会被称为git commit --amend --add-parent,但它并没有提供关键的洞察力,实际上git commit --amend是一个小谎言,或者说是一个巨大的谎言,因为它并没有改变提交,只是创建了一个使用不同父哈希的新提交。

正常的git commit过程将执行一堆步骤,就好像它们都一次性完成一样,所以要么它们全部正确完成,要么什么也不会发生(或者至少看起来是这样)。这些步骤包括:

  1. 获取当前提交的哈希值(git rev-parse HEAD)。将其称为P:现有的HEAD将成为新提交的父级。(如果您正在结束合并,则MERGE_HEAD也将存在,并且git commit会读取它以获取其他父级。不过这只是用于完成合并。)
  2. 将当前索引写入以创建一个tree对象(git write-tree)。将其称为T,即树的哈希值。(这可能与先前某个提交的树相同,也可能不同。)
  3. 获取您的姓名、电子邮件地址和当前时间作为提交者。通常,将其用作作者(您可以覆盖它们)。
  4. 获取提交消息。
  5. 使用所有这些信息(tree Tparent P、在步骤3中获取的authorcommitter,以及在步骤4中获取的提交消息)编写一个新的提交。结果是一个新的提交哈希值C
  6. 哈希C写入当前分支名称,以便git rev-parse HEAD现在产生C
使用git commit --amend会改变步骤1的程序:Git不再将HEAD作为父提交,而是读取当前提交的父哈希值(如果您正在--amend合并,则可能有多个),并在步骤5中使用它们。
其效果是将当前提交推到一旁。
...--o--o--*   <-- master (HEAD)

变成:

          *   [the commit that was HEAD before]
         /
...--o--o--@   <-- master (HEAD)

你想让Git做的事情有点不同。
为什么(以及如何)shell命令起作用
Git的commit-tree命令会生成新的提交对象。就像上面六步提交序列中的第5步。但它没有创建树状结构,并且还没有预先计算好的父提交哈希值,因此需要将其作为命令行参数传递:git commit-tree tree-hash -p parent-hash-1 -p parent-hash-2
在这种情况下,我们想要的tree-hashgit merge -s ours相同,即当前提交所具有的相同树。我们可以使用HEAD^{tree}来命名该树, gitrevisions文档中有相关描述。我们想要的两个父哈希值始于当前提交的父级。 (我们可以假设只有一个这样的父级。)再次,gitrevisions语法为我们提供了一种编写此内容的方法:我们可以使用parent^1parent~1,或从这些表达式中省略1。我们想要的另一个父哈希是指向other_branch的提交,因此我们只需命名即可。 这给了我们:
git commit-tree HEAD^{tree} -p HEAD^ -p other_branch

这个命令从标准输入中读取提交的信息。如果我们想保留当前提交的消息,可以使用git log提取它:--format=%B告诉git log将每个提交作为文本打印出来,并且-n 1告诉git log只显示一个提交。默认情况下,git log显示的第一个提交是HEAD提交。因此,这给了我们:
git log --format=%B -n 1 |

部分——我们将git log的标准输出管道传输到git commit-tree的标准输入。

git commit-tree在创建提交后会将其哈希ID打印到自己的标准输出中。因此,如果我们仅运行此管道本身,我们将看到新的提交哈希打印出来,但我们不会将其存储在任何地方。我们需要做的是更改当前分支名称(无论它是什么)以指向新提交;git reset --soft commit-hash将完成此操作,因此:

git reset --soft $(...)
$(...) 结构是最后一部分:shell 将其解释为“运行给定的命令,捕获其标准输出,然后将该标准输出文本视为 git reset --soft 命令的参数。”由于只有一个输出词——新提交的哈希值——因此会在新的提交 ID 上运行 git reset --soft

第一步还要读取MERGE_HEAD(如果存在)以获取合并提交的其他父提交。 - user4003407
@PetSerAl:没错,我应该把它包含在我的列表中(或进行编辑)。顺便说一句,感谢您指出的拼写错误,今天早上我很匆忙... - torek
感谢 @torek 的出色回答!对于 Windows 用户的一个小提示:显然在“cmd”终端中,当答案中提到“^”时,我们需要键入“^^”。 - 62mkv
@62mkv:是的,我已经读到了在cmd.exe中,“^”是引用字符,有点像sh/bash中的“\”。我不确定这个比喻是否正确,因为我不使用Windows(当我被迫使用时,我安装了Cygwin,在00年代后期)。 - torek

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