为什么会出现与 git rebase interactive 的合并冲突?

Why do I get this merge conflict with git rebase interactive?

我还在学习git。

我有一个名为 names.txt 的文件。有了这个文字。

这是我的提交历史

第一次提交添加了文件。 第二次提交添加了第一行 Mary。 第三次提交添加了第二行 John.

git show 7bdb5ef

git show 80384aa

我想对其进行变基并编辑提交 Mary 以将文本更改为 Mary Shelly

I do git rebase -i 4a5244b

接下来我将提交 Mary 设置为编辑并 运行 变基。

变基到此为止。

现在 name.txt 在 Mary 提交时具有值。

我将其更改为 Mary Shelly 并上演。

我运行

git commit --amend 

其次是

git rebase --continue

现在我得到了这个合并冲突。

我不明白为什么会这样。 Commit John 仅更改文件中的第二行。当我们编辑提交 Mary 时,我们只更改文件的第一行。 这怎么会引起冲突?

文件级合并操作(即 Git 需要协调文件的两组更改的操作)尝试允许您移动代码而不会引起太多冲突,因此为了尝试并找到应用更改的正确位置,上下文 - 周围的线集 - 也被考虑在内。

在这里,重新应用提交 John 会导致麻烦:原始提交在行 Mary 旁边添加了 John。现在 Git 正在尝试重新应用提交,但是说 Mary 的参考行不再存在 - 所有那一行都说 Mary Shelly... 请记住 Git 不理解您的文件的目的 and/or 含义,因此在这种情况下,它不会有任何机会并将其作为冲突呈现给您,以便您可以检查它。

JohnMary 之间的许多其他行再次尝试相同的操作,您将保持相同 - 您会发现不会发生冲突。

问题是您对原始行所做的更改实际上很可能也需要在添加的相邻行上才能使合并成功,而无需人工判断。我用的例子是

