恢复错误合并后恢复丢失的更改

Posted

技术标签:

【中文标题】恢复错误合并后恢复丢失的更改【英文标题】:Recovering lost changes after reverting bad merge 【发布时间】:2019-03-28 06:24:17 【问题描述】:

我对 Git 比较陌生,在误解了一篇帮助文章后犯了一个(愚蠢的)错误,我不确定如何使用 Git 完全解决问题,而不是手动重新引入对目标分支的更改。

S---sc1---sc2---sc3-----sc4----M4---R1---M5---sc5
 \                         \  /         /
  T1-------------------M2---M3--------R2
   \               \  /
    F---fc1---fc2---M1

一些注意事项:S 是本场景中的主分支,T1 是从S 拉出的团队分支,F 是从T1 拉出的我的功能分支。

我设置了自动合并,所以当提交到 T1 分支时,它们会通过持续集成运行,然后自动合并到 ST1 分支中有一个文件与来自另一个团队成员的提交的 S 存在合并冲突,因此我决定在完成对 F 的工作后修复该问题。

我将T1 合并到F (M1),然后将F 合并到T1 (M2)。考虑到我过去遇到的合并冲突解决方案不符合我预期的问题,我想我会尝试一些新的东西:将冲突文件从 S 合并到 T1,解决那里的合并冲突,删除合并中的所有其他文件,然后允许持续集成将所有内容合并到S

我在没有提交的情况下开始从 ST1 (M3) 合并,解决了冲突,从合并中删除了其他 (~200) 个文件,然后提交。这会自动合并到 S (M4)。

我立即注意到,排除这大约 200 个文件似乎完全消除了更改,这相当于 2 个团队大约一个月的工作量。我(错误地)决定最好的行动方案是在我的错误进入其他人的本地存储库之前迅速采取行动并恢复合并提交M4M3。我首先恢复了M4 (R1),一旦提交,我就恢复了M3 (R2)。我认为这是正确的顺序,因为当自动合并启动时,我不确定反过来是否会引入问题。最终R2 被提交并自动合并到S (M5)。

这解决了其他人的更改被清除的问题,但是我在F 中的所有更改以及最初存在合并冲突的文件都从S 中消失了。我能够将单个文件的更改直接提交给S (sc5),但F 中的更改要复杂得多。他们仍然住在T1,但由于他们是从S 恢复为R1 的一部分,所以我不能只是将他们提交回来。

我花了一天的大部分时间试图弄清楚如何最好地使这些更改达到S,但git rebasegit cherry-pick 似乎不会满足我的需要,不过我很清楚我可能是错的。如果有人比我更擅长 Git,至少可以提出一个起点,那就太棒了。谢谢!

编辑:从图表中删除了无用/令人困惑的点。 M2 没有自动合并到 S,因为我试图用 M3 解决合并冲突。

编辑 2: 在阅读了 torek 的精彩解释后,我开始尝试变基。我忘记了在F 的整个历史中,我已经多次将T1 分支合并到F 分支中,因为这个特性分支跨越了多少时间。这意味着有很多很多的合并冲突需要解决。

在 torek 对此的回应中,我尝试了合并壁球。我最初的想法是我需要将新分支从合并壁球合并到T1 分支,然后将T1 分支合并到S,但我遇到了同样的问题,它没有看到变化。我认为这是因为 T1 中已经存在更改,所以它基本上只是将相同的、先前还原的更改返回到 S,它不想要它们。

编辑 3: 多亏了 torek 的详细解释(非常感谢!),我正在处理合并壁球,然后将结果合并到解决冲突后的S 分支。

【问题讨论】:

