即使冲突解决后如何查看三向 git diff

Tom*_*lis 4 git git-rebase git-merge-conflict

如果我在 git 中存在冲突(我使用 rebase 但同样适用于合并),它会向我的文件添加冲突标记,以便我可以通过编辑来解决

  line1
++<<<<<<< HEAD
 +line1a
++=======
+ line1b
++>>>>>>> b


  line2
++<<<<<<< HEAD
 +line2a
++=======
+ line2b
++>>>>>>> b
Run Code Online (Sandbox Code Playgroud)

合并的一部分git diff仍然显示三向差异

  line1
 +line1a
+ line1b


  line2
++<<<<<<< HEAD
 +line2a
++=======
+ line2b
++>>>>>>> b
Run Code Online (Sandbox Code Playgroud)

但是一旦我解决了所有冲突并添加它们,就git diff什么也没有显示。如何查看三向差异?具体来说,我想看到类似的东西

  line1
 +line1a
+ line1b


  line2
 +line2a
+ line2b
Run Code Online (Sandbox Code Playgroud)

tor*_*rek 5

长话短说

\n\n

考虑使用git checkout -m,但要非常小心,因为它是一个破坏性命令。请注意,它仅有时有效。

\n\n

长的

\n\n

这些实际上是不同的:在冲突的合并\xe2\x80\x94期间,您在工作树文件中看到的内容与使用Git\的合并引擎的任何内容不同,包括在rebase\xe2\x80\期间发生的樱桃采摘x94是Git低级合并驱动留下的东西,你运行时看到的东西git diff是Git组合diff代码产生的。

\n\n

第一种输出\xe2\x80\x94没有正式名称\xe2\x80\x94,只要你拥有所有三个输入文件,就可以随时复制。第二种输出,组合差异,是......更棘手。

\n\n

低级合并驱动程序本身作为单独的程序使用git merge-file

\n\n
\n

我怎样才能看到[组合]差异?

\n
\n\n

不幸的是,对于已解析的文件,没有工具可以执行此操作。你可以得到你想要的,但它很棘手:

\n\n
    \n
  • 如果您尚未完成变基或樱桃选择操作(或还原,这也可以完成此操作),您可以破坏您的解决方案,将文件放回冲突状态。为此,请git checkout -m对相关文件使用,但请注意,它会破坏您迄今为止所做的工作:

    \n\n
    git checkout -m -- path/to/file.ext\n
    Run Code Online (Sandbox Code Playgroud)\n\n

    (您可以将之前手动合并的文件保存在其他位置\xe2\x80\x94,只需将其移开,例如\xe2\x80\x94,因为您将恢复整个冲突状态。将合并的文件放回原处您已准备好并git add像以前一样使用,以再次将其标记为已解决。)

  • \n
  • 如果您完成变基或类似操作,则必须重复所涉及的特定操作,以再次引发冲突。

  • \n
  • 合并有点不同,我们稍后会看到。

  • \n
\n\n

在 Git 中,进行“三向合并”时会出现冲突。三向合并意味着三个输入文件。当您使用 plain 时git merge,这三个文件的源代码更容易查看,因此让我们在进行变基和挑选之前考虑这种情况。不过,为了了解这里发生的事情,您首先需要了解更多背景知识。

\n\n

关于 Git 索引需要了解什么

\n\n

我们将从一系列从一些共同的共享历史记录开始的提交开始,如下所示:

\n\n
...--G--H   <-- master\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们现在将创建两个新的分支名称branch1branch2,两者都指向哈希值为 的现有提交H

\n\n
...--G--H   <-- master, branch1, branch2\n
Run Code Online (Sandbox Code Playgroud)\n\n

以便所有提交都在所有分支上。然后,在这两个新分支中的每一个上,我们都会进行一些新的提交。数量多少并不重要,只要每个分支上至少有一个即可;一旦我们到达那里,我会在这里每人画两个。

\n\n

关于提交需要了解的是,每个提交都以一种特殊的、只读的、仅限 Git 的压缩格式保存所有文件的快照。这会永久冻结文件的副本,以便 Git 可以在以后从任何提交中随时取回它们。不过,冻结的副本只能Git 使用,因此未冻结的普通副本需要转移到其他地方。你告诉你想要git checkout哪个提交,它就会提取文件,将它们转回普通且有用的文件,将有用的副本放入你的工作区,Git 将其称为你的工作树工作树

\n\n

