如何摆脱合并提交中的错误并保留正确的部分?

How to get rid of mistakes in a merge commit and keep the right parts?

有人不熟悉 git 在他的分支上提交,然后与 develop 分支进行合并提交。合并时,他:

  1. 通过完全重写解决了冲突
  2. 对几个可以合并而不会发生冲突的文件进行了更改
  3. 丢弃了本应自动合并的其他更改

现在我想保留1和2的部分,但要还原3rd的部分,怎么办?注意到他的分支已经被推送到远程所以我希望可以避免reset

我尝试过的:

  1. git revert <commit-id> -m 1 并回到合并前的提交
  2. 再次尝试合并,但被告知 'Already up to date.' 并且丢弃的更改仍然存在。

我在这里期待的应该和git reset head^; git merge develop一样,但我似乎没有正确理解revert

好的,我设法自己修复了它。我 post 万一幸运的人遇到类似情况的解决方案。

  1. develop 签出一个新分支,我们称之为 fix
  2. 将错误的分支合并到fix中,挑对的部分丢弃错误的部分
  3. 合并fix到故障分支因为我想保持分支干净

看起来很容易,为什么我要花那么多时间想出解决方案...?

这个特定问题没有正确答案。只有留下几个问题的答案,和留下很多问题的答案。每个问题的严重程度取决于您的具体情况:

  • 例如,使用 git reset 剥离合并,然后使用 git push --force,会给使用远程克隆的其他人带来问题。但也许只有 另一个人 在使用那个克隆,并且另一个人已经知道该做什么,或者可以被指示该做什么。

    在这种情况下,剥离错误的合并并重新开始的“坏处”相对较小,尤其是因为您可以保留良好的分辨率(尽管这需要手动工作和大量 Git 知识) .完成后,没有人需要再次处理错误的合并,这让一切都处于良好状态。

  • 但也许 很多 人正在使用那个远程存储库,并且删除错误的合并会造成无法弥补的损失。在那种情况下,剥离坏合并的“坏处”是巨大的,你应该使用另一种策略。

要记住的主要事情是,Git 存储库最终只不过是 提交 的集合。存储库中的提交 是历史记录 并且 是存储库 .1 所以,无论你最终如何这样做,您将 将提交添加到存储库 。要修复错误的合并提交,您必须添加更多提交。

这些不必是合并提交。您可以保留现有的合并,只需记住它(或将其标记为 git notes)为“错误” , 不使用”。然后您可以添加解决问题的普通(非合并)提交。

每个提交存储每个文件的完整快照。提交包含与上一次提交的差异。因此,错误的合并提交只是一些文件内容错误的提交。随后的非合并提交可以存储具有正确内容的文件。

因此,您的问题归结为两部分:

  • 你必须决定是否删除坏合并。这是一个价值判断,没有正确答案。

  • 您必须提出更正的内容。这是一个机械问题:您将如何产生正确的文件?在这里,Git 可以提供帮助。

让我把脚注移开,然后描述 Git 如何提供帮助。


1这是一种温和的夸大:可能有 git notes,尽管从技术上讲,它们无论如何都存储在提交和标签中;并且人类重视 b运行ch 名称,它们也在存储库中,但相当短暂,不应该如此依赖。


Git 如何执行真正的合并

在 Git 中,真正的合并是对 三个输入提交的操作。2 这三个提交包括您的当前提交,由您的当前 b运行ch 名称 和特殊名称HEAD 选择。你在命令行上给 Git 另一个提交:当你 运行 git merge <em>other-b运行ch-name</em> or git merge <em>hash-id</em>, Git用这个定位另一个b运行ch 提示 提交。有关 b运行ch tips 的工作原理以及 HEAD 的工作原理的更多信息,请参阅 Think Like (a) Git。该站点也将有助于理解下一部分。

鉴于这两个 b运行ch 提示提交,Git 现在找到第三个——或者在某种意义上,第一个——三个输入提交单独使用 提交图 。每个普通的非合并提交都向后连接到某个较早的提交。这一系列的后向连接最终必须到达某个共同起点,其中两个 b运行ches 最后共享某个特定提交。

我们可以这样画出这种情况:

          I--J   <-- our-branch (HEAD)
         /
...--G--H
         \
          K--L   <-- their-branch

我们最近的提交,我将其绘制为提交 J,向后指向一些较早的提交,我将其绘制为提交 I。他们最新的提交 L 指向一些较早的提交 K。但是 IK 向后指向某个提交——这里是 H——它同时在 both b运行ches. Think Like (a) Git 有很多关于它是如何工作的,但为了我们的目的,我们只需要看到 Git 自己找到提交 H,并且它在两个 b[=377 上=]ches.

当我们 运行 git merge 将提交 J 作为我们的提交时——Git 调用 --oursHEADlocal 提交——并提交 L 作为他们的提交——Git 称其为 --theirsremote提交,通常—Git 发现提交H 作为合并基础。然后它:

  1. 提交 H 中的快照与我们提交 J 中的快照进行比较。这会找出 我们 更改了哪些文件,以及我们对这些文件进行了哪些更改。

  2. H 中的快照与 L 中的快照进行比较。这会找出 他们 更改了哪些文件,以及他们对这些文件做了哪些更改。

  3. 合并更改。这是艰苦工作的部分。 Git 使用简单的文本替换规则进行组合:它不知道真正应该使用哪些更改。在规则允许的情况下,Git 自行进行这些更改;如果规则声称存在冲突,Git 会将冲突传递给我们,让我们解决。在任何情况下,Git 应用 对起始提交中快照的组合更改:merge base H。这样可以在添加他们的更改的同时保留我们的更改。

