Git,如何判断合并文件时发生了什么变化 'deleted by us'

Git, how to tell what changed when merging a file 'deleted by us'

  1. 我在功能分支中删除了一个文件,因为我在其他地方重构了它的代码。

  2. Develop 分支更改了此文件中的代码以修复错误。

  3. 我将开发分支合并到我的功能分支中,以便在我继续开发该功能时使其保持最新。

我收到有关文件的 'Deleted by us' 冲突。

如何获得对开发分支上的文件所做更改的差异,以便我可以将这些更改重新实现到我的功能分支上的重构代码中?

您想查看索引槽 #1,并将其与索引槽 #3 中的内容进行比较,以获得路径:

git diff :1:path :3:path

您还可以提取各种index-slot版本,使用git checkout-index,然后使用普通文件操作而不是仅使用[=276来检查它们=] 工具。 git mergetool 程序执行后者,因此如果您使用 git mergetool,您将拥有该文件的两个版本。 (我自己从不使用 git mergetool。)

这是什么意思?

根据定义,任何合并都有三个输入。三个输入是 commits,1,其中包含文件的快照,所以通常这三个输入会导致每个文件的三个版本。这三个文件版本结束了索引中的这些编号槽:也就是说,对于正在合并的某些文件 path/to/file.ext,有一个 :1:path/to/file.ext、一个 :2:path/to/file.ext 和一个 :3:path/to/file.ext. (在这种情况下,当 b运行 中的一个 删除 一个文件时,这三个条目中的一个不存在。)

让我们倒序列出索引槽,因为最后一个是这里的关键:

  • 插槽 3(他们的) 保存 他们的 文件,在 develop b 的顶端运行ch:你运行git merge develop。请记住,每次提交都是所有文件的完整快照。这不是一组 更改 ,它只是一个快照。然而不知何故,Git 知道他们 更改了 某个特定文件。改了,关于what?

  • 插槽 2(我们的) 保存您当前提交的文件(也称为 HEAD)。在这种情况下,HEAD 中的某些文件已被删除。但是:Git 怎么知道你 删除了 文件?删了,关于什么?

  • 插槽 1(基地) 保存来自 合并基地 的文件。这就是这两个中的什么

合并基础是您的分歧 branch-tips 开始的(单一/最佳)共同提交。 Git 将此提交(实际上是整个提交)与两个 branch-tip 提交(作为一个整体)中的每一个进行比较,以匹配文件。2 然后,匹配了三个提交中的所有文件后,Git 将开始整个 "put some files into special slots" 过程。

也就是说,如果我们在您开始 git merge 时绘制提交图,它看起来像这样 — 尽管确切的细节会有所不同,而且通常很难扫描图来找到 BRHEAD 或提交 L 通常很容易找到):

          o--...--L   <-- our-branch (HEAD)
         /
...--A--B
         \
          o--...--R   <-- develop

提交 AB(以及早于 A 的所有内容)都在 both b运行ches 上,因此是共享的, 但 B 最好的 一个,因为它是 最后一个 共享的一个。

为了进行合并,Git 比较了 BL 以查看 我们 发生了什么变化,并且 B vs R 看看 他们 改变了什么。没有人更改的文件在所有三个提交中都是相同的,因此 Git 使用此类文件的任何版本。对于只有 他们 更改的文件,Git 从提交 R 获取他们的文件版本。对于只有 we 更改的文件,Git 从提交 L.

获取该文件的我们版本

对于我们 都更改的文件,在某些方面——包括 "delete the file entirely"–Git 必须更加努力。现在重要的是要了解索引——Git 使 new 提交的东西——在合并期间具有扩展的作用。

通常情况下,索引对于每个文件只有一个槽。这个插槽已编号,但它是 number-zero 插槽,因此您通常不需要做任何特别的事情来引用它:您只需告诉 Git 到 git 添加 <em>somefile</em> 将文件 somefile 从 work-tree 复制到索引中,使其准备提交。

但是,对于合并案例,我们和他们都对一个文件做了一些事情,Git 需要 三个 ——好吧,最多三—每个文件的副本。因此,对于这种特殊情况,Git 将来自提交 B 的文件的 merge base 版本放入索引槽 #1。 Git 将我们的文件版本(来自提交 L 并且已经在索引槽 #0 中)移动到索引槽 #2 中,并将来自提交 R 的文件的另一个版本放入索引中插槽 #3。

对于删除的文件(这种特殊情况)Git 将插槽 #2 或 #3 留空,具体取决于谁删除了文件。对于 add/add 冲突——文件不存在于 B 但存在于 LR 中——Git 将插槽 #1 留空。 (不存在 delete/delete 冲突这样的事情:如果我们都删除了文件,Git 只是删除文件并继续。但是有一些重命名的情况,这更棘手。)