如果你链接到这个 Octopus CI 的东西,它可能会有所帮助。您的图表绘制也相当不错,但并未反映 Git 内部的实际工作方式——Git 的分支名称仅标识 tip 提交,该分支包含的提交集随着图表的增长而动态变化。我必须在这里做一堆假设...... 提及章鱼没有帮助,所以我删除了它。回复:CI - 我的意思是它贯穿了我们的持续集成构建,所以我也修复了它。至于图表,我在其中添加的提交(sc1、tc1 等)只是为了证明合并之间的这些分支上正在发生活动。我在内部对 Git 不够熟悉,无法将其绘制出来——我只是将它用作视觉辅助工具,正如我在阅读的文档和指南中所做的那样。 好的,但是您说的是“自动合并”(我假设是 CI 系统的一部分),但是您在绘图中显示了合并之间的几个提交。例如,有 tc2 指向 tc1,没有从顶部到中间行的合并。 (随着时间向右增加的 Git 图表应该从右向左阅读:Git 从末尾开始——我提到的那些提示提交——并向后工作。)这里的最终结果是我不确定该假设什么你实际的 Git 图表。由于图表控制合并的作用,因此这一点很重要。 好点,感谢您的耐心等待。我已经编辑了图表并在编辑中解释了 M2 合并。希望这会有所帮助。 好的,我认为现在这一切对我来说都是有意义的。还有一个问题:这些内容是否已在任何地方推送/发布,或者您可以安全地删除和重写其中的许多提交吗? (从技术上讲,这两者并不相互排斥,但考虑 Git 提交的一种简单方法是,如果您还没有让其他人看到它们,则可以删除它们而不必担心。) 【参考方案1】:

这很长,所以请随意跳过您已经知道的部分(或一直滚动到最后)。每个部分都有设置信息来解释正在发生的事情,或者我们正在做什么,在后面的部分。

介绍-y位

让我首先以我喜欢的方式重新绘制这个图(我认为这是一个部分图,但它包含我们需要的关键提交):

S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5   <-- branch-S
  \                        \  /         /
   T0-------------o----M2---M3--------R2   <---- branch-T1
    \              \  /
    F0--fc1---fc2---M1   <------------------- branch-F

这里,分支名称branch-Sbranch-T1branch-F,这些名称目前用于识别其哈希 ID 无法发音且人类无法记住的提交,但我们分别调用sc5R2M1。任何o 节点都是没有以任何方式特别区分的提交,实际上可能代表任意数量的提交。命名的fc&lt;number&gt;s 是功能分支上的一组提交,M&lt;number&gt; 提交是合并的。我将第一个提交重命名为 S0T0F0,只是为了将它们与分支名称区分开来。

一些合并是手动进行的:

$ git checkout <branch-name>
$ git merge [options] <other-branch>
... fix up conflicts if necessary, and git commit (or git merge --continue)

其他合并是由软件进行的,只有在没有冲突的情况下才会发生。 R 提交来自运行:

git checkout <branch>
git revert -m 1 <hash ID of some M commit>

其中&lt;branch&gt;T1S,而-m 1 是因为您总是必须告诉git revert 在恢复合并时使用哪个父级,并且它几乎总是父级#1。

提交会移动一个分支名称

最简单的 Git 提交图是一条直线,只有一个分支名称,通常是 master

A--B--C   <-- master (HEAD)

这里,我们需要提到Git的index。最好将索引描述为 Git 构建 next 提交的地方。它最初包含保存在当前提交中的每个文件(此处为C):您检查此提交,使用来自提交C 的文件填充索引和工作树。名称master 指向此提交,名称HEAD 附加到名称master

然后修改工作树中的文件,使用git add 将它们复制回索引,如果需要,使用git add 文件复制到索引中,然后运行@987654354 @。通过将这些索引副本冻结到快照中来进行新的提交。然后,Git 添加快照元数据(您的姓名和电子邮件、您的日志消息等)以及当前提交的哈希 ID,以便新提交指向现有提交。结果是:

A--B--C   <-- master (HEAD)
       \
        D

有了新的提交,有了新的唯一哈希 ID,就在半空中闲逛,没有什么可记住的。因此,进行新提交的最后步骤是将新提交的哈希 ID 写入分支名称:

A--B--C--D   <-- master (HEAD)

现在当前提交是D,并且索引和当前提交匹配。如果你git add-ed 工作树中的所有文件,那也匹配当前提交和索引。如果没有,您可以git add 更多文件并再次提交,使名称master 指向新提交E,依此类推。在任何情况下,新提交的(单个)父级是当前提交的。

