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

Merge conflicts from git revert - Should I accept current change or incoming and why?

我有这样的提交 - A <- B <- C <- D <- E <- Head

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

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

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

我遇到了合并冲突,我不确定是应该接受当前更改还是即将到来的更改,因为这实际上是倒退。

TL;DR

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

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

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

(假设您在分支 main 上,因此名称 main 选择提交 E)。但我通常懒得画连接线,因为这样更容易,而且带有对角箭头的箭头字体效果不是很好,而 \/ 用于倾斜连接线效果很好。

无论如何,“向后”恢复的原因是如果我们想撤消提交E和[=540=的效果 ] git revert E 提交 Ǝ:

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

在提交 Ǝ 中生成的 源快照 将与提交 D 中的源快照完全匹配。这意味着我们现在可以 运行 git revert D 并获得“撤消” D 效果的提交,而不会出现任何合并冲突。生成的快照与 C 中的相匹配,使得还原 C 变得微不足道,从而产生与 B 相匹配的快照,依此类推。

换句话说,通过以相反的顺序恢复,我们确保我们永远不会有任何冲突。没有 冲突 ,我们的工作就容易多了。

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

话虽如此,让我们开始您的问题……好吧,几乎您的问题。 cherry-pick 和 revert 都实现了 作为 一个 three-way 合并操作。要正确理解这一点,我们首先需要了解 Git 如何执行 three-way 合并,以及它为何有效(以及何时有效,以及冲突意味着什么)。


1这是必要的原因是任何提交的任何部分都不能更改,即使是 Git 本身。由于较早的提交一旦完成就固定不变,因此无法返回并使其 link 成为较晚的提交。


一个标准git merge

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

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

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

我们知道每个提交都包含一个完整的快照——不是一组更改,而是一个快照——文件被压缩并且 de-duplicated 否则 Git-ified。但是每个提交 代表 一些变化:通过比较 HI 中的快照,例如,我们可以看到谁提交了 I 修复了 README 文件中单词的拼写,例如第 17 行。

所有这些意味着要查看更改,Git 总是必须比较两个提交2 鉴于这一现实,很容易看出 Git 可以通过比较最好的 [=280] 来找出 我们 branch1 上发生了什么变化=]shared commit, commit H, to our last commit, commit J.无论这里的文件有何不同,无论我们我们做了什么改变,这些都是我们的改变。

同时,合并的目标是合并更改。所以 Git 应该 运行 这个差异——两次提交的比较——来查看 我们的 更改,但也应该 运行 类似的差异来查看 他们的改变。要查看 他们 发生了什么变化,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 文件 版本。如果他们更改了一些文件而我们没有,那就意味着使用他们的文件版本。如果我们都触及 相同的 文件,Git 必须弄清楚如何组合这些更改,如果没有人触及某些文件——如果 所有三个版本都匹配—Git这三个版本中的任何一个都可以。

这些给了 Git 一堆 short-cuts。合并我们的更改的缓慢而简单的方法是从 H 本身提取所有文件,在不冲突的地方应用我们和他们的更改,并在 的地方应用带有冲突标记的冲突更改做冲突。 Git 的实际作用与此相同。如果没有任何冲突,生成的文件都准备好进入新的 merge commit M:

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

新提交成为 branch1 的最后一次提交。它 link 回到提交 J,就像任何新提交的方式一样,但它 link 回到提交 L ,该提交目前仍是 branch2.

的最后一次提交

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

请注意,提交 M 包含最终合并结果:所有文件的简单快照,包含 correctly-merged 内容。提交 M 只有一个方面是特殊的:它有 一个 parent J 而不是 两个 parents,JL

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

git merge --continue

或:

git commit

merge --continue 只是确保有一个合并要完成,然后 运行s git commit 给你,所以效果是一样的)。这会使用您在解决所有冲突时提供的快照提交 M。请注意,最后,resolved-conflict 合并与 Git-made、no-conflict 合并没有什么不同:它仍然只是文件的快照。此冲突合并的唯一特殊之处在于 Git 必须停下来并寻求您的帮助才能得出该快照。


2Git 还可以将 one 提交的快照与存储在任何提交之外的一组普通文件进行比较,或者两组文件都在提交之外,或者其他什么。但大多数情况下,我们将在这里使用 files-in-commits。


复制 cherry-pick

提交的效果

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

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

在这里,我们在我们分支的顶端使用一些散列 H 进行一些提交,当我们意识到以下内容时即将做一些工作:嘿,我看到 Bob 修复了这个错误是昨天/last-week/每当。我们意识到我们不需要做任何工作:我们可以在“cherry”提交 C 中复制 Bob 的修复。所以我们 运行:

