Git 在不同位置合并相同文件名的问题

Git merge problem with same filename on a different location

一个奇怪的问题:

我有以下例子:

文件夹B中的文件原来是文件夹A中文件的副本。

一位同事确定她编辑了文件夹A中的文件,但是对文件夹B中的文件进行了master上的更改。 这可能吗?是否存在可能发生此类事情的已知问题,或者我们正在寻找人为错误?

99.9999% 的人为错误。

但尝试 git 记录分析

可以在一种情况下自动发生:Git必须“误检测”重命名的文件。而且,为了 Git 检测到 any 重命名,我们必须有一些文件被删除,还有一些其他文件被创建。因此,在 Git 中,重命名与删除和添加“相同”。 (这就是 为什么 无论您使用 git mv 还是只删除一个文件并添加您通过复制制作的另一个文件都没有关系。)

请记住,每次提交都会存储每个文件的完整快照。在将任何一个提交与任何其他提交进行比较时,没有可用的信息,关于如何一个提交被更改为另一个提交。相反,Git 只有两个快照。 Git 现在必须玩一局 Spot the Difference。当然,Git 有两组 文件[=,而不是两张带有图像的图片进行比较(例如,两个时钟:它们显示的时间是否相同?) 159=],每个文件都有一个文件名——例如 dir1/file.extdir2/file.ext,用(正向)斜杠完成——两个提交中的每个文件都有一些内容。

这个操作——比较两个提交——是git diff命令的领域,Git中的差异发现是由差异引擎完成的.在这种情况下,Git 中的差异引擎以左侧提交中每个文件 name 的列表开始,每个文件 name 在右侧提交中。

如果出于某种原因,左侧提交包含 dir1/file1.ext dir2/file2.ext,并且右侧提交包含 dir2/file2.ext 不是 dir1/file1.ext,那么我们有两个杂散文件。 也许,Git 对自己说,只是也许,而不是两个杂散文件,我们这里有一个文件对。也许用户重命名 dir1/file1.ext 为新名称dir2/file2.ext.1 为了决定是否如此,Git将比较这两个文件的内容。比较内容,Git 获得一个百分比,它称为 相似性指数

Git 对未配对的左侧和右侧文件的所有可能配对重复此过程,因此如果左侧提交有此未配对的 dir1/file1.ext 并且右侧有两个未配对的文件 dir2/file2.extdir3/file3.ext, Git 将计算 file1 → file2 重命名的相似度索引,以及 file1 → file3 重命名的相似度索引。此处具有较高相似性指数分数的“获胜”。该分数还必须超过某个最低阈值。默认值为 50%,但您可以在 运行 git diff 时选择其他值。如果最佳得分超过最低阈值,Git 声明文件 1 实际上已重命名为赢得得分的右侧文件。

确切的评分方法是模糊的,但在实践中它工作得很好,至少只要文件中至少有几千字节的文本。 Git 如果右侧名称的最后一个“组件”部分与左侧的“组件”部分匹配,则 Git 还会秘密提高相似度指数 1%,其中“组件”是由(向前) 斜线。因此,如果 dir1/file1.ext 的两个相同副本,在 dir2/file2.extdir3/file1.ext 中,Git 将判定重命名为从 dir1/file1.extdir3/file1.ext。相似度得分本来是相同的——比如说,70% 相似——但是 file1.ext 得到了 1% 的提升,使 dir3/file1.ext 获得了 71% 的获胜得分。2


1Git,当然,实际上并没有跟自己对话。但这可以很好地作为 Git 所做的事情的心智模型。 (另外:不要将计算机拟人化;他们讨厌那样。)

2这个 1% 的提升技巧非常俗气。 Git 应该有一种更聪明的方法来处理这个问题,而现代 Git 有时会尝试变得更聪明并识别整个目录重命名。该代码虽然有很多问题。希望它现在变得更加稳定和可靠。


当你在看合并时,为什么我在谈论差异?

我们需要花时间了解 git diff 的工作原理的原因是 git merge 运行 两个 git diff .3 合并过程有三个输入提交:

  • 这三个中的一个是您的 当前提交,由 git switchgit checkout select编辑(以哪个为准一个你曾经来过这里)。例如,您可能 运行 git switch main 将 select main 的当前最后 提交作为当前提交。

  • 这三个中的一个是 您在命令行中命名的提交: 您现在可以 运行 git merge feature/tall,例如, 合并 feature/tall.

    last 提交
  • 第三个也是非常重要的提交是 Git 在给定这两个输入的情况下自行找到的提交。此提交是 两个 提交的 最佳公共(共享)祖先 ,如提交图中所示。我们将在这里忽略所有用于执行此操作的图论,只是将其视为魔法,尽管没有实际的魔法,您可以 运行 git merge-base --all 手动找到它。

最后一次提交(Git 自己找到的提交)是 合并基础 ,这也是 Git 实际进行合并的方式。此时,找到合并基础提交,Git 运行 两个 git diff 命令。 这两个 git diff 命令中的每一个都可以找到一些文件重命名操作集 如果找到了,Git 在组合对各种文件的更改时会考虑到这一点文件。

例如,

Git 可能决定从 base 到“他们的”(feature/tall) 提交,他们dir1/file1.ext 重命名为dir3/file1.ext。这只有在他们 在他们的 (feature/tall) 提示提交中没有 dir1/file1.ext 时才会发生,但是当然,这是完全可能的,特别是如果检测到重命名确实发生了。但可能是他们在某些提交中删除了 dir1/file1.ext,并添加了一个新的但不相关的 dir3/file1.ext,而 Git 只是在调用重命名时失败了。

请注意,当这种情况确实发生时,Git 在将合并基础提交与 您的 进行比较时检测到的重命名(如果有的话)也很重要(main) 使用 git switchgit checkout 切换到的提示提交。如果 Git 检测到您进行了相同的重命名,那么 Git 没问题。如果 Git 检测到您根本没有 dir3/file1.ext,并且将 dir1/file1.ext 命名为 dir1/file1.ext,Git 会说您没有重命名文件,他们做到了,并将通过保留重命名来合并这两项工作。

如果 Git 决定 both 他们重命名 dir1/file1.ext,那没关系,只要你们都选择了相同的最终名称。在这种情况下,Git 保留重命名。如果你们都重命名了文件,但使用了两个不同的名称,则会出现 rename/rename 冲突 并且 git merge 将在合并完成之前停止合并,因此从进行合并的用户那里获得帮助。选择正确的文件名,并通常构建正确的合并结果将成为他们的责任。

您还可以看到 rename/delete 冲突,其中合并的“一方”重命名文件,但另一方将其完全删除。所有这些类型的冲突都是我所说的高级冲突。有些人称它们为 tree 冲突。所有这些都会导致 git merge 停下来寻求用户的帮助:-X ours-X theirs 选项不计入此处。

请注意,如果一方重命名文件(或 Git 无论如何这么认为)而另一方,您 不会 得到高层/树冲突对同一个文件什么都不做,或者改变它的内容。对文件的任何内容更改都会像往常一样合并,如果更改中有重叠 (或邻接行,但让我们忽略这种细节),则会发生合并冲突。在这里,-X 选项将像往常一样解决那些 low-level 冲突。 Git 将通过重命名将一侧的高级/树级重命名与另一侧的无更改结合起来。

如果在合并期间或合并之后,有人注意到“嘿,有些文件不见了”(因为它已重命名)并且放回它的副本,这可能会导致你所描述的。对于不了解 Git 如何处理高层冲突的人来说,这很容易发生。


3同样,实际情况实际上比这复杂得多,但这足以很好地理解合并的作用。