关于合并

让我概述一下git merge 的实际工作原理。在某些情况和某些方面非常简单,让我们从最简单的 true-merge 案例开始。考虑如下图:

          o--...--L   <-- mainline (HEAD)
         /
...--o--*
         \
          o--...--R   <-- feature

我们已经运行了git checkout mainline; git merge feature,所以我们告诉Git 将分支feature/提交R 合并到分支mainline/提交L。为此,Git 必须首先找到 merge base 提交。粗略地说,合并基础是两个分支共同的“最近”提交,即可从访问。在这个简单的例子中,我们从L 开始并向后走到较旧的提交,并从R 开始并向后走,我们遇到的第一个地方是提交*,所以这就是合并基础。

(有关可达性的更多信息,请参阅Think Like (a) Git。)

找到合并基础后,Git 需要将L(左侧/本地/--ours)和R(右侧/远程/--theirs)快照转换为变更集。这些变更集告诉 Git 自合并基础 * 以来我们在 mainline 上做了什么,以及自合并基础以来它们在 feature 上做了什么。这三个提交都有哈希ID,是三个提交的真实名称,所以Git可以在内部运行相当于:

git diff --find-renames <hash-of-*> <hash-of-L>   # what we changed
git diff --find-renames <hash-of-*> <hash-of-R>   # what they changed

合并只是合并两组更改,并将合并后的设置应用于* 中的快照中的文件。

如果一切顺利,Git 会以通常的方式进行新提交,除了新提交有 两个 父级。这使得当前分支指向新的合并提交:

          o--...--L
         /         \
...--o--*           M   <-- mainline (HEAD)
         \         /
          o--...--R   <-- feature

M 的第一个父级是L,第二个是R。这就是为什么 reverts 几乎总是使用 parent #1,以及为什么 git log --first-parent 只“看到”主线分支,从 M 遍历到 L 而完全忽略 R 分支。 (注意这里的branch这个词指的是图的结构,而不是像feature这样的分支names:此时我们可以删除name 完全是feature。另见What exactly do we mean by "branch"?)

当出现问题时

如果两个变更集以“糟糕的方式”重叠,则合并将停止,合并冲突。特别是,假设 base-vs-L 说要更改文件 F 的第 75 行,而 base-vs-R also 说要更改文件 F 的第 75 行。如果两个更改集都说要进行 same 更改,Git 可以这样做:这两个更改的组合是进行一次更改。但是,如果他们说要进行 不同 更改,Git 就会声明合并冲突。在这种情况下,Git 会在自己能做的一切之后停下来,让你收拾残局。

由于有三个输入,Git 将在此时将文件F所有三个 版本保留在索引中。通常,索引对每个要提交的文件都有一个副本,但在此冲突解决阶段,它最多有三个副本。 (“up to”部分是因为你可能有其他类型的冲突,由于篇幅原因我不会在这里讨论。)同时,在文件Fwork-tree副本中, Git 将其近似值保留为合并,工作树文件中的两组或全部三组行带有 &lt;&lt;&lt;&lt;&lt;&lt;&lt; / &gt;&gt;&gt;&gt;&gt;&gt;&gt; 标记。 (要获得所有三个,请将merge.conflictStyle 设置为diff3。我更喜欢这种模式来解决冲突。)

如您所见,您可以以任何您喜欢的方式解决这些冲突。 Git 假定无论您做什么都是解决问题的正确方法:这会产生完全正确的最终合并文件,或者在某些情况下会缺少文件。

但是,无论您做什么,最终的合并(假设您没有中止它,并且没有使用合并的非合并变体之一)仍然会在图表中产生相同的结果,并且无论您放置什么在索引中,通过解决冲突,是合并的结果。这是合并提交中的新快照。

更复杂的合并基础

当图形像上面那样非常简单时,合并基很容易看到。但是图表并不简单,你的也不是。包含一些合并的图的合并基础比较棘手。例如,考虑以下片段:

 ...--sc4----M4---R1
         \  /