git cherry-pick <hash-of-C>

为了 Git 完成它的工作,Git 必须 比较 C 的 parent,提交 P,提交 C。当然,这是 git diff 的工作。所以 Git 运行s git diff (用通常的 --find-renames 等等)看看 Bob 改变了什么。

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

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

这当然会出现一堆 不相关的 变化,其中 P-vs-H 不同只是因为它们'在不同的发展路线上。我们从一些共享的(但无趣的)提交开始 o;他们进行了一系列更改和提交,导致 P;我们进行了一系列更改和提交,EF 以及 G,导致我们的提交 H。但是:那又怎样?鉴于git merge 将在完全没有冲突的地方获取 我们的 文件,我们将只从 H 获取我们的文件。并且,鉴于“我们”和“他们”都更改了一些文件,Git 将“保留我们的更改”从 PH,然后 添加他们的将 P 更改为 C,这将采用 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 组合这些更改,将它们应用到“公共”或“合并基础”提交 P。它 不是 两个分支所共有的事实 无关紧要。 我们得到正确的 结果 ,这就是 的全部意义。

当我们完成“组合”这些更改后(取回我们自己的文件,对于 Bob 未触及的文件,并应用 Bob 的更改,对于 Bob 触及的文件),我们有 Git 如果一切顺利,自己进行新的提交。这个新提交 不是 合并提交。这只是一个常规的、普通的、日常的提交,通常是 parent:

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

HIgit diff 引入了与 Pgit diff 相同的 变化 C行号可能会在必要时移动,如果是这样,moving-about会使用合并机制自动发生。此外,新提交 I re-uses 来自提交 C 提交消息 (尽管我们可以用 git cherry-pick --edit 修改它) .

如果有冲突怎么办?好吧,想一想:如果某个文件 F 中存在冲突,这意味着 Bob 对 F 的修复会影响该文件中的某些行他们的 parent P 和我们的提交 H 不同。 为什么这些线不同?要么我们没有我们可能需要的东西——也许有一些提交 before C 有一些我们需要的关键设置代码——或者有一些我们 do 有的东西,我们不想。所以只接受我们的很少是正确的,因为这样我们就不会获得 Bob 对文件的修复。但仅仅接受他们的也很少是正确的,因为那样我们就会失去一些东西,或者我们失去一些我们拥有的东西

还原是向后的cherry-picking

假设不是这样:

...--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.

实际上,我们想要做的是 CP 的差异——与我们之前为 cherry-pick 所做的相同的差异,但倒退了。现在,不是 在此处添加一些行 来添加一些功能(这实际上是一个错误),而是 在此处删除相同的行(这会删除错误)。

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

我们做的是一个简单的技巧:我们选择提交 C 作为“parent”,或者伪合并基础。提交H,我们当前的提交,一如既往地是--oursHEAD提交,提交P,提交C的parent,是另一个或 --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"

我们有合并机制将它们结合起来,就像以前一样。这次 merge base 是提交 C,我们正在“撤销”的提交。

对于任何合并,包括来自 cherry-pick 的合并,都必须仔细考虑此处的任何冲突。 “他们的”更改是支持提交 C 的内容,而“我们的”更改是 P 不同的内容——他们返回时的开始这就是我们的承诺 H。这里没有皇室short-cut,没有-X ours-X theirs,那永远是对的。你只需要考虑一下。

谨慎使用-n:考虑不使用它

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

如果您提交,下一个 cherry-pick 或恢复将从您提交的 HEAD 版本开始。如果您在任何中间版本中出现问题,仅此一项就可能导致冲突;或者,这里可能会发生冲突无论如何。只要你解决了这个问题并且也做出了承诺,你就会留下痕迹。您可以返回并查看 每个人 cherry-pick 或返回 看看您是否正确。

现在,您可以使用git cherry-pick -ngit revert -n跳过末尾的提交。如果您这样做,next cherry-pick 或 revert 将使用您的 工作树文件 ,就好像它们是 HEAD-提交版本。这与以前的工作方式相同,但这次,你没有留下踪迹。如果出了什么问题,你不能回头看看你以前的工作,看看哪里哪里出了问题。

如果你离开 -n,你将得到一系列的提交:

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

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

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

如果您现在说“这很好,但我真的不想 混在一起”,很容易在保持效果的同时摆脱它 ,使用 git rebase -igit reset --soft。例如,具有提交哈希 ID Egit reset --soft 会导致:

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

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

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

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

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