如果您git checkout通过哈希 ID 进行提交,Git 会将所有该提交的冻结文件提取到您的工作树中,以便您可以查看并使用这个历史版本。但这并不完全是您通常使用 Git 的方式。

\n\n

关于提交需要了解的是,Git 是从 Git 的索引而不是从您的工作树创建它们的。也就是说:我们用来git checkout选择一个分支名称,它又选择该分支中包含的最后一次提交。我们现在有一个当前名称\xe2\x80\x94Git 将特殊名称附加HEAD到分支名称之一 \xe2\x80\x94 和当前提交。Git 将每个提交的文件从提交中复制到您的工作树中...但它也会将每个提交的文件复制到 Git\ 的索引中。

\n\n

换句话说,索引保存当前提交中每个文件的副本。1 这个副本一开始似乎毫无意义:你的工作树中有一个。为什么不用那个呢?其他版本控制系统实际上会这样做,但 Git 不会。到底为什么,这取决于 Git 作者,但我们可以注意到这一点:索引副本采用冻结格式。这意味着无需再次重新压缩工作树副本。该git add命令可以获取更新的工作树副本并对其进行压缩,现在索引副本已更新并准备好提交。当你运行时git commit索引副本将进入新的提交。

\n\n

因此,我们可以说该索引保存了您提议的下一次提交。稍后它会变得有点复杂,但现在让我们git checkout branch进行一次新的提交。我们将从以下开始:

\n\n
...--G--H   <-- master, branch1 (HEAD), branch2\n
Run Code Online (Sandbox Code Playgroud)\n\n

当前分支是branch1. 当前提交是(H代表一些实际的哈希 ID)。Git 的索引和工作树都填充了 commit 的快照H

\n\n

您现在更改一些工作树文件并git add运行git commit. Git 从您\xe2\x80\x94、您的姓名和电子邮件地址、日志消息等\xe2\x80\x94 中收集适当的元数据,并将新提交设置为将提交作为H其父提交。Git 将索引中的冻结格式文件打包以制作新的快照。Git 将所有这些写出来,它获取一个新的唯一哈希 ID,我们将其称为I,并I设置为指向现有提交H\xe2\x80\x94,即我们在工作时得到的\xe2\x80 \x94 这给了我们:

\n\n
          I\n         /\n...--G--H   <-- master, branch1 (HEAD), branch2\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在神奇的一步发生了:Git 将新提交的哈希 ID 写入当前名称,因此branch1现在指向I

\n\n
          I   <-- branch1 (HEAD)\n         /\n...--G--H   <-- master, branch2\n
Run Code Online (Sandbox Code Playgroud)\n\n

因此,当我们将分支git checkout取出、修改工作树文件、将git add更新的文件复制回索引以准备快照时,分支一次会增长一个提交,然后运行git commit以创建快照。新的快照指向当前\xe2\x80\x94 是HEAD\xe2\x80\x94,现在新的快照是当前的。新的只是索引创建的,因此索引和提交匹配,就像我们H之前干净地签出提交时所做的那样,并且我们准备好修改和提交更多内容。

\n\n
\n\n

1从技术上讲,索引包含对内部 Git blob 哈希 ID 的引用,而不是文件的实际副本。但是,除非您开始在索引详细信息\xe2\x80\x94 中进行探索,就像我们稍后会\xe2\x80\x94 那样,否则您无法真正区分这与拥有文件的完整副本之间的区别。

\n\n
\n\n

合并,正常样式

\n\n

因此,假设我们在每个分支上进行了两次提交,并且branch1现在已经退出,如下所示:

\n\n
          I--J   <-- branch1 (HEAD)\n         /\n...--G--H\n         \\\n          K--L   <-- branch2\n
Run Code Online (Sandbox Code Playgroud)\n\n

(名字master仍然指向H,但我会偷懒,现在停止画它)。我们现在跑git merge branch2

\n\n

Git 自动找到最佳的公共合并基础提交\xe2\x80\x94,两个分支都从其下降的共享提交\xe2\x80\x94,在本例中显然是 commit H。这三个提交中的每一个都有我们所有文件的完整快照。这就是 Git 所做的,至少在原则上是这样的(实际上这一切都经过了优化):