...--M2---M3--------R2

如果R1R2 是两个提示提交,它们的合并基础是什么?答案是M3,而不是sc4。原因是虽然M3sc4 都是从R1R2 开始并向后工作的提交,但M3 更“接近”R2(后退一步)。从R1M3sc4 的距离是两跳——跳到M4,然后再往后退一步——但是从R2M3 的距离是一跳,从R2sc4 是两跳。所以M3 是“较低的”(在图表中),因此赢得了比赛。

(幸运的是,您的图表没有出现平局的情况。如果有 平局,Git 的默认方法是合并所有已绑定的提交,一次两个,以产生一个 "虚拟合并基础”,它实际上是一个实际的(尽管是临时的)提交。然后它使用通过合并合并基础所做的临时提交。这就是 recursive 策略,它的名字来源于事实Git 递归地合并合并基以获得合并基。您可以选择 resolve 策略,它只是简单地选择一个看似随机的基 - 无论哪个基在算法前面弹出. 几乎没有任何优势:递归方法通常要么做同样的事情,要么是对随机选择获胜者的改进。)

这里的关键点是进行合并提交更改,提交未来合并将选择作为它们的合并基础。即使在进行简单的合并时,这一点也很重要,这就是为什么我用粗体字表示的原因。这就是我们进行合并提交的原因,而不是不是合并的 squash-“合并”操作。 (但 squash 合并仍然有用,我们稍后会看到。)

介绍问题:出了什么问题(这样你以后可以避免它)

解决了上述问题,现在我们可以看看真正的问题了。让我们从这个开始(稍微编辑以使用更新的提交和分支名称):

我将branch-T1 合并到branch-F (M1),然后将branch-F 合并到branch-T1 (M2)。

我在这里假设合并fc2(作为branch-F 的then-tip)和o(作为branch-T1 的then-tip)进展顺利,并且Git 能够使M1 on它自己的。正如我们之前看到的,合并实际上不是基于分支,而是基于commits。它是创建一个调整分支名称的新提交。所以这创建了M1,所以branch-F 指向M1M1 本身指向 branch-T1 的现有提示——我现在将 o 标记为它的第二个父提交,fc2 作为它的第一个父提交。 Git 通过 git diff 计算出正确的 内容 提交,将合并基础 T0 的内容与 ofc2 进行对比:

T0-------------o   <-- branch-T1
 \
 F0--fc1---fc2   <--- branch-F (HEAD)

一切顺利,Git 现在可以自己生成M1

T0-------------o   <-- branch-T1
 \              \
 F0--fc1---fc2---M1   <--- branch-F (HEAD)

现在你 git checkout branch-T1git merge --no-ff branch-F (没有 --no-ff Git 只会快进,这不是图片中的内容),所以 Git 找到了 oM1 的合并基础,即o 本身。这种合并很简单:从oo 的区别是什么,加上从oM1 的区别等于M1 的内容。所以M2,作为一个快照,和M1是一模一样的,Git很容易就创建了:

T0-------------o----M2   <-- branch-T1 (HEAD)
 \              \  /
 F0--fc1---fc2---M1   <--- branch-F

到目前为止,一切都很好,但现在事情开始变得非常糟糕:

T1 分支中有一个文件与 S 存在合并冲突...鉴于我过去遇到的问题,合并冲突解决方案的行为不符合我的预期,我想我会尝试一些新的东西:只将有冲突的文件从S 合并到T1,解决那里的合并冲突,从合并中删除所有其他文件,然后允许持续集成将所有内容合并到S

所以,此时你所做的是:

git checkout branch-T1
git merge branch-S

由于合并冲突而停止。此时的图表与上面的图表相同,但包含更多上下文:

S0--sc1---sc2---sc3-----sc4   <-- branch-S
  \
   T0-------------o----M2   <-- branch-T1 (HEAD)
    \              \  /
    F0--fc1---fc2---M1   <-- branch-F

合并操作找到合并基础(S0),将其与两个提示提交(M2sc4)进行比较,合并生成的更改,并将它们应用于S0 的内容。一个冲突的文件现在作为三个输入副本在索引中,在工作树中作为 Git 的合并努力,但带有冲突标记。同时所有不冲突的文件都在索引中,准备被冻结。

唉,您现在在冲突合并期间删除了一些文件 (git rm)。这将从索引和工作树中删除文件。生成的提交M3 将表明基于合并基础S0 组合提交M2sc4 的正确方法是删除这些文件。 (这当然是错误的。)

这会自动合并到S (M4)。

在这里,我假设这意味着系统,使用它所拥有的任何预编程规则,做了相当于:

git checkout branch-S
git merge --no-ff branch-T1

找到了提交sc4branch-S的提示)和M3的合并基,即M3,与oM1的合并基相同的方式是M1早些时候。所以新的提交,M4,在内容方面匹配M3,此时我们有:

