合并来自 git revert 的冲突 - 我应该接受当前的更改还是传入,为啥?

Posted

技术标签:

【中文标题】合并来自 git revert 的冲突 - 我应该接受当前的更改还是传入,为啥?【英文标题】:Merge conflicts from git revert - Should I accept current change or incoming and why?合并来自 git revert 的冲突 - 我应该接受当前的更改还是传入,为什么? 【发布时间】:2021-10-20 14:56:48 【问题描述】:

我有这样的提交 - A

我正在使用git revert --no-commit [git hash] 撤消我想要保留的提交之间的特定提交。假设我想还原 D 和 B。

基于this post,正确的还原方式是从您要还原的最新提交开始 - 例如,

git revert --no-commit D
git revert --no-commit B
git commit

我遇到了合并冲突,我不确定是否应该接受当前的更改或传入的更改,因为这实际上是在倒退。

【问题讨论】:

【参考方案1】:

TL;DR

一般来说,您将不得不考虑结果。您不想盲目地接受“我们的”,因为这将保留您尝试撤消的提交。您不想盲目地接受“他们的”,因为这几乎肯定会消除您想要保留other 提交中的一个或部分。总体而言,您通常可能更喜欢“他们的”——但需要思考。要了解原因,请继续阅读。

这是一个小问题,与您的问题及其答案没有直接关系,但值得一提:Git 在内部向后工作(因为它必须)。1因此提交链接向后而不是向前。从较晚提交到较早提交的实际链接是后来提交的一部分。所以你的绘图会像这样更准确:

A <-B <-C <-D <-E   <-- main (HEAD)

(假设您在分支main,因此名称main 选择提交E)。但是我通常对此很懒惰并绘制连接线,因为它更容易,因为带有对角箭头的箭头字体效果不佳,而倾斜连接线的\/工作正常。

无论如何,“向后”进行还原的原因是,如果我们想要撤消提交E 的效果,并运行git revert E 以进行提交Ǝ

A--B--C--D--E--Ǝ   <-- main (HEAD)

在提交Ǝ 中生成的源快照将与提交D 中的源快照完全匹配。这意味着我们现在可以运行git revert D 并获得一个“撤消”D 效果的提交,而不会看到任何合并冲突。生成的快照与C 中的快照匹配,因此恢复C 变得很简单,从而生成与B 匹配的快照,依此类推。

换句话说,通过以相反的顺序还原,我们确保我们永远不会发生任何冲突。没有冲突,我们的工作就更轻松了。

如果我们要选择特定提交来恢复,这种避免冲突的策略就会失败,并且可能没有充分的理由以相反的顺序恢复。使用相反的顺序可能仍然是好的 - 例如,如果它导致更少冲突 - 或者它可能是中立的甚至是坏的(如果它导致更多/更坏的冲突,尽管这在大多数现实中不太可能场景)。

有了这个,让我们来回答你的问题......好吧,几乎回答你的问题。 cherry-pick 和 revert 都作为三路合并操作实现。为了正确理解这一点,我们首先需要了解 Git 如何进行三向合并,以及它为什么起作用(以及何时起作用,以及冲突意味着什么)。


1这是必要的原因是任何提交的任何部分都不能被更改,即使是 Git 本身也是如此。由于较早的提交一旦完成就已确定,因此无法返回并使其链接到后面的提交。


标准git merge

我们通常的简单合并案例如下所示:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

这里我们有两个分支,share 向上提交,包括提交 H,但随后出现分歧。提交IJ只在branch1,而K-L 目前只在branch2

我们知道每个提交都包含一个完整的快照(不是一组更改,而是一个快照),其中的文件被压缩和重复数据删除,并以其他方式被 Git 化。但是每个提交代表一些变化:例如,通过比较 H 中的快照和 I 中的快照,我们可以看到提交 I 的人修复了README 文件,例如第 17 行。

所有这一切意味着要查看更改,Git 总是必须比较两个提交2 鉴于这一现实,很容易看到Git 可以通过比较最佳共享 提交、提交H 和我们的最后 提交来找出我们branch1 上发生了什么变化,提交J。无论这里的文件有何不同,无论我们做了什么更改,这些都是我们的更改。

同时,合并的目标是合并更改。因此,Git 应该运行这个 diff(两次提交的比较)来查看 我们的 更改,但也应该运行类似的 diff 来查看 他们的 更改。要查看 他们 发生了什么变化,Git 应该从相同的最佳共享提交 H 开始,并将其与 他们 最后一次提交 L 进行比较:

git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed
git diff --find-renames <hash-of-H> <hash-of-L>   # what they changed

Git 现在将合并这两组更改:如果我们更改了 README 文件而他们没有更改,这意味着 使用我们的 README 文件版本时间>。如果他们更改了某些文件而我们没有更改,这意味着使用他们的该文件版本。如果我们都接触了 same 文件,Git 必须弄清楚如何组合这些更改,如果没有人接触过某个文件——如果 所有三个版本都匹配——Git 可以接受这三个版本中的任何一个。