当合并因合并冲突而停止时,这些索引槽仍然填充三个,或者在第第二种情况,文件的版本。因此,您可以检查索引,查看 higher-stage(非零)插槽,并了解哪些文件存在冲突。各种 Git 工具,包括 git statusgit diff,都可以执行此操作。

当您通过任何方式解决冲突后,您必须告诉 Git 清除 higher-stage 槽并将文件的良好副本放入索引槽 #0。最简单的方法是 git add 文件的正确版本。 (如果没有正确的版本——如果它应该消失——你可以 git rm 文件,从所有索引槽和 work-tree 中删除它。一般来说,如果它不在 work-tree, git add 也将其从索引中删除,尽管我习惯于 git rm-ing 应该消失的冲突文件,所以我没有测试 git add 是否一致关于在此处删除 higher-stage 条目。如果它不在 work-tree 中,git rm 将其从索引中删除,抱怨它不在 work-tree 中,然后一切都是好。)


1在偷偷摸摸的特殊情况下,合并尚未提交的文件是可能的。例如,git checkout -mgit stash apply 会出现这种情况。在这种情况下,Git 通常只是根据需要将项目从插槽 0 移动到插槽 2 ...以及 work-tree 中但从未提交的更改,can 被期望插槽 #2 是 work-tree 中内容的安全副本的工具破坏 and/or! (这是我不喜欢 git stash 的原因之一。)但是 运行ning git merge 不会调用这个奇怪的路径,事实上,如果你的存储库不在一个很好的 ready-to-start-a-merge状态。

2这是重命名检测的用武之地。如果您在提交中重命名了某个文件, :1:path/to/file.ext 现在可能是 :2:path/different/file.ext:2:path/to/different.ext。这些文件被检测为同一个文件,尽管它现在有两个不同的名称。这里有一个小缺陷,因为在合并的整个过程中插槽没有链接在一起。例如,如果合并因冲突而停止,则很难恢复 2:path/to/different.ext:1:path/to/file.ext 一致的事实。 Git 打印 信息,因此它可能仍然在 window 中,您可以在笔记本电脑或其他任何设备上看到,但不会记录在其他地方。


脚注:more-tangled 图表

这是一个 b运行ch 的示例图,其中包含一些重复的合并:

...--A--B--C--D--G--H--K   <-- branch1
         \     \     \
          E-----F--I--J--L   <-- branch2

这里,FJ都是合并提交,有两个parent:F的两个parent是EDJ的两个parent是IH。如果您 运行 git checkout branch2; git merge branch1,您将尝试进行新的合并提交 M,其第一个 parent 是 L,第二个是 K。这里的合并基础是提交 H 因为从 K 开始并向后工作,我们得到 H,而从 L 开始并向后工作,我们得到 J 然后——可以说是同时——到 IH,因为我们已经到达顶部的 H,这就是合并基础。

请注意,合并基础计算是对称的。如果我们 git checkout branch1 && git merge branch2,Git 仍然选择 H 作为合并基础。但是,如果合并基础不明显,您可以 运行:

git merge-base --all branch1 branch2

这将产生 "best merge base" 的所有候选人。

理想情况下,只有一个。然而,历史看起来像这样:

...--o--o---A--...   <-- branch1
         \ /
          X
         / \
...--o--o---B--...   <-- branch2

其中 AB 都是 "equally close" 到两个 b运行ch 提示的合并提交,有 两个 合并基地。这是通过从 b运行ch1 合并到 b运行ch2 并立即进行合并,使用 git merge --no-ff,从 b运行ch2 到 b运行ch1,有时称为 criss-cross 合并。

在这个模棱两可的合并基本案例中,没有单一的最佳候选者。由于合并通常是对称的,因此提交 ABcontents 可能是相同的。在那种情况下,选择 AB Git 中的哪一个都无关紧要。但是如果内容不一样,也无所谓,这就是Git的递归合并的用武之地。当有多个合并基础时,Git会先,作为内部合并,合并两个合并基础(使用 他们的 最佳共同祖先,无论是什么)并根据结果进行新的提交。 Git 将使用该新提交的内容作为外部合并的合并基础。

-s resolve 合并策略(不是默认的)选择两者之一,AB,在 apparent-random(真的,只是更方便的算法)。如果 AB 具有相同的内容,则可以正常工作;如果不是,递归 "merge A and B first" 的内部合并可能会 产生更好的结果。如果递归合并有冲突,它会产生一些混乱。 (通常明智的做法是避免 criss-cross 合并。)