当文件报告为新/已删除时如何区分更改

How to diff changes when file reports as new / deleted

我重命名了两个文件并进行了一些更改(在 Visual Studio 中)。 git 状态显示如下:

    On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    deleted:    Core/Models/Metadata/MetadataModel.cs
    deleted:    Core/Models/Metadata/MetadataModelCollection.cs
    new file:   Core/Models/Metadata/MetadataValueModel.cs
    new file:   Core/Models/Metadata/MetadataValueModelCollection.cs

如果我尝试 git diff --staged 它不会显示已删除文件和新文件之间的差异。相反,它将每个文件中的所有行列为已删除或已添加。不足为奇,因为 git 没有将更改识别为重命名。

如何区分 MetadataModel.cs 和 MetadataValueModel.cs? 或者 MetadataModelCollection.cs 和 MetadataValueModelCollection.cs?

以防万一,我使用的是 Windows 10 Pro 和 git 版本 2.20.1.windows.1

TL;DR

您在这里有两个选择:要么进行多次提交,每一步进行较小的更改。或者,使用 --find-renames=<em>percentage</em> 参数(拼写为 -X find-renames=... for git merge,但 --find-renames=...-M... for git diff),将相似度阈值从默认的 50% 降低。请注意,git status 没有旋钮:git status 始终使用 50%。

这基本上是一个关于身份的问题。从哲学上讲,这是 The Ship of Theseus,或祖父之斧悖论。 ("This is my grandfather's axe. My father replaced the handle, and I replaced the head, but it's the same axe. Or is it?")

在时间点 A 和时间点B,这样虽然整个名字不一样,大部分内容也不一样,但我们应该叫它"the same"文件?好吧,您可能自己进行了重命名,所以 当然知道。 :-) 但是 Bob 或 Carol 会知道吗?如何? Git会知道吗?

最后的答案是没有,Git不会知道。 Git干脆不记录此信息。 Git 只是制作和使用快照。快照要么一个名为Core/Models/Metadata/MetadataModel.cs的文件,要么没有具有该名称的文件。如果两个待比较的快照都有同名文件,Git 假设1 两个文件都是 "the same" 文件,只是内容有所改变.如果一个快照有文件而另一个没有,那就更复杂了。

Git 所做的是(尝试)检测 事后重命名。假设左侧快照有 Core/Models/Metadata/MetadataModel.cs 而右侧快照没有,但左侧快照 没有 Core/Models/Metadata/MetadataValueModel.cs 而右侧快照。例如,这里就是这种情况。

在这种情况下,文件有可能被重命名(也可能被修改)。如果您要求 Git 这样做,Git 将比较左侧 all 文件的 contents而不是 所有 文件的右侧而不是左侧。对于内容足够相似的任何两个文件,Git 为这对文件分配一个 "similarity score",Git 表示为百分比——介于 0%(完全不相似)和100%(完全相同)。

100% 相同的情况特别有用,因为它可以保证工作并且非常快。2 所以如果你 重命名 一个文件 完全不更改 ,然后立即提交结果,"before" 和 "after" 提交几乎相同。它们具有所有相同的文件,具有所有相同的内容,除了一对文件——或两对,或 N 对,如果您重命名两个或 N 个文件。 Git 可以比较左侧提交和右侧提交,看到除了重命名的文件之外所有文件都已经配对,然后使用快速 100% 完全匹配的情况进行内容比较和检测重命名。

进行中间提交后,您可以对重命名的文件进行更改——甚至是大量更改,然后进行另一次提交。当 Git 比较父子提交时,所有文件都具有相同的 names,即使某些文件的内容发生了巨大变化,然后 Git 可以给出您为 更改名称的配对文件逐个文件差异。 (再次参见脚注 1。)

不会 当您比较第一个快照(重命名前)与最后一个快照 post-重命名 post-巨大的变化。只有当您重新命名为 post-rename,然后作为第二步,post-rename 为 post-massive-change 时,它​​才会有所帮助;或者等效地,一次向后提交一个,就像 Git 通常那样。所以它对以后的 git merge.

没有多大帮助