这些为 Git 提供了许多捷径。组合我们的更改的缓慢而简单的方法是从H 本身提取所有文件,在不冲突的地方应用我们和他们的更改,并在他们的地方应用带有冲突标记的冲突更改> 冲突。 Git 真正做的事情也有同样的效果。如果没有任何冲突,则生成的文件都准备好进入新的合并提交 M:

          I--J
         /    \
...--G--H      M   <-- branch1 (HEAD)
         \    /
          K--L   <-- branch2

新的提交成为branch1 的最后一个提交。它链接回提交J,就像任何新提交一样,但它链接回提交L,该提交仍然是branch2的最后一次提交。 p>

现在所有提交都在branch1(包括新的)。提交 K-L,以前只在 branch2 上,现在也在 branch1 上。这意味着在未来合并中,最佳共享提交将是提交L,而不是提交H。我们不必重复相同的合并工作。

请注意,提交 M 包含最终合并结果:所有文件的简单快照,包含正确合并的内容。提交 M 仅在一个方面特别:它有 两个J,而不是 一个JL

如果存在 冲突,Git 会让你——程序员——修复它们。您编辑工作树中的文件,和/或访问 Git 拥有的三个输入副本——分别来自提交 HJL——并组合文件以产生正确的结果。无论正确的结果是什么,运行git add 将其放入未来的快照中。完成后,运行:

git merge --continue

或:

git commit

merge --continue 只是确保有一个合并完成,然后为你运行git commit,所以效果是一样的)。这使得提交M 带有您在解决所有冲突时提供的快照。请注意,最后,解决冲突合并与 Git 制造的无冲突合并没有什么不同:它仍然只是文件的快照。这种冲突合并的唯一特别之处在于 Git 必须停下来寻求您的帮助才能生成该快照。


2Git 还可以将 一个 提交的快照与存储在任何提交之外的一组普通文件或两组文件都在提交之外,或者任何。但大多数情况下,我们将在这里处理提交中的文件。


使用cherry-pick复制提交的效果

我们现在通过cherry-pick命令进行一次次旅行,其目标是将提交的更改(和提交消息)复制到一些不同的提交(具有不同的哈希 ID,通常在不同的分支上):

        (the cherry)
              |
              v
...--o--o--P--C--o--...   <-- somebranch
      \
       E--F--G--H   <-- our-branch (HEAD)

在这里,我们在分支的顶端使用一些哈希 H 进行提交,并且当我们意识到:嘿,我昨天看到 Bob 修复了这个错误/最后-每周/无论何时。我们意识到我们不需要做任何工作:我们可以在“樱桃”提交C 中复制 Bob 的修复。所以我们运行:

git cherry-pick <hash-of-C>

为了让 Git 完成它的工作,Git 必须比较C 的父级,提交P,提交C。当然,这是git diff 的工作。所以 Git 运行 git diff(使用通常的 --find-renames 等等)来查看 Bob 发生了什么变化。

现在,Git 需要将该更改应用到我们的提交 H。但是:如果在提交H 中需要修复的文件有一堆不相关 的更改会歪曲行号怎么办? Git 需要找到这些更改移动到的位置

有很多方法可以做到这一点,但有一种方法每次都能很好地工作:Git 可以运行git diff比较P 中的快照——我们的樱桃——在我们的提交H 中的快照。这将发现 HP-C 对之间不同的文件中的任何差异,包括插入或删除的长段代码,这些代码移动了 Bob 的修复需要去的地方。

这当然也会带来一堆不相关的变化,其中P-vs-H 是不同的,只是因为它们处于不同的开发线。我们从一些共享(但无趣)的提交开始o;他们进行了一系列更改和提交,导致P;我们进行了一系列更改和提交,EFG,导致我们的提交 H。但是:那又怎样?鉴于git merge 将在完全没有冲突的地方获取我们的 文件,我们将只从H 获取我们的文件。而且,鉴于“我们”和“他们”都更改了一些文件,Git 将“保留我们的更改”从 PH,然后 添加他们的更改PC,它将接收 Bob 的更改。

所以这是关键实现:如果我们运行合并机制,我们会遇到冲突的唯一地方是 Bob 的更改不适合的地方。因此,我们这样做 运行合并机制:

git diff --find-renames <hash-of-P> <hash-of-H>   # what we changed
git diff --find-renames <hash-of-P> <hash-of-C>   # what Bob changed

然后我们让 Git 组合这些更改,将它们应用到“common”或“merge base”提交 P。它不是两个分支共有的事实并不重要。我们得到正确的结果,这就是所做的一切 很重要。

当我们完成“组合”这些更改后(取回我们自己的文件,对于 Bob 没有接触过的文件,并应用 Bob 的更改,对于 Bob 接触过的文件),我们让 Git 在它自己的,如果一切顺利的话。不过,这个新的提交不是合并提交。这只是一个普通的、普通的、日常的提交,与通常的父母:

...--o--o--P--C--o--...   <-- somebranch
      \
       E--F--G--H--I   <-- our-branch (HEAD)

