为什么 Git rebase 会触发合并冲突?

6

我已经克隆了一个远程仓库,创建了一个新的分支b并开始工作和提交代码。我已经推送了b分支,但是只有我在使用这个分支。

过了一段时间,我想要将我的本地分支与远程主分支进行变基,以便与系统中可能发生的更改进行同步。请注意,我确定只有我在处理这些特定文件。

因此,我执行了以下操作:

git fetch --all
git rebase origin/master

然后Git通知我发生了合并冲突。

现在,我可以轻松地手动解决冲突,但有个问题困扰着我:为什么会发生合并冲突?

如果我没记错的话,git rebase 的整个思想就是将我的当前分支上的所有提交"重播"到指定分支的末端。这些特定文件或分支只有我一个人在工作。

那么为什么会发生这种情况呢?我的做法有问题吗?


git rebase 会重放您的更改。如果补丁无法应用,则必须在继续之前解决冲突。您遇到了什么样的冲突(添加的、在两个分支中都编辑过等)?然后查看主分支的历史记录,以了解是什么导致了冲突。 - Adam
1
假设您当前的文件包含 AAA,但您尝试变基的提交具有增量 AAB -> AAC,这是一个冲突,需要选择 AAA 还是 AAC - Alexey Larionov
2个回答

6
我认为有助于认识到git rebase实际上是自动运行git cherry-pick的方法。但仅有这些还不够,你还需要明白git cherry-pick是合并的一种形式。这就是合并冲突的来源所在。
当查看常规合并时,可以更容易地理解这一点。让我们绘制一个提交图表,每个提交用单个大写字母代表,如下所示:
          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

如果我们运行git merge branch2,Git会找到三个提交:
  • 其中一个提交,也就是最后的#2,永远都是当前的或者HEAD提交。由于HEAD附加在branch1名称上,当前提交就是由branch1标识的那个:提交J

  • 最后的#3提交是你指定的提交。通过使用branch2这个名称,你告诉Git读取该名称并查看它指向提交L

  • #1提交是Git自己找到的。Git通过找到两个分支上“最佳”的提交来实现此目的。在branch1上的提交包括...-G-H-I-J。在branch2上的提交包括...-G-H-K-L。因此,提交G位于两个分支上,但它比提交H更早。提交I-J仅出现在其中一个分支上,而K-L仅出现在另一个分支上。这意味着提交H是最好的共享提交。

Git现在可以执行合并。为此,Git实际上运行了两个git diff命令:

  • git diff --find-renames H的哈希码 J的哈希码:这告诉Git在公共起点和你的提交之间发生了什么变化,也就是你所做的更改。

  • git diff --find-renames H的哈希码 L的哈希码:这告诉Git在相同的起点和他们的提交之间发生了什么变化,也就是他们所做的更改。

现在,合并命令的工作是组合你的更改和他们的更改:

  • 对于没有被修改的在H中的文件,保留那些文件。
  • 对于在H中由修改而他们没有修改的文件,采用你的版本。
  • 对于在H中由他们修改而你没有修改的文件,采用他们的版本。
  • 对于你们都修改过的文件,需要找出是否可能将你们的更改结合起来。
有一些棘手的情况,比如你重命名了一个文件和/或他们重命名了一个文件,或者你删除了一个文件而他们修改了它等等。但是大多数情况下,当你们都对某个文件进行更改,并且你们都更改了该文件的相同行,或进行了“接触”更改时,将发生合并冲突。如果您的更改和他们的更改不会“接触”,Git会认为保留两个更改都可以。否则,你就会遇到合并冲突。
在前几次处理此类情况时可能有点棘手,但是久而久之,它会感觉非常自然。例如,如果Alice将“红球”更改为“蓝球”,而Bob将“红球”更改为“红砖”,Git不知道该怎么做,并使您选择正确答案。
进入cherry-pick
git cherry-pick命令的作用是复制提交。也就是说,给定表示所有文件的完整快照的某个提交,我们要找出该文件中发生了哪些变化。
在Git中,很容易将两个相邻的提交——发生在另一个提交之后的两个快照——转换为一组更改。我们只需要求Git运行git diff来执行这项操作。Git会确定哪些文件相同,并对此保持沉默。它会确定哪些文件不同,并生成一份食谱——一组要添加和/或删除的行——以将较早提交的文件更改为较新提交的副本。如果我们使用--find-renames(自Git 2.9起默认开启),那么如果左侧消失了一个文件,而右侧出现了一个新文件,则Git还会确定是否表示文件重命名操作。
然后,想象一下我们有以下内容:
...--G--H--I--J   <-- main
         \
          K--L   <-- feature

