合并冲突如何在提交历史中保持未解决状态?

How can merge conflicts remain unresolved in commit history?

我最近通过以下方式浏览项目的提交历史:

  1. git rebase -i --root
  2. 选择每个提交进行编辑(但仅查看它们;不编辑它们)
  3. git rebase --continue 查看后每次提交,仅此而已

从其中一个提交继续后,发生了合并冲突。我认为当发生合并冲突时,需要先解决(通过编辑或跳过)才能应用合并。但是,这已经是项目提交历史的一部分,它远远超过了我当前正在进行的提交,所以我假设确实发生了某种合并。

提交如何在没有解决冲突的情况下通过这一点?这种事情如何以及何时会发生?

I was recently browsing through a project's commit history [using git rebase]

git rebase 所做的是 copy 提交(对新的,可能是改进的)。提交 历史,因此通过复制提交,您正在创建新的(并且可能是改进的)历史。

After continuing from one of the commits, a merge conflict occurred.

这很正常:这意味着复制 next 提交的尝试无法继续,因为此时在您新的和改进的提交系列中合并冲突。

I thought that when merge conflicts occur, they need to be resolved (via editing or skipping) before the merge can be applied.

他们有。但是请注意,作为 ,您真正需要做的只是 标记 冲突已处理。 Git 相信你,即使你没有在这里做任何事情。无论您作为解析文件放入 Git 的索引中,Git 都假定这是正确的合并结果。

However, this was already part of the project's commit history which goes well past the commit I'm currently on, so I assume some sort of merge did happen.

可能会,也可能不会。这取决于您如何重新排列旧提交以进行新的和改进的提交。

如果这个特定的合并冲突确实之前发生过,您可以使用以前合并的已解决文件来解决新的和改进的提交的合并冲突。这通常涉及使用 git checkout(pre-Git-2.23)或 git restore(Git 2.23 或更高版本)从原始合并中提取感兴趣的文件,尽管这需要注意具体取决于您所做的改进可能是什么。

然而,在这种特殊情况下,这些提交很可能是 git rebase 处理复制合并提交的默认方法的结果:根本不复制它们。也就是说,如果我们的历史是这样的:

     C--D
    /    \
A--B      G--H   <-- main
    \    /
     E--F

(总共八次提交),我们 运行:

git rebase -i --root

我们将得到一条指令 sheet,其中包含 七个 ,而不是八个 pick 命令。这些将按顺序用于提交 AB,然后是 CDE 和 [=126= 中的 F ]some order—rebase 使用 --topo-order 因此我们可以预测 CD 将按该顺序组合在一起,而 EF 按此顺序放在一起,但这可能是 C-D-E-FE-F-C-D——然后提交 HMerge commit G 完全省略了。

如果合并提交 G 必须解决冲突,那么在选择 C-DE-F 中的至少一个时,几乎肯定会至少有一个冲突,以较晚者为准在指令中 sheet。这实际上与 G 中需要解决的冲突类型相同。但是即使 G 没有,我们也会在这里遇到冲突:细节相当复杂。

在任何情况下,最终结果,在完成完整的变基之后,看起来很像这样,或者可能与稍后的 C-D 对:

A--B--C--D--E--F--H   <-- main

但是由于其中一些提交与原始提交不同,因此它们将具有不同的哈希 ID。 (要注意这一点,您需要仔细注意 变基之前 的哈希 ID,然后将它们与变基后 后的哈希 ID 进行比较。 ) 如果上面的的结果,而CD由于cherry-picking不需要任何改变,1 我们将有:

A--B--C--D--E'-F'-H'  <-- main

E' 的快照可能与 E 的快照不同; E' 的父链接转到 D,而不是 B;所以 E' 必然是一个 不同的 ——新的和改进的——提交。

git rebase期间使用-f(或--force-rebase--no-ff,所有这些都只是替代拼写)强制Git复制那些提交可以原封不动地重新使用。也就是说,结果将是:

A'-B'-C'-D'-E'-F'-H'  <-- main

rebase 命令最近学习了使用 git rebase --rebase-merges(shorthand:git rebase -r)“复制”merge 提交的能力。这提出了一个相当复杂的指令 sheet,包含标签和“转到”构造和合并指令。它通过 重新执行 合并来工作,因为不能像 git cherry-pick 复制非合并提交那样复制合并。使用此选项,您将能够保留图形结构,而不是展平合并。


1这就是 -f 选项的用武之地,真的。字面上的 cherry-pick 总是会产生一个新的提交,带有一个新的日期和时间戳,因此会有一个新的哈希 ID。 Rebase 巧妙地计算出是否 需要 新提交,如果不需要,则将旧提交快进到位,而不是 运行ning cherry-pick。使用 -f 可以防止这种巧妙的做法,这在为以后重新合并而强制重新复制主题分支时很有用。


旧提交会怎样?

如果你替换了一些提交,那么你的新 main 指向新的和改进的提交 H' 而不是旧的和糟糕的 H ...旧提交会怎样?

答案是:没有发生在他们身上。它们仍然存在,在您的 Git 存储库中。您可以在名称 ORIG_HEAD 下找到它们,至少在更新该名称之前是这样。或者,您可以使用分支机构的 reflog 或 HEAD reflog 找到它们,至少在这些 reflog 条目过期之前。 (找到它们的另一种方法是记住它们的哈希 ID。但我敢打赌你没有。)

一旦 reflog 条目过期并且不再有办法 找到 原始提交,它们将被 Grim Reaper 收集...呃, Grim Collector 呃嗯垃圾收集器,git gc。在普通存储库中,这些提交至少有 30 天的宽限期才能被收集。

你应该怎么做

与其使用 git rebase 查看历史提交,不如将它们一项一项地检查出来。要获取所有提交哈希 ID 的列表,请使用 git rev-list,例如:

git rev-list HEAD > /tmp/allrevs

然后你可以,例如,使用 sh/bash 结构:

exec 3< /tmp/allrevs
while read hash; do
    git switch --detach --quiet $hash
    echo viewing $hash
    sh 3<&-
done <&3
exec 3<&-

这不会有任何重新合并问题,尽管尽早 退出 很烦人(需要 非常 快速 Ctrl+C 退出子 shell 后)。要解决这个问题,请在 sh 行之后做一些巧妙的事情。注意:exec 3< 用于处理非常大的哈希列表;对于较小的列表,不那么烦人的:

for hash in $(cat /tmp/allrevs); do ... done

construct 工作正常,不需要奇怪的重定向技巧。