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 mergegit rebase策略参数是-s--strategy;你在这里使用 recursive,这很好(ours 策略 不是)。扩展选项是 -Xourstheirs 扩展选项 确实有意义——但这里有一个陷阱:你想要 -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

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

  1. 提交H,作为合并基础;
  2. 提交 J,作为 --ours,通过 HEAD;和
  3. 通过名称 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 的子项;提交 NC 之后,OP 之后;这些都是通过名称 feature1.

找到的

提交 Tfeature2 上的最后一次提交,我们现在检查了分支 feature2。所以提交 THEAD 提交。

我们需要一些新代码来应用到 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--oursC--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

在这里,我们要复制的提交是 CDE。旧基础是提交 B。提交 FG 已添加到主线分支。所以我们 运行:

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' 作为 --oursD 作为 --theirs。 Git 合并更改,将合并的更改应用到现有提交 C',并进行新提交 D':

       C--D--E   <-- branch
      /
...--B--F--G   <-- mainline
            \
             C'-D'  <-- HEAD

Git 现在cherry-picks E: D 是合并基础,D'--oursE--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