S0--sc1---sc2---sc3-----sc4----M4   <-- branch-S
  \                        \  /
   T0-------------o----M2---M3   <-- branch-T1
    \              \  /
    F0--fc1---fc2---M1   <-- branch-F

我立即注意到,排除这大约 200 个文件似乎完全消除了更改,这相当于 2 个团队大约一个月的工作量。我(错误地)决定最好的行动方案是迅速采取行动,并在我的错误进入其他人的本地存储库之前恢复合并提交 M4M3。我首先恢复了M4 (R1),一旦提交,我就恢复了M3 (R2)。

其实,这是一件好事!它得到正确的内容,当你立即执行时这非常有用。使用git checkout branch-s &amp;&amp; git revert -m 1 branch-S(或git revert -m 1 &lt;hash-of-M4&gt;)从M4创建R1基本上撤消了内容方面的合并,因此:

git diff <hash-of-sc4> <hash-of-R1>

应该不会产生任何东西。同样,使用git checkout branch-T1 &amp;&amp; git revert -m 1 branch-T1(或与哈希相同)从M3 创建R2 撤消在内容方面合并:比较M2R2,您应该会看到相同的内容。

撤消合并会撤消内容,但不会撤消历史记录

现在的问题是 Git 认为您的功能分支中的所有更改都已正确合并。任何git checkout branch-T1git checkout branch-S 后跟git merge &lt;any commit within branch-F&gt; 都会查看图表,沿着从提交到提交的反向链接,并看到branch-F 内的此提交——例如fc2M1——已合并

