git rebase with 'ours' 合并策略提示 rebase --continue 一次又一次
git rebase with 'ours' merge strategy prompting to rebase --continue again and again
这是我面临的问题
我从 master
分支创建了一个功能分支。我在一个功能分支上进行了大量工作,它比主分支早了 80 次提交。在这些提交中,我多次编辑了一些文件。过了几天,有人在master分支上push了几个commit,feature分支的Pull Request因为合并冲突无法合并
我尝试通过rebase来掌握和解决合并冲突,但是在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):
对于任何合并冲突,我想 select 我的更改。所以我在递归模式下尝试了 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:我们不允许重置master分支。我知道简单的方法是在功能分支上执行 {reset, stash, rebase, pop}
,但 PR 已经在进行中。
TL;DR
你真的很想要-X theirs
。 为什么你想要的是……长。
长
首先,一个初步的旁注:注意术语:您没有使用 ours
策略 而是 ours
策略选项。我发现 Git 的术语在这里令人困惑,并且更喜欢将这些 -X
选项称为 扩展选项 ,以避免重复单词 strategy.
现在,回到问题本身。当使用 git rebase
时,您实际上是在重复 运行ning git cherry-pick
。每个 cherry-pick 操作复制一个提交; git rebase
通过复制多个提交来工作。 git rebase
命令首先列出要复制的所有提交的哈希 ID,将它们保存到内部“to-do”文件中。随着 rebase 的进展,这些文件会得到更新。
(这些文件的详细信息多年来发生了变化,描述它们没有实际意义。但是,您的 shell 提示设置似乎可以正确读取这些 to-do 和进度文件,基于在您在这里看到的“1/85”和“32/85”上。)
cherry-pick 操作在技术上是 full-blown three-way 合并,因此会产生合并冲突。但是在这里必须非常小心。您写道:
git rebase master -s recursive -X ours
git merge
或git rebase
的策略参数是-s
或--strategy
;你在这里使用 recursive
,这很好(ours
策略 不是)。扩展选项是 -X
,ours
或 theirs
扩展选项 确实有意义——但这里有一个陷阱:你想要 -X theirs
.
怎么回事
在深入研究 cherry-pick 之前,让我们先看看 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
此时,Alice 可能会合并 Bob 的工作。她检查了她的提交 J
,分支名称 alice
附加了特殊名称 HEAD
;她现在 运行s git merge bob
合并 Bob 的提交 L
.
git merge
命令——从技术上讲,这是 recursive
策略而不是 git merge
本身——使用分支名称 bob
定位提交 L
。此提交成为 third 提交。 Git 使用特殊名称 HEAD
定位提交 J
,这成为 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
;和
- 通过名称
bob
.L
作为 --theirs
提交 L
。
合并命令——合并为动词的一部分,即——现在合并我们的更改,H
-vs-J
,随着他们的变化,H
-vs-L
。正是这个合并过程会产生合并冲突。
虽然 没有 合并冲突,Git 可以自动将合并的更改应用到合并基础提交中看到的文件H
。这在添加更改的同时保留了我们的更改,这当然正是我们想要的合并。
当有合并冲突时,git merge
在合并中途停止。它在 Git 的索引中留下所有三个输入文件:索引槽 #1 包含基本提交副本,槽 #2 包含来自 HEAD
的 --ours
副本,槽 #3 包含--theirs
从我们使用 git merge
命令命名的提交中复制。
Git 将尽最大努力写入冲突文件的 work-tree 版本。 Git 能够自行组合更改的地方已经包含该组合。 Git 发现 ours-vs-theirs 冲突的地方有冲突标记和两个,甚至所有三个输入文件行,具体取决于您如何设置 merge.conflictStyle
.
我把这些类型的冲突称为低级冲突。 (Git 在内部这样称呼它们。)还有我所说的 高级别 冲突,例如当一方——我们的或他们的——修改 and/or 重命名一个文件,对方删除了
使用扩展选项,-X ours
或 -X theirs
,告诉 Git:当你遇到 low-level 冲突时,只需分别采用我们的或他们的来解决它。这对高级冲突没有影响:您仍然必须手动解决这些冲突。
请注意,即使两个更改未同时更改 相同的 行,也会发生 low-level 冲突。例如,如果原始输入为:
line 1
line 2
line 3
line 4
并且 Alice 将 2
更改为 two
,而 Bob 将 3
更改为 three
,Git 将此称为 合并冲突。使用 -X ours
或 -X theirs
将放弃两个更改之一。在继续之前实际测试此类合并是个好主意。 (好吧,测试 any 合并是个好主意:只是因为 Git 认为可以合并两组不同的更改,并不意味着它真的 是 好。)
回顾
上面的内容——re-read如果需要的话——是:
-s strategy
负责所有工作;我们在这里谈论 -s recursive
(尽管 -s resolve
做同样的事情)。
- 合并操作有三个输入:base = #1,
ours
or HEAD = #2, theirs
= #3.
- Git 将自行合并无冲突的更改,无论
-X
选项如何。
- Git 将因 high-level 冲突而停止,无论
-X
选项如何。
-X
选项将支持“我们的”(#1-vs-#2)或“他们的”(#1-vs-#3)来解决 low-level 冲突。
Cherry-pick
我们现在准备看看 git cherry-pick
到底做了什么。 cherry-pick 的操作通常被描述为 重复上一次提交的更改 。虽然这捕获了 目标 ,但并未涵盖 机制 。在发生合并冲突之前,该机制是无关紧要的,然后突然变得非常重要。
说到机制,再画一个提交图片段。这一次,我们不再让 Alice 和 Bob 从一些共同的起点出发 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
! 所以我们 运行 git log
找到提交 C
的实际哈希 ID,然后 运行:
git cherry-pick <hash-of-C>
复制那个提交。
为了做复制——找出父提交P
和子提交C
之间发生了什么变化——Git将运行 与我们在上面看到的 git merge
相同的 git diff --find-renames
。但这只是 他们的改变。为了将他们的更改应用到我们的提交中,Git 将首先 运行 另一个 git diff --find-renames
,这次将父 P
与我们当前的 / HEAD 提交 T
进行比较。
换句话说,Git 运行s:
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
现在 Git 合并更改 ,使用与往常相同的合并引擎 (-s recursive
),并将合并的更改应用于快照P
。这会保留我们的工作,并添加他们的更改。提交 P
成为合并基础,提交 T
是 --ours
而 C
是 --theirs
.
合并冲突(如果有的话)是因为这两个 git diff
操作。如果确实发生,索引槽 #1 包含来自合并基础 P
的文件,槽 #2 包含我们来自 T
的文件,槽 #3 包含他们来自 T
的文件。 git checkout
的 --ours
选项是有道理的,因为 T
确实是 我们的 提交。 -X ours
选项有意义,因为 T
是我们的提交。
变基
如上所述,git rebase
的工作方式是列出需要复制的一些提交系列的提交哈希ID。然后它使用 Git 的 detached HEAD 模式来检查一个特定的提交。为了说明,让我们画一个只有三个提交的小变基:
C--D--E <-- branch (HEAD)
/
...--B--F--G <-- mainline
在这里,我们要复制的提交是 C
、D
和 E
。旧基础是提交 B
。提交 F
和 G
已添加到主线分支。所以我们 运行:
git checkout branch
git rebase mainline
Git 使用当前提交 E
并向后查找要复制的三个提交,同时使用名称 mainline
并向后查找提交 B
是复制停止时的共享提交。然后,Git 使用名称 mainline
进入分离 HEAD 模式:
C--D--E <-- branch
/
...--B--F--G <-- HEAD, mainline
Git 现在已准备好复制提交 C
。在内部,此时 Git 运行s git cherry-pick <hash-of-C>
和 git cherry-pick
执行它的ng.
如果一切顺利,cherry-pick 运行 的“合并”会起作用:Git 将基础 B
与“我们的”提交 G
进行比较,将基础 B
与“他们的”提交 C
进行比较,在提交 B
之上结合这两个差异,并创建一个新的提交,我们将其称为 C'
:
C--D--E <-- branch
/
...--B--F--G <-- mainline
\
C' <-- HEAD
Git 现在用提交 D
重复这个。 “合并”使用提交 C
作为合并基础,C'
作为 --ours
,D
作为 --theirs
。 Git 合并更改,将合并的更改应用到现有提交 C'
,并进行新提交 D'
:
C--D--E <-- branch
/
...--B--F--G <-- mainline
\
C'-D' <-- HEAD
Git 现在cherry-picks E
: D
是合并基础,D'
是--ours
,E
为--theirs
,新commit完成复制过程:
C--D--E <-- branch
/
...--B--F--G <-- mainline
\
C'-D'-E' <-- HEAD
复制完成后,git rebase
现在只需要从旧的提示提交 E
中提取名称 branch
,并使其指向 [=47= 的提交] 当前名称,即 E'
和 re-attach HEAD
以使一切看起来正常:
C--D--E [abandoned]
/
...--B--F--G <-- mainline
\
C'-D'-E' <-- branch (HEAD)
注意 --ours
的意思
在变基的 cherry-picking 部分,--ours
指的是:
- 首先提交
G
- 然后提交
C'
,
- 然后提交
D'
.
所以 --ours
首先引用他们的提交 G
,然后引用我们自己的提交 建立在新分支 .
--theirs
次提交的顺序是 C
,然后是 D
,然后是 E
。所以 --theirs
总是指 我们的 提交。
合并基础提交的顺序是 B
,然后是 C
,然后是 D
。没有 --base
选项来引用这些,但第一个是“他们的”提交,另外两个是我们的。
如果我们想覆盖“他们的”(mainline
) 分支更改,那么,大多数时候我们需要使用 --theirs
,而不是 --ours
。
这是我面临的问题
我从 master
分支创建了一个功能分支。我在一个功能分支上进行了大量工作,它比主分支早了 80 次提交。在这些提交中,我多次编辑了一些文件。过了几天,有人在master分支上push了几个commit,feature分支的Pull Request因为合并冲突无法合并
我尝试通过rebase来掌握和解决合并冲突,但是在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):
对于任何合并冲突,我想 select 我的更改。所以我在递归模式下尝试了 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:我们不允许重置master分支。我知道简单的方法是在功能分支上执行 {reset, stash, rebase, pop}
,但 PR 已经在进行中。
TL;DR
你真的很想要-X theirs
。 为什么你想要的是……长。
长
首先,一个初步的旁注:注意术语:您没有使用 ours
策略 而是 ours
策略选项。我发现 Git 的术语在这里令人困惑,并且更喜欢将这些 -X
选项称为 扩展选项 ,以避免重复单词 strategy.
现在,回到问题本身。当使用 git rebase
时,您实际上是在重复 运行ning git cherry-pick
。每个 cherry-pick 操作复制一个提交; git rebase
通过复制多个提交来工作。 git rebase
命令首先列出要复制的所有提交的哈希 ID,将它们保存到内部“to-do”文件中。随着 rebase 的进展,这些文件会得到更新。
(这些文件的详细信息多年来发生了变化,描述它们没有实际意义。但是,您的 shell 提示设置似乎可以正确读取这些 to-do 和进度文件,基于在您在这里看到的“1/85”和“32/85”上。)
cherry-pick 操作在技术上是 full-blown three-way 合并,因此会产生合并冲突。但是在这里必须非常小心。您写道:
git rebase master -s recursive -X ours
git merge
或git rebase
的策略参数是-s
或--strategy
;你在这里使用 recursive
,这很好(ours
策略 不是)。扩展选项是 -X
,ours
或 theirs
扩展选项 确实有意义——但这里有一个陷阱:你想要 -X theirs
.
怎么回事
在深入研究 cherry-pick 之前,让我们先看看 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
此时,Alice 可能会合并 Bob 的工作。她检查了她的提交 J
,分支名称 alice
附加了特殊名称 HEAD
;她现在 运行s git merge bob
合并 Bob 的提交 L
.
git merge
命令——从技术上讲,这是 recursive
策略而不是 git merge
本身——使用分支名称 bob
定位提交 L
。此提交成为 third 提交。 Git 使用特殊名称 HEAD
定位提交 J
,这成为 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
;和 - 通过名称
bob
.L
作为--theirs
提交L
。
合并命令——合并为动词的一部分,即——现在合并我们的更改,H
-vs-J
,随着他们的变化,H
-vs-L
。正是这个合并过程会产生合并冲突。
虽然 没有 合并冲突,Git 可以自动将合并的更改应用到合并基础提交中看到的文件H
。这在添加更改的同时保留了我们的更改,这当然正是我们想要的合并。
当有合并冲突时,git merge
在合并中途停止。它在 Git 的索引中留下所有三个输入文件:索引槽 #1 包含基本提交副本,槽 #2 包含来自 HEAD
的 --ours
副本,槽 #3 包含--theirs
从我们使用 git merge
命令命名的提交中复制。
Git 将尽最大努力写入冲突文件的 work-tree 版本。 Git 能够自行组合更改的地方已经包含该组合。 Git 发现 ours-vs-theirs 冲突的地方有冲突标记和两个,甚至所有三个输入文件行,具体取决于您如何设置 merge.conflictStyle
.
我把这些类型的冲突称为低级冲突。 (Git 在内部这样称呼它们。)还有我所说的 高级别 冲突,例如当一方——我们的或他们的——修改 and/or 重命名一个文件,对方删除了
使用扩展选项,-X ours
或 -X theirs
,告诉 Git:当你遇到 low-level 冲突时,只需分别采用我们的或他们的来解决它。这对高级冲突没有影响:您仍然必须手动解决这些冲突。
请注意,即使两个更改未同时更改 相同的 行,也会发生 low-level 冲突。例如,如果原始输入为:
line 1
line 2
line 3
line 4
并且 Alice 将 2
更改为 two
,而 Bob 将 3
更改为 three
,Git 将此称为 合并冲突。使用 -X ours
或 -X theirs
将放弃两个更改之一。在继续之前实际测试此类合并是个好主意。 (好吧,测试 any 合并是个好主意:只是因为 Git 认为可以合并两组不同的更改,并不意味着它真的 是 好。)
回顾
上面的内容——re-read如果需要的话——是:
-s strategy
负责所有工作;我们在这里谈论-s recursive
(尽管-s resolve
做同样的事情)。- 合并操作有三个输入:base = #1,
ours
or HEAD = #2,theirs
= #3. - Git 将自行合并无冲突的更改,无论
-X
选项如何。 - Git 将因 high-level 冲突而停止,无论
-X
选项如何。 -X
选项将支持“我们的”(#1-vs-#2)或“他们的”(#1-vs-#3)来解决 low-level 冲突。
Cherry-pick
我们现在准备看看 git cherry-pick
到底做了什么。 cherry-pick 的操作通常被描述为 重复上一次提交的更改 。虽然这捕获了 目标 ,但并未涵盖 机制 。在发生合并冲突之前,该机制是无关紧要的,然后突然变得非常重要。
说到机制,再画一个提交图片段。这一次,我们不再让 Alice 和 Bob 从一些共同的起点出发 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
! 所以我们 运行 git log
找到提交 C
的实际哈希 ID,然后 运行:
git cherry-pick <hash-of-C>
复制那个提交。
为了做复制——找出父提交P
和子提交C
之间发生了什么变化——Git将运行 与我们在上面看到的 git merge
相同的 git diff --find-renames
。但这只是 他们的改变。为了将他们的更改应用到我们的提交中,Git 将首先 运行 另一个 git diff --find-renames
,这次将父 P
与我们当前的 / HEAD 提交 T
进行比较。
换句话说,Git 运行s:
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
现在 Git 合并更改 ,使用与往常相同的合并引擎 (-s recursive
),并将合并的更改应用于快照P
。这会保留我们的工作,并添加他们的更改。提交 P
成为合并基础,提交 T
是 --ours
而 C
是 --theirs
.
合并冲突(如果有的话)是因为这两个 git diff
操作。如果确实发生,索引槽 #1 包含来自合并基础 P
的文件,槽 #2 包含我们来自 T
的文件,槽 #3 包含他们来自 T
的文件。 git checkout
的 --ours
选项是有道理的,因为 T
确实是 我们的 提交。 -X ours
选项有意义,因为 T
是我们的提交。
变基
如上所述,git rebase
的工作方式是列出需要复制的一些提交系列的提交哈希ID。然后它使用 Git 的 detached HEAD 模式来检查一个特定的提交。为了说明,让我们画一个只有三个提交的小变基:
C--D--E <-- branch (HEAD)
/
...--B--F--G <-- mainline
在这里,我们要复制的提交是 C
、D
和 E
。旧基础是提交 B
。提交 F
和 G
已添加到主线分支。所以我们 运行:
git checkout branch
git rebase mainline
Git 使用当前提交 E
并向后查找要复制的三个提交,同时使用名称 mainline
并向后查找提交 B
是复制停止时的共享提交。然后,Git 使用名称 mainline
进入分离 HEAD 模式:
C--D--E <-- branch
/
...--B--F--G <-- HEAD, mainline
Git 现在已准备好复制提交 C
。在内部,此时 Git 运行s git cherry-pick <hash-of-C>
和 git cherry-pick
执行它的ng.
如果一切顺利,cherry-pick 运行 的“合并”会起作用:Git 将基础 B
与“我们的”提交 G
进行比较,将基础 B
与“他们的”提交 C
进行比较,在提交 B
之上结合这两个差异,并创建一个新的提交,我们将其称为 C'
:
C--D--E <-- branch
/
...--B--F--G <-- mainline
\
C' <-- HEAD
Git 现在用提交 D
重复这个。 “合并”使用提交 C
作为合并基础,C'
作为 --ours
,D
作为 --theirs
。 Git 合并更改,将合并的更改应用到现有提交 C'
,并进行新提交 D'
:
C--D--E <-- branch
/
...--B--F--G <-- mainline
\
C'-D' <-- HEAD
Git 现在cherry-picks E
: D
是合并基础,D'
是--ours
,E
为--theirs
,新commit完成复制过程:
C--D--E <-- branch
/
...--B--F--G <-- mainline
\
C'-D'-E' <-- HEAD
复制完成后,git rebase
现在只需要从旧的提示提交 E
中提取名称 branch
,并使其指向 [=47= 的提交] 当前名称,即 E'
和 re-attach HEAD
以使一切看起来正常:
C--D--E [abandoned]
/
...--B--F--G <-- mainline
\
C'-D'-E' <-- branch (HEAD)
注意 --ours
的意思
在变基的 cherry-picking 部分,--ours
指的是:
- 首先提交
G
- 然后提交
C'
, - 然后提交
D'
.
所以 --ours
首先引用他们的提交 G
,然后引用我们自己的提交 建立在新分支 .
--theirs
次提交的顺序是 C
,然后是 D
,然后是 E
。所以 --theirs
总是指 我们的 提交。
合并基础提交的顺序是 B
,然后是 C
,然后是 D
。没有 --base
选项来引用这些,但第一个是“他们的”提交,另外两个是我们的。
如果我们想覆盖“他们的”(mainline
) 分支更改,那么,大多数时候我们需要使用 --theirs
,而不是 --ours
。