<<<<<<<<<<<<<
    if ( g->tag == mark 
      || g->tag == error ) {
||||||||||||||
    if ( tag == mark
      || tag == error ) {
==============
    if ( tag == mark 
      || tag == release
      || tag == error ) {
>>>>>>>>>>>>>>

其中一个更改将 g-> 添加到一对行,另一个更改在中间添加了 release 行。

问题是合并冲突,chepner's comment是理解原因的关键。好吧,还有 提交图 ,加上 git rebase 由重复的 git cherry-pick 操作组成的事实。 Interactive rebase 允许您在每个 git cherry-pick 之间添加自己的命令,甚至可以将 cherry-picks 更改为其他内容。 (初始命令-sheet 以所有-pick 命令开始,每个命令都意味着 执行一个cherry-pick。)

您的提交历史记录是您的提交图的摘要——本质上,是在提交图中访问每个提交的结果,从某个特定的终点开始(您的提示当前分支)并向后工作。如果您使用 git log --graph,您会得到一些可能很重要的信息,而这些信息在没有 --graph 的情况下被遗漏了,尽管在这种特殊情况下,很容易看出图形是线性的。所以你只有三个提交:

A <-B <-C   <-- master (HEAD)

其中 A 实际上是 4a5244bB 代表 7bdb5efC 代表 80384aa(如果我已经正确转录图像)。每个提交都有文件的完整副本 names.txt。副本在提交 ABC 中当然 不同 ,因为在 A 中,它是空的;在B中,是一行Mary;在 C 中,有两行内容分别为 MaryJohn

图表本身源于提交 C80384aa 包含提交 B7bdb5ef、[=161= 的哈希 ID ]内部 C 本身 。这就是为什么我画了一个箭头从 C 指向 B。 Git 调用此 C 父级 提交。 Git在名字master中记录C的哈希ID,然后将特殊名字HEAD附加到名字master,这样它就知道这是 git log 应该开始的地方,而那个提交 C 是你现在要继续处理的提交。

当你运行 git rebase -i 4a5244b——选择提交A作为新的基础——Git发现这意味着复制提交BC,所以它将它们的哈希 ID 放入 pick 命令列表中。然后它会在命令-sheet 上打开您的编辑器。您将 pick 更改为 edit,它告诉 Git:执行 cherry-pick,然后在操作中间退出 rebase。

您没有强制变基以制作真实副本。 (要做到这一点,请使用 -f--no-ff--force-rebase——它们的意思都是一样的。这在这里并不重要,在大多数情况下也不重要。)所以 Git看到有一条指令,复制B,让它在A之后,然后意识到:嘿,等等,B 已经在 A 之后。我就把它留在那里。 Git 做了那个然后停止了,让你处于这种状态:

A--B   <-- HEAD
    \
     C   <-- master

请注意 HEAD 不再附加到 master:它现在直接指向提交 B。提交 C 仍然存在,并且 master 仍然指向它,但是 Git 已停止在 "detached HEAD" 模式以允许您进行编辑。

您对文件 git addgit commit --amend 进行了更改。这会产生一个 new 提交——我们可以称它为 B'D,通常我使用 B' 因为通常它很像 B,不过这次够不一样了,还是用D吧。新提交将 A 作为其父项——这就是 --amend 所做的。 Git 更新 HEAD 以指向新提交。现有提交 B 保持不变。所以现在你有:

  D   <-- HEAD
 /
A--B
    \
     C   <-- master

D 中的文件 names.txt 有新的单行内容 Mary Shelly

你现在 运行 git rebase --continue,所以 Git 继续指令 sheet 中剩下的内容。由 pick <hash-of-C> 组成,这使得 Git 运行 git cherry-pick 复制 C。此副本需要在当前提交 D 之后进行。现有提交 C 没有,所以 Git 这次必须真正完成工作。

cherry-pick 是合并——合并为动词,至少

执行合并操作—合并,操作—Git需要三个输入。这三个输入是 merge base 提交,当前或 --ours 提交(有时也称为 local,特别是 git mergetool),另一个或 --theirs 提交(有时称为 remote)。对于常规合并,基础通常有点远:它是两行提交分叉的地方。对于 cherry-pick 和 revert,就此而言,基础就在提交旁边。此操作的合并基础是 C 的父提交 B!

合并的实际操作包括 运行对整个提交执行两个 git diff 命令:

  • git diff --find-renames <em>hash-of-base</em> <em>hash-of-ours</em>:我们改变了什么?
  • git diff --find-renames <em>hash-of-base</em> <em>hash-of-theirs</em>:他们改变了什么?

所以 Git 现在差异提交 B,基础,与提交 D,你的 current/ours 提交。该差异影响文件 names.txt 并表示:将表示 Mary 的一行更改为两行:一行读 Mary Shelly,另一行读 John。 然后 Git diffs B vs C,看看 "they"(你,早些时候)做了什么。差异影响文件 names.txt 并表示:在文件末尾添加读作 John 的行,在读作 Mary 的行之后。

这就是 Git 在合并冲突部分向您展示的内容:一个文件说 用 Mary Shelly 替换 Mary,另一个说 保留 Mary并添加 John。如果您愿意,可以告诉 Git 在合并冲突部分保留更多信息。为此,请将 diff.conflictStyle 设置为 diff3。 (如果未设置,默认值为 merge。)

使用 diff3 设置,您会看到 base 内容(由 ||||||| 标记)是一行 Mary ,并且来自冲突提交的两个文件分别用 Mary ShellyMary + 新行 John 替换了该基础。我发现这种合并冲突更清晰,更容易手动合并。

无论如何,此时你的工作是得出正确的结果——不管是什么——然后将其写出并将其复制到索引槽零中。通常,您只需编辑 Git 留在工作树中的混乱 names.txt,将正确的内容放入其中,然后 运行 git add names.txt.

正在恢复

解决冲突后,运行 git <em>whatever</em> --continue 恢复任何停止的操作——在这种情况下,rebase,但这也会发生在 cherry-pick 和 merge 中。 Git 将使用您用 git add 更新的索引内容来创建新提交,该提交是 C:

的副本
  D--C'   <-- HEAD
 /
A--B
    \
     C   <-- master

到达命令 sheet 的末尾,git rebase 现在通过从提交 C 中提取名称 master 并将其粘贴到 C' 来完成],也就是它制作的最后一个副本,然后重新附上HEAD:

  D--C'   <-- master (HEAD)
 /
A--B
    \
     C   [abandoned]