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)
考虑使用git checkout -m
,但要非常小心,因为它是一个破坏性命令。请注意,它仅有时有效。
这些实际上是不同的:在冲突的合并\xe2\x80\x94期间,您在工作树文件中看到的内容与使用Git\的合并引擎的任何内容不同,包括在rebase\xe2\x80\期间发生的樱桃采摘x94是Git低级合并驱动留下的东西,你运行时看到的东西git diff
是Git组合diff代码产生的。
第一种输出\xe2\x80\x94没有正式名称\xe2\x80\x94,只要你拥有所有三个输入文件,就可以随时复制。第二种输出,组合差异,是......更棘手。
\n\n低级合并驱动程序本身可作为单独的程序使用git merge-file
。
\n\n\n我怎样才能看到[组合]差异?
\n
不幸的是,对于已解析的文件,没有工具可以执行此操作。你可以得到你想要的,但它很棘手:
\n\n如果您尚未完成变基或樱桃选择操作(或还原,这也可以完成此操作),您可以破坏您的解决方案,将文件放回冲突状态。为此,请git checkout -m
对相关文件使用,但请注意,它会破坏您迄今为止所做的工作:
git checkout -m -- path/to/file.ext\n
Run Code Online (Sandbox Code Playgroud)\n\n(您可以将之前手动合并的文件保存在其他位置\xe2\x80\x94,只需将其移开,例如\xe2\x80\x94,因为您将恢复整个冲突状态。将合并的文件放回原处您已准备好并git add
像以前一样使用,以再次将其标记为已解决。)
如果您完成了变基或类似操作,则必须重复所涉及的特定操作,以再次引发冲突。
合并有点不同,我们稍后会看到。
在 Git 中,进行“三向合并”时会出现冲突。三向合并意味着三个输入文件。当您使用 plain 时git merge
,这三个文件的源代码更容易查看,因此让我们在进行变基和挑选之前考虑这种情况。不过,为了了解这里发生的事情,您首先需要了解更多背景知识。
我们将从一系列从一些共同的共享历史记录开始的提交开始,如下所示:
\n\n...--G--H <-- master\n
Run Code Online (Sandbox Code Playgroud)\n\n我们现在将创建两个新的分支名称branch1
和branch2
,两者都指向哈希值为 的现有提交H
:
...--G--H <-- master, branch1, branch2\n
Run Code Online (Sandbox Code Playgroud)\n\n以便所有提交都在所有分支上。然后,在这两个新分支中的每一个上,我们都会进行一些新的提交。数量多少并不重要,只要每个分支上至少有一个即可;一旦我们到达那里,我会在这里每人画两个。
\n\n关于提交需要了解的是,每个提交都以一种特殊的、只读的、仅限 Git 的压缩格式保存所有文件的快照。这会永久冻结文件的副本,以便 Git 可以在以后从任何提交中随时取回它们。不过,冻结的副本只能由Git 使用,因此未冻结的普通副本需要转移到其他地方。你告诉你想要git checkout
哪个提交,它就会提取文件,将它们转回普通且有用的文件,将有用的副本放入你的工作区,Git 将其称为你的工作树或工作树。
如果您git checkout
通过哈希 ID 进行提交,Git 会将所有该提交的冻结文件提取到您的工作树中,以便您可以查看并使用这个历史版本。但这并不完全是您通常使用 Git 的方式。
关于新提交需要了解的是,Git 是从 Git 的索引而不是从您的工作树创建它们的。也就是说:我们用来git checkout
选择一个分支名称,它又选择该分支中包含的最后一次提交。我们现在有一个当前名称\xe2\x80\x94Git 将特殊名称附加HEAD
到分支名称之一 \xe2\x80\x94 和当前提交。Git 将每个提交的文件从提交中复制到您的工作树中...但它也会将每个提交的文件复制到 Git\ 的索引中。
换句话说,索引保存当前提交中每个文件的副本。1 这个副本一开始似乎毫无意义:你的工作树中有一个。为什么不用那个呢?其他版本控制系统实际上会这样做,但 Git 不会。到底为什么,这取决于 Git 作者,但我们可以注意到这一点:索引副本采用冻结格式。这意味着无需再次重新压缩工作树副本。该git add
命令可以获取更新的工作树副本并对其进行压缩,现在索引副本已更新并准备好提交。当你运行时git commit
,索引副本将进入新的提交。
因此,我们可以说该索引保存了您提议的下一次提交。稍后它会变得有点复杂,但现在让我们git checkout branch
进行一次新的提交。我们将从以下开始:
...--G--H <-- master, branch1 (HEAD), branch2\n
Run Code Online (Sandbox Code Playgroud)\n\n当前分支是branch1
. 当前提交是(H
代表一些实际的哈希 ID)。Git 的索引和工作树都填充了 commit 的快照H
。
您现在更改一些工作树文件并git add
运行git commit
. Git 从您\xe2\x80\x94、您的姓名和电子邮件地址、日志消息等\xe2\x80\x94 中收集适当的元数据,并将新提交设置为将提交作为H
其父提交。Git 将索引中的冻结格式文件打包以制作新的快照。Git 将所有这些写出来,它获取一个新的唯一哈希 ID,我们将其称为I
,并I
设置为指向现有提交H
\xe2\x80\x94,即我们在工作时得到的\xe2\x80 \x94 这给了我们:
I\n /\n...--G--H <-- master, branch1 (HEAD), branch2\n
Run Code Online (Sandbox Code Playgroud)\n\n现在神奇的一步发生了:Git 将新提交的哈希 ID 写入当前名称,因此branch1
现在指向I
:
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
之前干净地签出提交时所做的那样,并且我们准备好修改和提交更多内容。
1从技术上讲,索引包含对内部 Git blob 哈希 ID 的引用,而不是文件的实际副本。但是,除非您开始在索引详细信息\xe2\x80\x94 中进行探索,就像我们稍后会\xe2\x80\x94 那样,否则您无法真正区分这与拥有文件的完整副本之间的区别。
\n\n因此,假设我们在每个分支上进行了两次提交,并且branch1
现在已经退出,如下所示:
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
。
Git 自动找到最佳的公共合并基础提交\xe2\x80\x94,两个分支都从其下降的共享提交\xe2\x80\x94,在本例中显然是 commit H
。这三个提交中的每一个都有我们所有文件的完整快照。这就是 Git 所做的,至少在原则上是这样的(实际上这一切都经过了优化):
首先,Git 扩展索引。现在,它不再保留每个文件的一个副本,而是最多保留每个文件的三个副本。这些副本已编号并称为暂存槽。
合并库中每个文件的副本commitH
进入槽 1。
当前提交 中每个文件的副本J
都会放入插槽 2。实际上,插槽 0\xe2\x80\x94 中已经有一个副本(正常的、不冲突的全解析插槽 \xe2\x80\x94),因此 Git 可以将其移动一步。如果您的索引和/或工作树脏了,这里有一些您通常不会看到的复杂情况,因为git merge
如果您的索引和/或工作树脏了,该命令将不会让您启动。2
此处,来自其他提交的每个文件的副本L
进入槽 3。
现在每个文件都有三个副本,至少对于所有三个提交中的每个文件来说都是如此,这是这里有趣的情况。
\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 线。当更改确实重叠或相邻时,标准低级合并驱动程序会将合并冲突写入文件的工作树副本中。
处理完所有行后,低级驱动程序会报告:要么表示所有更改已成功组合,要么表示合并冲突。这一信息决定了高层代码最终的作用。如果显示“合并成功”,则生成的文件将进入插槽 0,并且该文件被视为已合并。如果显示合并冲突,Git 会将所有三个文件保留在索引中。
\n\n较高级别的代码处理所有文件,对每个潜在冲突的文件使用低级别合并驱动程序,一次一个。当这一切完成后,如果其中任何一个存在合并冲突,则整个合并将停止。这就是你的工作\xe2\x80\x93和你的问题\xe2\x80\x94的用武之地。你必须提供正确的文件。
\n\n该git add
命令会将工作树文件中的所有内容复制到插槽 0 中,并擦除其他三个插槽。因此,更新工作树文件后,您可以运行git add
它,这标志着文件已解决。
解决了所有冲突后,您可以运行git merge --continue
或git commit
来告诉 Git 完成作业。Git 使用现在全部位于插槽 0 中的文件来进行新的提交。因此,它像往常一样具有索引的快照。新合并提交的唯一特别之处在于它不仅有通常的一个父级,而且有两个:
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
.
2这里的“脏”意味着索引和/或工作树中某些文件的副本与HEAD
该文件的 -commit 副本不匹配。只要所有三个副本都匹配,那么git status
命令就会说nothing to commit, working tree clean
,则该 slot-2 副本来自何处并不重要:所有三个副本都匹配。
让我们看一下一系列更简单的提交。而不是我们要合并的两个分支让我们假设我们只有这个,
\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 和中)。E
F
G
H
I
我们希望使用v1.1
我们从J
. 也就是说,我们希望将提交复制J
到一个新的提交,该提交类似于J
\xe2\x80\x94,它修复了 bug\xe2\x80\x94,但它发生在G
. 3 我们将这个新提交称为J\'
:
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\ngit checkout release/1\ngit cherry-pick develop\n
Run Code Online (Sandbox Code Playgroud)\n\ncherry-pick 本身的工作方式很简单:
\n\nJ
有一个父母,I
。G
git checkout
因此,Git 现在将比较 中的文件I
与 中的文件G
以查看我们更改的内容,即从到向后退,取消我们在 中所做的操作。它将比较 中的文件与 中的文件以查看它们更改了什么,以修复错误。然后它会像往常一样将我们的更改与他们的更改结合起来。I
G
H
I
J
当开发工作的取消与错误修复发生冲突时,就会发生任何合并冲突。事实上,这正是我们想要的:我们希望确保我们采取了修复错误所需的一切。
\n\n一旦所有冲突都得到解决,Git 会将新提交作为普通的单父提交,而不是合并提交。它的单亲是之前的提交HEAD
,新的提交现在和HEAD
往常一样。
3实际上,最好找到引入该错误的原始提交,并在那里创建一个分支并在该分支中修复它。然后,我们可以将此修复合并到每个版本中,而不是精挑细选。上图中的差异是无关紧要的\xe2\x80\x94事实上,樱桃选择更容易和更简单\xe2\x80\x94但随着时间的推移,差异最终在发布管理方面很重要。请参阅Raymond Chen 的系列文章。
\n\n如果我们从以下开始:
\n\n...--G--H <-- master\n \\\n I--J <-- feature (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n有人添加了一些master
提交,这样我们就有了:
...--G--H--K--L <-- master\n \\\n I--J <-- feature (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n我们可能想复制I
到新的和改进的I\'
,然后复制J
到新的和改进的J\'
,以获得:
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
:
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 I
toI\'
和 from J
toJ\'
正是这样git cherry-pick
做的。所以 rebase 可以:
I
,然后J
);4L
HEAD ,相当于git checkout --detach
,并且从历史上看,一种 rebase 确实运行了该命令;git cherry-pick
命令;5和HEAD
。6(我不会讨论 new-ish 是如何--rebase-merges
工作的,这让事情变得更加复杂。)
4获取要复制的正确提交列表实际上相当复杂。我们在这里不做详细介绍。
\n\n5一些变基操作实际上就是这样做的,一次一个:交互式变基尤其将每个pick
命令变成一个单独的git cherry-pick
步骤。其他人试图提高效率和/或在内部有所不同,尤其是旧式的内部git-rebase--am
后端。Git 2.26 最终不再使用这种旧式的 rebase 作为默认设置,因为它错过了一些重命名情况。
6如果由于某种原因您想要手动执行所有四个步骤,您可以使用 或 手动执行git checkout -B
最后一步。git switch -C
\n\n\n如何查看三向差异?
\n
显然我们需要三个输入:一个合并基础版本和两个其他版本。假设这里文件的名称是F。
\n\n如果您刚刚开始使用 Git 的合并引擎,并且正处于冲突合并中,则三个输入现在位于 Git 的索引中。这就是 Git 的低级合并驱动程序获取它们的地方。它自己编写了合并到工作树文件中的尝试,您可以通过查看它来看到这一点。
\n\n或者,你git diff
现在就可以跑。请git diff
注意,对于文件F,存在三个索引副本。它将三个 diff 进行比较,并将 diff 组合成一个组合 diff。7
您可以使用、和为某些 Git 命令命名这些索引副本。这里最有用的 Git 命令之一是::1:F
:2:F
:3:F
git show
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
在它们上运行(如果您愿意)。
但是,如果您运行了git add path/to/file
,Git 就会清除三个编号较大的副本,并用一个零槽副本替换它们。git show
您可以使用名称:path/to/file
或进行查看:0:path/to/file
,但它实际上只是您工作树中已经存在的名称,所以为什么要麻烦呢?
如果需要,您可以通过 Git 来重建合并冲突:
\n\n git checkout -m -- path/to/file\n
Run Code Online (Sandbox Code Playgroud)\n\nGit 将三个副本放回到三个插槽中,并重新运行合并驱动程序,覆盖工作树副本。8
\n\n为了获得git diff
此时的组合差异,您必须将三个副本放入索引中。如果您确实想要,有一种方法可以使用 ,在任何暂存槽编号处将任意文件内容上传到索引中git update-index
,但这很棘手:您必须首先将它们转换为 Git blob 对象并获取它们的哈希 ID。我不建议这样做,因为很难做到正确:
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
,它更易于使用,但不允许您写入非零槽号。)
一旦您提交合并结果\xe2\x80\x94作为合并,或精心挑选的提交,或任何\xe2\x80\x94,所有数据都git checkout -m
消失了,您无法以这种方式重建合并状态。然而,合并提交会记录其父提交,并且git show
在合并提交上运行会调用组合差异代码。
这里有一个很大的警告:git show
合并提交默认为--cc
(两个破折号,两个c)样式组合差异。这与冲突合并期间的输出不同git diff
,而冲突位于索引的非零暂存槽中。使用git show -c
强制 Git 使用-c
one-dash one-c 样式,这更接近(但仍然不相同)git diff
冲突合并期间的输出。
7这不太正确,因为当您修改工作树副本时,您将看到输出发生git diff
变化。Git 知道这些不是我们关心的:我们真正希望看到 slot-2-vs-work-tree 和 slot-3-vs-work-tree。这就是这里的差异和组合。
8您git checkout -m
无需git add
先将文件标记为已解决即可执行此操作。在这种情况下,三个插槽已经满了并准备就绪。不过,工作树副本仍然会被破坏,这可能是这里最重要的部分。
这根本不是一回事,但您可能对interdiffs和range diffs感兴趣。请参阅interdiff 能做什么而 diff 不能做什么?以及如何获得这两个 git 提交之间的差异?了解更多信息。
\n