\n\n
    \n
  • 首先,Git 扩展索引。现在,它不再保留每个文件的一个副本,而是最多保留每个文件的三个副本。这些副本已编号并称为暂存槽

  • \n
  • 合并库中每个文件的副本commitH进入槽 1。

  • \n
  • 当前提交 中每个文件的副本J都会放入插槽 2。实际上,插槽 0\xe2\x80\x94 中已经有一个副本(正常的、不冲突的全解析插槽 \xe2\x80\x94),因此 Git 可以将其移动一步。如果您的索引和/或工作树脏了,这里有一些您通常不会看到的复杂情况,因为git merge如果您的索引和/或工作树脏了,该命令将不会让您启动。2

  • \n
  • 此处,来自其他提交的每个文件的副本L进入槽 3。

  • \n
\n\n

现在每个文件都有三个副本,至少对于所有三个提交中的每个文件来说都是如此,这是这里有趣的情况。

\n\n

合并命令现在比较三个副本。如果三者都相同\xe2\x80\x94(对于许多合并而言,几乎所有文件都适用 \xe2\x80\x94),则结果很简单:任何副本都可以。Git 会将其移至插槽 0,并擦除剩余的三个插槽。该文件现已解决。工作树副本也已经很好了,所以 Git 不理会它。

\n\n

如果合并基础副本与其副本 \xe2\x80\x94slot 1 = slot 3\xe2\x80\x94 匹配,但我们的不匹配,那么我们必须修改该文件。正确的合并结果是获取我们的文件,以便 Git 将 slot-2 副本移动到 slot-0,擦除其他两个槽,并再次保留工作树文件。文件已解决:我们使用我们的。

\n\n

合并基础副本与我们的副本 \xe2\x80\x94slot 1 = slot 2\xe2\x80\x94匹配,但他们的副本不匹配,那么他们一定修改了文件。正确的合并结果是获取他们的文件,因此 Git 将 slot-3 副本移动到 slot-0,这次还将 slot-3 副本提取到工作树。文件已解决:我们使用了他们的。

\n\n

仅对于所有三个插槽都不同的情况,Git 才需要执行任何实际工作。Git 现在调用其低级单文件合并驱动程序

\n\n

低级驱动程序将文件的工作树副本写入作为其输出。它还查看每个实际的源代码行更改,即如果我们运行git diff. 它将文件的合并基础(插槽 1)副本与我们的副本(插槽 2)进行比较,以查看我们更改了什么,并将合并基础与他们的副本(插槽 1 与插槽 3)进行比较,以查看它们更改了什么。在更改重叠或邻接(接触)的情况下,标准低级合并驱动程序将用其他插槽线替换插槽 1 线。当更改确实重叠或相邻时,标准低级合并驱动程序会将合并冲突写入文件的工作树副本中。

\n\n

处理完所有行后,低级驱动程序会报告:要么表示所有更改已成功组合,要么表示合并冲突。这一信息决定了高层代码最终的作用。如果显示“合并成功”,则生成的文件将进入插槽 0,并且该文件被视为已合并。如果显示合并冲突,Git 会将所有三个文件保留在索引中

\n\n

较高级别的代码处理所有文件,对每个潜在冲突的文件使用低级别合并驱动程序,一次一个。当这一切完成后,如果其中任何一个存在合并冲突,则整个合并将停止。这就是你的工作\xe2\x80\x93和你的问题\xe2\x80\x94的用武之地。你必须提供正确的文件。

\n\n

git add命令会将工作树文件中的所有内容复制到插槽 0 中,并擦除其他三个插槽。因此,更新工作树文件后,您可以运行git add它,这标志着文件已解决。

\n\n

解决了所有冲突后,您可以运行git merge --continuegit commit来告诉 Git 完成作业。Git 使用现在全部位于插槽 0 中的文件来进行新的提交。因此,它像往常一样具有索引的快照。新合并提交的唯一特别之处在于它不仅有通常的一个父级,而且有两个

\n\n
          I--J\n         /    \\\n...--G--H      M   <-- branch1 (HEAD)\n         \\    /\n          K--L   <-- branch2\n
Run Code Online (Sandbox Code Playgroud)\n\n

合并的第一个父级是相同的提交,它始终是 \xe2\x80\x94commit ,J本例中为 \xe2\x80\x94 ,第二个父级是另一个提交:在本例中为L.

\n\n
\n\n

2这里的“脏”意味着索引和/或工作树中某些文件的副本与HEAD该文件的 -commit 副本不匹配。只要所有三个副本匹配,那么git status命令就会说nothing to commit, working tree clean,则该 slot-2 副本来自何处并不重要:所有三个副本都匹配。

\n\n
\n\n

樱桃采摘正在融合

\n\n

让我们看一下一系列更简单的提交。而不是我们要合并的两个分支让我们假设我们只有这个,