HIgit diff 引入了与从PCgit diff 相同的更改。如有必要,行号可能会移动,如果是这样,移动会使用合并机制自动发生。此外,新的提交 I 重用了来自提交 C提交消息(尽管我们可以使用 git cherry-pick --edit 进行修改)。

如果有冲突怎么办?好吧,考虑一下:如果某个文件 F 中存在冲突,这意味着 Bob 对 F 的修复会影响该文件中的某些行,而这些行在其父文件中是不同的 @ 987654428@ 和我们的提交 H为什么这些行不同?要么我们没有我们可能需要的东西——也许有一些提交之前 C 有一些我们需要的关键设置代码,或者我们确实拥有一些我们不想丢失的东西。所以仅仅接受我们的很少是正确的,因为那样我们就没有得到 Bob 的修复到文件。但是仅仅接受他们的也很少是正确的,因为那样我们错过了一些东西,或者我们失去了我们拥有的东西

还原是向后的樱桃采摘

假设不是这样:

...--o--o--P--C--o--...   <-- somebranch
      \
       E--F--G--H   <-- our-branch (HEAD)

我们有这个:

...--o--o--P--C--D--...   <-- somebranch
                  \
                   E--F--G--H   <-- our-branch (HEAD)

提交C,可能仍然由 Bob 制作,其中有一个错误,摆脱错误的方法是撤消提交 C 的整个更改。

实际上,我们想要做的是 diff CP — 与我们之前为我们的樱桃选择所做的相同的 diff,但向后。现在,我们不是在此处添加一些行来添加一些功能(这实际上是一个错误),而是在此处删除那些相同的行(这会删除错误)。

我们现在希望 Git 将此“向后差异”应用到我们的提交 H。但是,和以前一样,行号可能是关闭的。如果您怀疑合并机制是这里的答案,那么您是对的。

我们所做的是一个简单的技巧:我们选择提交 C 作为“父”,或假合并基础。提交H,我们当前的提交,一如既往地是--oursHEAD 提交,而提交C 的父提交P 是另一个或--theirs 提交。我们运行相同的两个差异,但这次的哈希 ID 略有不同:

git diff --find-renames <hash-of-C> <hash-of-H>   # what we changed
git diff --find-renames <hash-of-C> <hash-of-P>   # "undo Bob's changes"

我们让合并机制像以前一样将这些结合起来。这次合并基础是提交C,我们正在“撤消”的提交。

与任何合并一样,包括来自cherry-pick 的合并,此处的任何冲突都必须仔细考虑。 “他们的”更改是退出提交 C,而“我们的”更改是 不同 P(他们退出时的开始)和我们的提交 @ 987654449@。这里没有皇家捷径,没有-X ours-X theirs,这永远是对的。你只需要考虑一下。

小心-n:考虑不要使用它

如果您在使用git cherry-pickgit revert 时遇到冲突,您必须解决它们。如果你使用-n,你解决它们然后提交。如果您对多个提交执行此操作,您的下一个操作也可能会发生冲突。

如果您已提交,则下一次精选或还原将从您的提交作为HEAD 版本开始。如果您在任何中间版本中出现问题,仅此一项就可能导致冲突;或者,这里可能会出现无论如何都会发生的冲突。只要你解决了这个问题并且也提交了,你就会留下痕迹。您可以返回并查看每个单独的cherry-pick 或revert,看看您是否做对了。

现在,您可以使用git cherry-pick -ngit revert -n在最后跳过提交。如果您这样做,next 挑选或恢复使用您的工作树文件,就好像它们是HEAD-commit 版本一样。这与以前的工作方式相同,但这次,您不会留下任何痕迹。如果出现问题,您无法回顾以前的工作并查看哪里出了问题。

如果你不使用-n,你会得到一系列的提交:

A--B--C--D--E--Ↄ   <-- main (HEAD)

例如,在还原 C 之后。如果你然后去 revert A 并且一切顺利,你可能会得到:

A--B--C--D--E--Ↄ--∀   <-- main (HEAD)

如果您现在说“这很好,但我真的不想在混合中使用 ”,很容易在保持其效果的同时摆脱它,使用 git rebase -i 或 @ 987654467@。例如,git reset --soft 的哈希 ID 为提交 E 会导致:

              Ↄ--∀   ???
             /
A--B--C--D--E   <-- main (HEAD)

但是让 Git 的索引和你的工作树充满了构成提交 内容的文件。所以你现在可以运行git commit 并获得一个新的提交:

              Ↄ--∀   ???
             /
A--B--C--D--E--Ↄ∀   <-- main (HEAD)

其中Ↄ∀ 组合(即挤压)的效果。

如果没有出错,您将不得不执行此压缩操作,但如果确实出了问题,您不必从头开始。

【讨论】:

以上是关于合并来自 git revert 的冲突 - 我应该接受当前的更改还是传入,为啥?的主要内容,如果未能解决你的问题,请参考以下文章

ideasvn没有mergesource绿色加号

Git Revert 然后在修复后重新合并

git merge原理(递归三路合并算法)

git revert和git reset的区别

如果合并或重新定位,则主删除分支文件中的 Git `revert`

卡在git revert上 - 与冲突有关