问题描述
这是我面临的问题
我已经从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 merge
或git rebase
的 strategy 参数为-s
或--strategy
;您在此处使用recursive
,这很好(不是ours
strategy 的策略)。扩展选项为-X
,而ours
或theirs
扩展选项确实有意义,但是这里有一个陷阱:您想要-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
请注意,这里涉及的三个提交是:
- 提交
H
作为合并基础; - 通过
J
将--ours
提交为HEAD
;和 - 通过名称
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 merge
或merge.conflictStyle
告诉Git:当您遇到低级冲突时,只需分别考虑我们或他们的冲突即可解决。对高级冲突没有影响:您仍然必须手动解决这些冲突。
请注意,即使两次更改都没有改变 same 行,也会发生低级冲突。例如,如果原始输入内容为:
-X ours
,爱丽丝将-X theirs
更改为line 1
line 2
line 3
line 4
,而鲍勃将2
更改为two
,Git将这称为冲突。使用3
或three
将丢弃两个更改之一。在继续进行之前,实际测试这样的合并是一个好主意。 (嗯,测试任何合并是一个好主意:仅仅因为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)
的子对象;提交C
在P
之后,N
在C
之后;这些都是通过名称O
找到的。
提交P
是对feature1
的最后一次提交,并且我们现在已检出分支T
。因此,提交feature2
是feature2
提交。
我们需要一些新的代码来应用于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
成为合并基础,提交P
是P
,而T
是--ours
。
合并冲突(如果发生)是由于这两个C
操作引起的。如果确实发生,则索引槽#1包含来自合并基础--theirs
的文件,索引槽#2包含来自git diff
的文件,索引槽#3包含来自P
的文件。 T
的{{1}}选项很有意义,因为T
实际上是我们的 提交。 --ours
选项很有意义,因为git checkout
是我们的承诺。
变基
如上所述,T
的工作方式是列出需要复制的一系列提交的提交哈希ID。然后,它使用Git的分离式HEAD 模式签出一个特定的提交。为说明起见,让我们绘制一个小的基础,只需执行三个提交即可:
-X ours
在这里,我们要复制的提交是T
,git rebase
和 C--D--E <-- branch (HEAD)
/
...--B--F--G <-- mainline
。旧的基础是提交C
。提交D
和E
已添加到主线分支。所以我们运行:
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
作为D
,C
作为C'
。 Git合并更改,将合并的更改应用于现有提交--ours
,并进行新提交D
:
--theirs
现在,Git成为C'
的首选:D'
是合并基础, C--D--E <-- branch
/
...--B--F--G <-- mainline
\
C'-D' <-- HEAD
是E
,而D
是D'
,并且新的提交完成了复制过程:
--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
。