\n\n
        tag:v1.0\n           |\n           v\n...--E--F--G   <-- release/1\n            \\\n             H--I--J   <-- develop (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们已经发布了该软件的一些实际版本,提交的G是 1.0 版(标记和分支)。我们继续并开始在开发分支中添加新功能并做出新的提交H-I-J。现在我们意识到:嘿,在 commit 中J,我们所做的唯一更改是修复了 commit 中也存在的令人讨厌的错误(可能在 commit或G中引入,所以它存在于and 和中)。EFGHI

\n\n

我们希望使用v1.1我们从J. 也就是说,我们希望将提交复制J到一个新的提交,该提交类似于J\xe2\x80\x94,它修复了 bug\xe2\x80\x94,但它发生在G. 3 我们将这个新提交称为J\'

\n\n
        tag:v1.0\n           |\n           v\n...--E--F--G--J\'  <-- release/1\n            \\\n             H--I--J   <-- develop\n
Run Code Online (Sandbox Code Playgroud)\n\n

(一旦这一切完成,我们将标记提交J\'v1.1重新发布。)

\n\n

所以,我们运行:

\n\n
git checkout release/1\ngit cherry-pick develop\n
Run Code Online (Sandbox Code Playgroud)\n\n

cherry-pick 本身的工作方式很简单:

\n\n
    \n
  • 假设每个提交都有一个父提交。在这种情况下,J有一个父母,I
  • \n
  • 将位于 \xe2\x80\x94之后的当前提交 \xe2\x80\x94视为slot-2 提交。Ggit checkout
  • \n
  • 父级视为合并基础,并将提交本身视为 other\xe2\x80\x94 或 slot 3\xe2\x80\x94commit。
  • \n
\n\n

因此,Git 现在将比较 中的文件I与 中的文件G以查看我们更改的内容,即从到向后退,取消我们在 中所做的操作。它将比较 中的文件与 中的文件以查看它们更改了什么,以修复错误。然后它会像往常一样将我们的更改与他们的更改结合起来。IGHIJ

\n\n

当开发工作的取消与错误修复发生冲突时,就会发生任何合并冲突。事实上,这正是我们想要的:我们希望确保我们采取了修复错误所需的一切。

\n\n

一旦所有冲突都得到解决,Git 会将新提交作为普通的单父提交,而不是合并提交。它的单亲是之前的提交HEAD,新的提交现在和HEAD往常一样。

\n\n
\n\n

3实际上,最好找到引入该错误的原始提交,并在那里创建一个分支并在该分支中修复它。然后,我们可以将此修复合并到每个版本中,而不是精挑细选。上图中的差异是无关紧要的\xe2\x80\x94事实上,樱桃选择更容易和更简单\xe2\x80\x94但随着时间的推移,差异最终在发布管理方面很重要。请参阅Raymond Chen 的系列文章

\n\n
\n\n

Rebase本身主要是一系列的cherry-pick操作

\n\n

如果我们从以下开始:

\n\n
...--G--H   <-- master\n         \\\n          I--J   <-- feature (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

有人添加了一些master提交,这样我们就有了:

\n\n
...--G--H--K--L   <-- master\n         \\\n          I--J   <-- feature (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们可能想复制I到新的和改进的I\',然后复制J到新的和改进的J\',以获得:

\n\n
                I\'-J\'  <-- HEAD (detached HEAD)\n               /\n...--G--H--K--L   <-- master\n         \\\n          I--J   <-- feature\n
Run Code Online (Sandbox Code Playgroud)\n\n

完成后,我们希望 Git 将名称feature从 commit 中剥离J并使其指向 commit J\',然后重新附加HEAD

\n\n
                I\'-J\'  <-- feature (HEAD)\n               /\n...--G--H--K--L   <-- master\n         \\\n          I--J   [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n\n

复制 from ItoI\'和 from JtoJ\'正是这样git cherry-pick做的。所以 rebase 可以:

\n\n
    \n
  • 按正确的顺序列出要复制的提交(I,然后J);4
  • \n
  • 通过哈希 ID 检查目标提交来分离LHEAD ,相当于git checkout --detach,并且从历史上看,一种 rebase 确实运行了该命令;
  • \n
  • 运行两个git cherry-pick命令;5
  • \n
  • 强行移动分支并重新附加HEAD6
  • \n
\n\n

(我不会讨论 new-ish 是如何--rebase-merges工作的,这让事情变得更加复杂。)

\n\n
\n\n

4获取要复制的正确提交列表实际上相当复杂。我们在这里不做详细介绍。

\n\n

5一些变基操作实际上就是这样做的,一次一个:交互式变基尤其将每个pick命令变成一个单独的git cherry-pick步骤。其他人试图提高效率和/或在内部有所不同,尤其是旧式的内部git-rebase--am后端。Git 2.26 最终不再使用这种旧式的 rebase 作为默认设置,因为它错过了一些重命名情况。

\n\n

6如果由于某种原因您想要手动执行所有四个步骤,您可以使用 或 手动执行git checkout -B最后一步。git switch -C

\n\n
\n\n

最后回到你原来的问题

\n\n
\n

如何查看三向差异?

\n
\n\n

显然我们需要三个输入:一个合并基础版本和两个其他版本。假设这里文件的名称是F。

\n\n

如果您刚刚开始使用 Git 的合并引擎,并且正处于冲突合并中,则三个输入现在位于 Git 的索引中。这就是 Git 的低级合并驱动程序获取它们的地方。它自己编写了合并到工作树文件中的尝试,您可以通过查看它来看到这一点。

\n\n

或者,你git diff现在就可以跑。请git diff注意,对于文件F,存在三个索引副本。它将三个 diff 进行比较,并将 diff 组合成一个组合 diff7

\n\n

您可以使用、和为某些 Git 命令命名这些索引副本。这里最有用的 Git 命令之一是::1:F:2:F:3:Fgit show

\n\n
git show :1:path/to/file > file.BASE\ngit show :2:path/to/file > file.OURS\ngit show :3:path/to/file > file.THEIRS\n
Run Code Online (Sandbox Code Playgroud)\n\n

例如。现在有了三个普通文件,并且可以执行相同的操作\xe2\x80\x94 或git merge-file在它们上运行(如果您愿意)。

\n\n

但是,如果您运行了git add path/to/file,Git 就会清除三个编号较大的副本,并用一个零槽副本替换它们。git show您可以使用名称:path/to/file或进行查看:0:path/to/file,但它实际上只是您工作树中已经存在的名称,所以为什么要麻烦呢?

\n\n

如果需要,您可以通过 Git 来重建合并冲突:

\n\n
 git checkout -m -- path/to/file\n
Run Code Online (Sandbox Code Playgroud)\n\n

Git 将三个副本放回到三个插槽中,并重新运行合并驱动程序,覆盖工作树副本。8

\n\n

为了获得git diff此时的组合差异,您必须将三个副本放入索引中。如果您确实想要,有一种方法可以使用 ,在任何暂存槽编号处将任意文件内容上传到索引中git update-index,但这很棘手:您必须首先将它们转换为 Git blob 对象并获取它们的哈希 ID。我不建议这样做,因为很难做到正确:

\n\n
git hash-object -w -t blob --stdin < contents\n
Run Code Online (Sandbox Code Playgroud)\n\n

生成适当的 blob 哈希,然后git update-index --index-info可以从 stdin 读取行以将内容放入索引槽中。标准输入流的格式git update-index --index-info非常严格,仅供其他程序使用。(请注意--cacheinfo,它更易于使用,但不允许您写入非零槽号。)

\n\n

一旦您提交合并结果\xe2\x80\x94作为合并,或精心挑选的提交,或任何\xe2\x80\x94,所有数据都git checkout -m消失了,您无法以这种方式重建合并状态。然而,合并提交会记录其父提交,并且git show在合并提交上运行会调用组合差异代码。

\n\n

这里有一个很大的警告:git show合并提交默认为--cc(两个破折号,两个c)样式组合差异。这与冲突合并期间的输出不同git diff,而冲突位于索引的非零暂存槽中。使用git show -c强制 Git 使用-cone-dash one-c 样式,这更接近(但仍然不相同)git diff冲突合并期间的输出。

\n\n
\n\n

7这不太正确,因为当您修改工作树副本时,您将看到输出发生git diff变化。Git 知道这些不是我们关心的:我们真正希望看到 slot-2-vs-work-tree 和 slot-3-vs-work-tree。这就是这里的差异和组合。

\n\n

8git checkout -m无需git add先将文件标记为已解决即可执行此操作。在这种情况下,三个插槽已经满了并准备就绪。不过,工作树副本仍然会被破坏,这可能是这里最重要的部分。

\n\n
\n\n

相关工作

\n\n

这根本不是一回事,但您可能对interdiffsrange diffs感兴趣。请参阅interdiff 能做什么而 diff 不能做什么?以及如何获得这两个 git 提交之间的差异?了解更多信息。

\n