git使用“我们的”合并策略重新设置基础,提示重新设置基础-一次又一次继续 发生了什么事回顾樱桃采摘变基注意E'的含义

问题描述

这是我面临的问题

我已经从master分支创建了一个功能分支。我在Feature分支上进行了大量工作,它比master分支提前80次提交。在这些提交中,我多次编辑了一些文件。几天后,有人在master分支上推送了两次提交,因此由于合并冲突,无法合并feature分支的Pull Request。

我尝试通过变基来掌握和解决合并冲突,但是在git rebase --continue之后我越来越多地遇到冲突

govi@falcon:/home/my_user/project/ (feature/xyz): git rebase master
govi@falcon:/home/my_user/project/ (feature/xyz | REBASE 32/85):

在这里,如果有任何合并冲突,我想选择我的更改。因此我尝试了ours递归模式下的冲突解决策略。现在git并没有强迫我解决任何冲突,而是要我执行git rebase --continues近80次。

govi@falcon:/home/my_user/project/ (feature/xyz): git rebase master -s recursive -X ours
govi@falcon:/home/my_user/project/ (feature/xyz | REBASE-i 1/85)
Last command done (1 command done):
     pick db2511c Modify file
Next command to do (1 remaining command):
     pick d1c2037 Modify file one more time

在上述情况下,是否有更好的方法解决合并冲突?还是一个更好的基准调整方法

PS:我们不允许重置主分支。我知道简单的方法是在功能分支上执行{reset,stash,rebase,pop},但是PR已经在进行中。

解决方法

TL; DR

您真的想要-X theirs为什么你想要...很长。

首先,请注意一点:小心使用术语:您不是在使用ours strategy ,而是在使用ours strategy选项 。我在这里发现Git的术语令人困惑,并且更喜欢将这些-X选项称为扩展选项,以避免重复 strategy 一词。

现在,继续讨论问题本身。实际上,使用git rebase时,实际上是在重复运行git cherry-pick。每个“自动选择”操作都复制一个提交。 git rebase通过复制多个提交来工作。 git rebase命令首先列出要复制的所有提交的哈希ID,然后将其保存到内部“待办事项”文件中。这些文件随后随着rebase的进展而更新。