因此,如果合并顺利进行,Git 将进行新的合并提交 M,如下所示:

          I--J
         /    \
...--G--H      M   <-- our-branch (HEAD)
         \    /
          K--L   <-- their-branch

新提交 M 有一个快照,就像任何提交一样,还有一个日志消息和作者等等,就像任何提交一样。 M 唯一的特别之处在于它不仅链接回提交 J——我们开始时的提交——而且还链接回提交 L,我们告诉其哈希 ID 的提交 git merge 关于(使用原始哈希 ID,或使用名称 their-branch)。

如果我们必须自己修复合并,我们会这样做,然后 运行 git add 然后 git commitgit merge --continue,使合并提交 M。当我们这样做时,我们可以完全控制进入 M.

的内容

2这种合并会导致 合并提交,即有两个父项的提交。 Git 也可以执行它所谓的 快进合并 ,这根本不是合并并且不产生新的提交,或者它所谓的 octopus merge,需要三个以上的输入提交。八达通合并有一定的限制,这意味着它们不适用于这种情况。真正的合并可能涉及进行 递归 合并,这也使图片复杂化,但我将在这里忽略这种情况:复杂性与我们将要做的事情没有直接关系.


重做错误的合并

我们这里的情况是我们开始于:

          I--J   <-- our-branch (HEAD)
         /
...--G--H
         \
          K--L   <-- their-branch

然后有人——大概不是我们——运行 git merge their-branch 或等价物,遇到了合并冲突,并错误地解决了它们并提交:

          I--J
         /    \
...--G--H      M   <-- our-branch (HEAD)
         \    /
          K--L   <-- their-branch

重新执行合并,我们只需要签出/切换到提交J:

git checkout -b repair <hash-of-J>

例如,或:

git switch -c repair <hash-of-J>

使用新的(自 Git 2.23 起)git switch 命令。然后我们运行:

git merge <hash-of-L>

要获得两个哈希 ID,我们可以在合并提交 M 上使用 git rev-parse,使用时髦的 ^1^2 语法后缀;或者我们可以 运行 git log --graph 或类似的方法找到两个提交并直接查看它们的哈希 ID。或者,如果名称 their-branch 仍然找到提交 L,我们可以 运行 git merge their-branch。 Git 只需要找到正确的提交。

此时,

Git 将按照完全相同的规则重复之前尝试的合并尝试。这将产生完全相同的冲突。我们现在的工作是解决这些冲突,但这一次,我们做对了。

如果我们喜欢其他人在提交 M 中做出的决议,我们可以询问 git checkout(Git 的所有版本)或 git restore(Git 2.23 及更高版本)提取其他人提交的已解析文件 M:

git checkout <hash-of-M> -- <path/to/file>

例如。即使我们不喜欢整个分辨率,我们仍然可以这样做,然后修复文件和 运行 git add;只有当我们不喜欢 任何 的决议,并且想自己完成整个修复时,我们才 必须 完成整个自己装修。

尽管如此,我们只是修复了每个文件,git add 结果告诉 Git 我们已经修复了文件。 (git checkout <em>hash</em> -- <em>path</em> 技巧使我们可以跳过git add 在某些情况下是一步,但它也不会伤害到 运行 git add。)当我们都完成后,我们 运行 git merge --continuegit commit 完成此合并:结果是一个新的合并提交 M2N,在我们的新 b运行ch repair 或我们在我们创建了它:

          I--J-----M2   <-- repair (HEAD)
         /    \   /
...--G--H      M /  <-- our-branch
         \    /_/
          K--L   <-- their-branch

我们现在可以 git checkout our-branch,这使我们能够提交 M,并直接从 repair:

抓取文件
git checkout our-branch
git checkout repair -- path/to/file1
git checkout repair -- path/to/file2
...

然后我们准备好 git commit 进行新的提交 N。或者,我们可以从 M2:

集中抓取 每个 文件
git checkout repair -- .

和运行 git status, git diff --cached, and/or git commit 在这一点上,取决于我们对这一切的确定程度。

以上结果为:

          I--J-----M2   <-- repair
         /    \   /
...--G--H      M-/--N   <-- our-branch (HEAD)
         \    /_/
          K--L   <-- their-branch

我们现在可以完全删除 b运行ch name repair:commit N 只是“神奇地修复”了。

如果我们打算保持提交M2,我们可以使用git mergerepair合并到M。我们可能想要 运行 git merge --no-commit 以便我们获得完全控制:这将阻止 git merge 进行实际提交,以便我们可以检查即将进入的快照新合并。然后最后的 git merge --continuegit commit 使 N 作为新的合并提交:

          I--J-----M2   <-- repair
         /    \   /  \
...--G--H      M-/----N   <-- our-branch (HEAD)
         \    /_/
          K--L   <-- their-branch

我们可以再次删除名称repair;它不再增加任何有价值的东西。

(我通常只是自己做一个简单的非合并修复提交,而不是另一个合并。使 N as 合并的合并基础是提交 JL,这意味着 Git 将进行递归合并,除非我们指定 -s resolve。递归合并往往很混乱,有时会有奇怪的冲突。)

如果自错误合并以来有提交

发生在之后 bad-merge-M 的提交只需要将它们的更改转入我在上面绘制的最终提交 N 中。你如何着手实现这一目标并不是非常重要,尽管某些方法可能 Git 为你做更多的工作。这里要记住的是我之前所说的:最后,重要的是存储库 中的提交 。这包括图表——从提交到早期提交的回溯连接——和快照。该图对 Git 本身很重要,因为它是 git log 的工作方式以及 git merge 找到合并基础的方式。快照对很重要,因为它们是Git存储你关心的内容的方式。