让他们进入的诀窍是进行 new 提交,它与从 F0M1 的提交序列所做的事情相同,那是 不是 已经合并。最简单(尽管最丑陋)的方法是使用git merge --squash。更难,或许更好的方法是使用git rebase --force-rebase 创建一个new 功能分支。 (注意:这个选项有三个拼写,最容易打的一个是-f,但是Linus Torvalds' description里面的那个是--no-ff。我觉得最难忘的是--force-rebase这个版本,但我实际上会用@987654516 @我自己。)

让我们快速浏览一下两者,然后考虑使用哪个以及为什么。无论哪种情况,一旦你完成了,这次你必须正确地合并新的提交,而不是删除文件;不过既然你知道git merge 真正在做什么,那应该会容易很多。

我们首先创建一个新的分支名称。我们可以重复使用branch-F,但我认为如果我们不这样做会更清楚。如果我们想使用git merge --squash,我们创建这个新的分支名称指向提交T0(忽略T0之后有提交的事实——记住,任何分支名称都可以指向any提交):

T0   <-- revised-F (HEAD)
 \
  F0--fc1--fc2--M1   <-- branch-F

如果我们想使用git rebase -f,我们创建这个指向提交fc2的新名称:

T0-----....
 \
  F0--fc1--fc2--M1   <-- branch-F, revised-F (HEAD)

我们这样做:

git checkout -b revised-F <hash of T0>   # for merge --squash method

或:

git checkout -b revised-f branch-F^1      # for rebase -f method

取决于我们要使用的方法。 (^1~1 后缀——您可以使用其中任何一个——排除 M1 本身,将第一个父步骤退回到 fc2。这里的想法是排除提交 o 和任何其他可访问的提交来自o。在提交的最后一行中,不需要有其他合并到branch-F。)

现在,如果我们想使用“squash 合并”(它使用 Git 的合并机制而不进行合并 commit),我们运行:

git merge --squash branch-F

这使用我们当前的提交,加上branch-F(commit M1)的提示,作为合并的左右两侧,找到它们共同的提交作为合并基础。常见的提交当然只是F0,所以合并resultM1 中的快照。但是,新的提交只有 一个 父级:它根本不是合并提交,它看起来像这样:

   fc1--fc2--M1   <-- branch-F
  /
F0-------------F3   <-- revised-F (HEAD)

F3 中的 快照M1 中的匹配,但提交本身是全新的。它会收到一条新的提交消息(您可以编辑),当 Git 将 F3 视为提交时,它的效果是进行从 F0M1 的相同更改集。

如果我们选择变基方法,我们现在运行:

git rebase -f <hash-of-T0>

(您可以改用o 的哈希值,即branch-F^2,即M1 的第二个父项。在这种情况下,您可以从revised-F 开始指向M1 本身。这可能是我会怎么做,以避免不得不剪切和粘贴大量带有潜在拼写错误的哈希 ID,但除非你已经完成了大量的图形操作练习,否则它是如何工作的并不明显。)

也就是说,我们希望将 F0fc2 的提交复制到 new 提交,并使用新的哈希 ID。这就是 git rebase 将要做的事情(请参阅上面的其他 *** 答案和/或 Linus 的描述):我们得到:

  F0'-fc1'-fc2'   <-- revised-F (HEAD)
 /
T0-----....
 \
  F0--fc1--fc2--M1   <-- branch-F

现在我们有 revised-F 指向单个提交 (F3) 或提交链(以 fc2' 结尾的链,fc2 的副本),我们可以 git checkout 其他一些分支和git merge revised-F

基于 cmets,这里有两条重新合并的路径

我假设此时您有一个 squash-merge 结果(不是合并的单父提交,但确实包含所需的快照,我在这里称之为 F3)。我们还需要根据 cmets 对重新绘制的图表进行一些修改,这些 cmets 表明有更多的合并到 branch-F

S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5   <-- branch-S
  \                        \  /         /
   T0-----o-------o----M2---M3--------R2   <---- branch-T1
    \      \       \  /
    F0--fc1-o-fc2---M1   <--------------- branch-F

现在我们将添加revised-F 分支,它应该有一个提交,它是F0T0 的后代。哪一个并不重要。由于我之前使用了F0,所以让我们在这里:

S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5   <-- branch-S
  \                        \  /         /
   T0-----o-------o----M2---M3--------R2   <---- branch-T1
    \      \       \  /
    F0--fc1-o-fc2---M1   <--------------- branch-F
      \
       ---------------------------------F3   <-- revised-F

提交F3 的内容与M1 的内容匹配(所以git diff branch-F revised-F 什么也没说),但F3 的父级是F0。 (注意:有使用git commit-tree创建F3的快捷方式,但只要它已经存在并且与M1内容匹配,我们就可以使用它。)

如果我们现在这样做:

git checkout branch-T1
git merge revised-F

Git 将在提交 R2(分支 T1 的提示)和 F3revised-F 的提示)之间找到合并基础。如果我们沿着R2 的所有向后(向左)链接,我们可以通过M3 到达T0,然后是M2,然后是一些os,最后是T0,或者我们可以到达@ 987654588@ 通过M3 然后M2 然后M1 然后fc2 回到F0。同时我们可以从F3直接到F0,只需一跳,所以合并基可能是F0

(要确认这一点,请使用git merge-base

git merge-base --all branch-T1 revised-F

这将打印一个或多个哈希 ID,每个合并基数一个。理想情况下,只有一个合并基础,即提交 F0。)

Git 现在将运行两个git diffs,比较F0F3 的内容——即我们为完成该功能所做的一切——并将F0 的内容与@ 的内容进行比较987654604@,在branch-T1 的顶端。我们会在两个差异更改相同文件的相同行时发生冲突。在其他地方,Git 将获取 F0 的内容,应用合并的更改,并准备好提交结果(在索引中)。

解决这些冲突并提交会给你一个新的提交,结果是:

S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5   <-- branch-S
  \                        \  /         /
   T0-----o-------o----M2---M3--------R2-----M6   <---- branch-T1
    \      \       \  /                     /
    F0--fc1-o-fc2---M1   <-- branch-F      /
      \                                   /
       ---------------------------------F3   <-- revised-F

现在M6 或许可以合并到branch-S


或者,我们可以直接合并到branch-S。哪个提交是合并基础不太明显,但它可能又是F0。这又是同一张图:

S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5   <-- branch-S
  \                        \  /         /
   T0-----o-------o----M2---M3--------R2   <---- branch-T1
    \      \       \  /
    F0--fc1-o-fc2---M1   <--------------- branch-F
      \
       ---------------------------------F3   <-- revised-F

从提交sc5 开始,我们向后工作到M5R2,我们现在处于与以前相同的情况。所以我们可以git checkout branch-S 并进行相同的合并,解决类似的冲突——这次我们将F0sc5 进行比较,而不是R2,因此冲突可能略有不同——并最终提交:

S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5----M6   <-- branch-S
  \                        \  /         /           /
   T0-----o-------o----M2---M3--------R2   <------ / -- branch-T1
    \      \       \  /                           /
    F0--fc1-o-fc2---M1   <-- branch-F            /
      \                                         /
       ---------------------------------------F3   <-- revised-F

要验证F0 是合并基,请像以前一样使用git merge-base

git merge-base --all branch-S revised-F

要查看需要合并的内容,请从合并基础运行两个 git diffs 到两个提示。

(要进行哪种合并取决于您。)

【讨论】:

哇,非常感谢您如此详细的解释和回答!你已经向我展示了我 100% 对 Git 的想法是错误的。我现在正在做rebase,但是因为T0 是从大约两个月前开始的,所以我正在处理大量的合并冲突。我已经完成了所有这些 - 如果这有效,我会告诉你。 当你进行分支复制时,你应该得到 no 合并冲突:如果你得到合并冲突,你就会得到你不想要的提交(通过F0--fc1--fc2 链中未显示但实际上必须在其中的合并)。在这种情况下,您可能会使用 squash 方法,或精心手工构建的提交列表以供挑选。这就是为什么(以及何时)准确的图表至关重要:提交列表基于遍历图表和“可达性”概念。 或者:也许他们正在通过M1 接收,在这种情况下,您希望从指向fc2revised-F 开始。我应该解决上述问题 执行 one-big-squash 和 re-merge 的缺点是以后使用 git bisect 更难找到错误。优点是现在处理起来更容易。 rebase-or-cherry-pick 方法(通过副本保留原始作品)需要枚举要复制的提交集。这有多难,取决于所有合并的谨慎程度:git rev-list --topo-order --reverse --first-parent &lt;cutoff&gt;..&lt;endpoint&gt; 可能会生成正确的列表。这将遍历图形,但仅遵循每个合并的第一个父级。 (续) 一旦你有了正确的列表,在(比如说)一个哈希 ID 的文件中,你可以挑选它们。这仍然很棘手,因为必须考虑任何合并。这是一项相当大的工作,可能不值得为潜在的未来收益付出代价,所以在这一点上,我会选择 squash-merge-then-re-merge 方法。

以上是关于恢复错误合并后恢复丢失的更改的主要内容,如果未能解决你的问题,请参考以下文章

重装xp系统后分区全部合并到C盘其它盘的文件怎样恢复

从错误分支恢复合并后无法从另一个远程拉取

win10系统重装后分区全部合并到一个盘别的分区的数据如何恢复

重装win8系统后分区全部合并到一个盘别的分区的文件怎么恢复

win10系统重装后分区全部合并到C盘别的盘的文件怎样恢复

如何在不进行任何更改的情况下恢复推送的合并?