(这些文件的详细信息多年来已经更改,描述它们没有任何实质意义。但是,您的shell提示符设置似乎可以正确读取这些待办事项和进度文件,基于“ 1/85”和您在这里看到的是“ 32/85”。

从技术上讲,樱桃拾取操作是成熟的三向合并,因此可能产生合并冲突。但是这里必须要非常小心。您写道:

git rebase master -s recursive -X ours

git mergegit rebase strategy 参数为-s--strategy;您在此处使用recursive,这很好(不是ours strategy 的策略)。扩展选项为-X,而ourstheirs 扩展选项确实有意义,但是这里有一个陷阱:您想要-X theirs

发生了什么事

在深入探究之前,让我们看一下git merge。如果不首先查看git merge,那么Cherry-pick所做的某些事情根本就没有意义。

要执行git merge操作,我们先进行一系列的提交,例如,两个不同的开发人员以相同的初始提交链开始:

...--F--G--H   <-- main

这两个开发者who we'll call Alice and Bob in the usual way,各自进行了一些新的提交。我将从爱丽丝的角度在这里工作:

       I--J   <-- alice (HEAD)
      /
...--H
      \
       K--L   <-- bob

这时,爱丽丝可能会合并鲍勃的作品。她的提交J已签出,分支名称HEAD后面附加了特殊名称alice;她现在运行git merge bob以合并Bob的提交L

git merge命令(从技术上讲,这是recursive策略,而不是git merge本身)使用分支名称L定位提交bob。该提交成为第三提交。 Git使用特殊名称J来定位提交HEAD,这将成为 second 提交。最后-变成第一个-它在提交图上向后向后工作 来找到最佳公共提交,在本例中为提交H

每个提交都具有Git知道的每个文件的完整快照,无论何时进行提交。因此,Git现在可以轻松地将合并基础提交H中的快照与Alice提交J中的快照进行比较,然后对Bob的提交L做相同的事情:

git diff --find-renames <hash-of-H> <hash-of-J>   # what Alice changed
git diff --find-renames <hash-of-H> <hash-of-L>   # what Bob changed

请注意,这里涉及的三个提交是:

  1. 提交H作为合并基础;
  2. 通过J--ours提交为HEAD;和
  3. 通过名称L--theirs的身份提交bob

merge命令-将合并为动词的一部分,即-现在组合我们的更改H-vs-{{1} }及其更改,J-vs-H。正是这种合并过程会产生合并冲突。

但是,在没有合并冲突的情况下,Git可以将合并的更改自动应用于文件,如合并基础提交L所示。这样可以在添加更改的同时保留我们的更改,这当然正是我们希望从合并中获取的内容。

存在合并冲突时,H在合并的中间停止。它将所有三个输入文件保留在Git的索引中:索引插槽#1包含基本提交副本,插槽#2包含git merge的{​​{1}}副本,插槽#3包含--ours的副本从我们用HEAD命令命名的提交中。

Git将最有效的方式写入冲突文件的工作树版本。 Git能够自行组合更改的地方已经包含该组合。 Git发现我们与他们的冲突的地方带有冲突标记,并且取决于您设置--theirs的方式,有两个甚至三个输入文件的行。

我将这类冲突称为“低级冲突”。 (Git在内部称它们为此类。)还有我所说的高级冲突,例如当一侧(我们或他们的一侧)修改和/或重命名文件,而另一侧时删除它。

使用扩展选项git mergemerge.conflictStyle告诉Git:当您遇到低级冲突时,只需分别考虑我们或他们的冲突即可解决。对高级冲突没有影响:您仍然必须手动解决这些冲突。

请注意,即使两次更改都没有改变 same 行,也会发生低级冲突。例如,如果原始输入内容为:

-X ours

,爱丽丝将-X theirs更改为line 1 line 2 line 3 line 4 ,而鲍勃将2更改为two,Git将这称为冲突。使用3three将丢弃两个更改之一。在继续进行之前,实际测试这样的合并是一个好主意。 (嗯,测试任何合并是一个好主意:仅仅因为Git认为可以将两组不同的更改组合在一起就可以了,但这并不意味着它确实是

回顾

以上内容(如有必要,请重新阅读)是

  • -X ours负责所有工作;我们在这里谈论的是-X theirs(尽管-s strategy做的是同样的事情)。
  • 合并操作具有三个输入:base =#1,-s recursive或HEAD =#2,-s resolve =#3。
  • 无论ours选项如何,Git都会自行组合无冲突的更改。
  • 无论是否有theirs选项,Git都将在发生高级冲突时停止。
  • -X选项将有利于“我们的”(#1-vs-#2)或“他们的”(#1-vs-#3)来解决低级冲突。

樱桃采摘

我们现在准备看看-X的实际作用。摘樱桃的动作通常描述为重复上次提交的更改。尽管这捕获了目标,但并未涵盖机制。直到发生合并冲突,该机制才变得无关紧要,然后突然变得非常重要。

要讨论该机制,让我们绘制另一个提交图片段。这次,不是让Alice和Bob从某个共同的出发点-X出发,而是让我们来看一个或两个使用两种不同功能的程序员,例如:

git cherry-pick

提交H是父提交...--P--C--N--O <-- feature1 ...--R--S--T <-- feature2 (HEAD) 的子对象;提交CP之后,NC之后;这些都是通过名称O找到的。

提交P是对feature1的最后一次提交,并且我们现在已检出分支T。因此,提交feature2feature2提交。

我们需要一些新的代码来应用于T,我们意识到:等等,我只是看到该代码,或者是上周写的。它在提交HEAD中!因此,我们运行T来查找提交C的实际哈希ID,然后运行:

git log

复制该提交。

为了进行复制(以找出父提交C和子提交git cherry-pick <hash-of-C> 之间的变化),Git将运行与复制相同的P我们上面看到的是C。但这只是他们的改变。为了将其更改应用于我们的提交,Git首先将运行另一个git diff --find-renames,这次将父git merge与我们当前的/ HEAD提交git diff --find-renames进行比较。

换句话说,Git运行:

P

现在,Git使用与往常相同的合并引擎(T)合并更改 ,并将合并的更改应用于git diff --find-renames <hash-of-P> <hash-of-T> # what we changed git diff --find-renames <hash-of-P> <hash-of-C> # what they changed 中的快照。这样可以保留我们的工作,并增加他们的更改。提交-s recursive成为合并基础,提交PP,而T--ours

合并冲突(如果发生)是由于这两个C操作引起的。如果确实发生,则索引槽#1包含来自合并基础--theirs的文件,索引槽#2包含来自git diff的文件,索引槽#3包含来自P的文件。 T的{​​{1}}选项很有意义,因为T实际上是我们的 提交。 --ours选项很有意义,因为git checkout是我们的承诺。

变基

如上所述,T的工作方式是列出需要复制的一系列提交的提交哈希ID。然后,它使用Git的分离式HEAD 模式签出一个特定的提交。为说明起见,让我们绘制一个小的基础,只需执行三个提交即可:

-X ours

在这里,我们要复制的提交是Tgit rebase C--D--E <-- branch (HEAD) / ...--B--F--G <-- mainline 。旧的基础是提交C。提交DE已添加到主线分支。所以我们运行:

B

Git使用当前提交F并向后工作以查找要复制的三个提交,而使用名称G并反向工作以发现该提交git checkout branch git rebase mainline 是共享提交复制停止。然后,Git使用名称E进入分离的HEAD模式:

mainline

Git现在准备好复制提交B。在内部,此时,Git运行mainline,而 C--D--E <-- branch / ...--B--F--G <-- HEAD,mainline 完成其工作。

如果一切顺利,cherry-pick运行的“合并”将起作用:Git将基础C与“我们的”提交git cherry-pick <hash-of-C>进行比较,将基础git cherry-pick与“他们的”提交进行比较B,结合提交G的两个区别,并进行一个新的提交,我们将其称为B

C

Git现在通过提交B重复此操作。 “合并”使用提交C'作为其合并基础, C--D--E <-- branch / ...--B--F--G <-- mainline \ C' <-- HEAD 作为DC作为C'。 Git合并更改,将合并的更改应用于现有提交--ours,并进行新提交D

--theirs

现在,Git成为C'的首选:D'是合并基础, C--D--E <-- branch / ...--B--F--G <-- mainline \ C'-D' <-- HEAD E,而DD',并且新的提交完成了复制过程:

--ours

复制完成后,E现在只需从旧的提示提交--theirs中删除名称 C--D--E <-- branch / ...--B--F--G <-- mainline \ C'-D'-E' <-- HEAD ,并使其指向git rebase当前的提交名称,即branch,然后重新附加E,使所有内容看起来都正常:

HEAD

注意E'的含义

在重新选择基础的过程中,HEAD指的是:

  • 首先提交 C--D--E [abandoned] / ...--B--F--G <-- mainline \ C'-D'-E' <-- branch (HEAD)
  • 然后提交--ours
  • 然后提交--ours

因此G首先引用其提交C',然后引用我们在新分支上构建的提交

D'提交依次为--ours,然后依次为G,然后依次为--theirs。因此C总是引用我们的提交。

合并基础提交的顺序依次为D,然后依次为E--theirs。没有B选项可以引用它们,但是第一个是“他们的”提交,另外两个是我们的。

如果我们想覆盖“他们的”(C)分支更改,那么大多数时候我们需要使用D,而不是--base