对于不合适的情况——包括在 git merge 时间,当 git merge 运行s git diff --find-renames 在 base 和 tip 上提交时从未查看任何内容之间的提交数量——您可以降低最小相似度。我们在上面所做的,通过两次提交,利用了快速简单的案例:给定两个名称不同但内容 100% 相同的文件,Git 轻松地将它们配对。但是给定两个名称不同且只有 90% 相似内容的文件,Git 仍然可以将它们配对。它只是需要更多的工作。

改名文件的内容越多,越难说这两个文件相似。但是 Git 无论如何都会尝试——它会尝试所有可能的配对。3 最好的匹配,不管是什么,都是被采用的匹配,只要 它达到或超过您指定的 最小值 匹配。该最小值默认为 50%。

要选择默认值以外的值,例如,使用 git diff --find-renames=30 表示 30%,使用 git merge -X find-renames=30 在合并期间使用相同的减少限制。你怎么知道要使用多少百分比?答案真的只是 尝试一下 — 相似性指数计算有点奇怪,所以您只需要试验一下,看看什么对您的案例有效。如果您有两个提交哈希 ID,您可以 运行 git diff --find-renames=25 --name-status --diff-filter=R 来查看配对率为 25% 的内容,如果配对过多或过少,则重复使用 75 或您喜欢的任何其他数字。

当你运行git status,那运行s 两个 git diffs,每两棵树:

  • HEAD 对比指数
  • 索引与工作树

两个比较都已打开重命名检测并设置为 50%。没有更改此选项的选项。

索引和工作树都不是实际提交,所以你不能完全将它们交给 git diff,但 git diff 本身可以进行相同的比较,在这里你可以使用选项:

git diff --cached --name-status --find-renames=...  # for HEAD vs index
git diff --name-status --find-renames=...           # for index vs work-tree

添加 --diff-filter=R 以仅显示检测到的重命名,如果这是您关心的。

请注意,--find-renames 自 Git 2.9 起默认开启,而在早期的 Git 版本中默认关闭。使用 --find-renames 以 50% 或您提供的数字打开检测。配置设置 diff.renames 可以设置为 truefalsecopiescopy。只有 porcelain diff 命令(例如 git diffgit loggit show)使用配置的 diff.renames——管道命令不受用户设置的影响。 (这是他们 "plumbing commands" 的重要组成部分。)


1当使用git diff时,你可以告诉Git打破一对。也就是说,如果您有两个名称相同但内容完全不同的文件,您可以告诉 Git: 在进行重命名检测之前,将内容差异太大的文件对分开。将分解的对放入重命名检测池。此选项在git merge中不可用,仅在git diff中可用。

2Git 通过哈希 ID 存储每个内容,因此检测到提交 A 中名称为 X 的文件与提交 B 中名称为 Y 的文件 100% 相同只是查看哈希 ID 的问题。如果哈希 ID 匹配,则文件也匹配。找到这些 100% 相同的内容匹配后,Git 现在已将 A:X 与 B:Y 配对,这两个名字不再在 "files to be paired" 池中。

请注意,虽然这既快速又简单并且保证有效,但如果还有一个 B:Z 与 A:X 100% 相同,则无法确定 A:X 是否会继续与 B:Y 或 B:Z 匹配。在这里,您可能想要启用 Git 的 copy 检测,而不是 - 或者除了 - 重命名检测之外,这样 Git 可以说 A:X 已 复制 到 B:Y 和 B:Z。这里的交互细节有点复杂。

3事实上,Git 尝试配对的次数是有限制的。重命名检测代码有两个文件名队列:左侧不匹配右侧不匹配。相似度计算必须比较每个左右条目,即 len(left) * len(right) 文件比较。如果两个长度为 N,则为 N2——计算量非常大。 Git 因此有一个名为 renameLimit 的设置,它限制了队列的长度。这个限制原来是100,然后在Git 1.5.6增加到200,然后在Git 1.7.4.2 / 1.7.5增加到400,但是你可以通过配置限制到[=49]来设置它到"unlimited" =],如果你愿意(尽管 Git 仍会在内部将其限制为 32767)。

有一个可单独配置的合并重命名队列长度限制,当前默认为 1000。如果设置 diff.renameLimit 但不设置 merge.renameLimit,两者都使用 diff.renameLimit 值。