如果我们要求从HK的差异,我们将看到与H相比,在K中发生了什么变化。例如,可能会说“在file.py的第72行后添加此行”。
但是如果我们想将这些更改应用于提交J呢? 我们可以闭上眼睛,希望“在第72行后添加此行”有意义,但是如果原来的第72行现在是第75行,或者甚至更远怎么办呢? 我们可以搜索上下文,但是也许我们甚至可以做得更好。
与其盲目地应用此更改或检查上下文,不如先抓取第二个差异,即提交H与提交J之间的差异。 这将告诉我们它们所做的更改。 如果他们在第72行上面添加了3行,那么现在第72行就明确是第75行。 这告诉我们应该放置更改的位置。
但是等一下,这个“获取两个差异并组合起来”的想法正是git merge的工作方式!实际上,这正是git cherry-pick的工作方式:我们选择我们要复制的提交的父级,并假装它是合并基础。 我们得到两个差异,一个是从合并基础到我们要复制的提交-这些是“他们”的更改,另一个是从合并基础到我们现在正在工作的提交,即提交J,这些是“我们”的更改。 我们让Git将它们组合起来,使用运行git merge时使用的相同代码。
如果一切顺利,git cherry-pick将为我们创建一个新的提交。 git rebase命令以Git称为“分离头”模式执行所有操作,因此现在的情况如下所示:
                K'  <-- HEAD
               /
...--G--H--I--J   <-- main
         \
          K--L   <-- feature

我们将新提交称为K',以表示它是原始提交K的复制品。现在是时候精选提交L了,因此Git将对比KL,以查看“他们”(实际上是我们)更改了什么,并对比KK',以查看“我们”(实际上是之前的所有操作和包括之前的精选操作)更改了什么。然后,Git将尝试将这两组更改——“我们”的更改(来自K-vs-K')和“Theirs”的更改(来自K-vs-L)结合起来。如果一切顺利,git cherry-pick将创建一个新提交L'
                K'-L'  <-- HEAD
               /
...--G--H--I--J   <-- main
         \
          K--L   <-- feature

如果在 git cherry-pick 步骤中出现问题,Git 将停止操作并要求我们解决冲突,与 git merge 操作完全相同。
一旦所有提交被复制,git rebase 有一个最后的技巧:它会将旧位置上的名字 feature 拉下来,并将其贴到 HEAD 所指向的位置,然后 "重新附加" HEAD 到分支名。在本例中,这样就产生了:
                K'-L'  <-- feature (HEAD)
               /
...--G--H--I--J   <-- main
         \
          K--L   [abandoned]

如果你现在用git log查看提交记录,将完全看不到原始的K-L提交记录,而只能看到新的K'-L'提交记录。在L之前的下一个提交是J,特性分支已经被合并到主分支上并进行了重新设置基底。
任何合并冲突都是因为“你”和“他们”在合并奇怪的基底过程中接触到同一文件的相同或相邻行。当然,“他们”的提交实际上是“你”的提交 - 在重新设置基础时,你要重新安排自己的提交,并且“你”的提交最初往往是其他人的提交。最终,“你”的提交混合了你的提交和他们的提交,这非常令人困惑。
(我喜欢将merge.conflictStyle设置为diff3,以获取更多有关合并冲突的信息。)

0
问题在于“回放”的定义。Rebase所做的正是Merge所做的事情:它创建了一个差异(在这种情况下,从您的分支b与master分离的点到您的分支b的末端),并尝试将其应用到origin/master的末端。
因此,将分支分叉提交称为“split”。
现在,我们知道origin/master不是split,因为如果是这样,您需要首先将其rebase到master上。因此,自split以来已经添加了一些提交到master。

在合并时可能会出现冲突。从splitmaster的差异和从splitb的差异可能包含不能同时自动完成的内容,例如同一文件的同一区域以两种不同的方式进行了编辑,或者一个文件在一个路径中进行了编辑但在另一个路径中被删除等等。这就是冲突。

请注意,冲突并不意味着发生了任何不好的事情!“冲突”这个词非常不恰当。它仅仅意味着git不想试图读取你的思想来预判事情的走向;它要求你手动完成合并,因为如果它自动选择要做什么,可能会做出你不想要的事情。


奇怪的是,由于分支bsplit分支中分离出来,因此在与分支b相关的相同文件中,主分支上没有添加新提交 - 它们使用完全不同的文件。这样冲突是否仍然有意义? - Aviv Cohn
我坚持我的答案。我已经告诉你为什么在变基中会出现冲突,这是正常的。另一方面,你没有告诉我任何信息。特别是,你没有告诉我冲突是什么,那我为什么要猜测呢?你告诉我冲突,我就告诉你为什么它是一个冲突。我不会坐在这里挥舞手臂。 - matt
请注意,“在同一文件中”这样的说法是不存在的。提交并不包含一组文件。每个提交都是一个完整的快照:它包含了所有的文件。所以无论分支b关注什么,它都有所有的文件。因此,到达b末尾的差异会对每个文件做出断言,即使您从未明确地处理过该文件。而且,您在b中拥有的内容可能与master现在拥有的内容不同,这可能构成冲突。 - matt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接