【问题标题】:Git Diff of same files in two directories always result in "renamed"两个目录中相同文件的 Git Diff 总是导致“重命名”
【发布时间】:2017-03-14 02:10:35
【问题描述】:

git diff --no-index --no-prefix --summary -U4000 directory1 directory2

这按预期工作,因为它返回两个目录之间所有文件的差异。添加的文件按预期输出,删除的文件也会产生预期的差异输出。

但是,由于 diff 将文件路径作为文件名的一部分考虑在内,因此在两个不同目录中具有相同名称的文件会导致 diff 输出带有重命名标志而不是更改。

  1. 有没有办法告诉 git 不考虑 diff 中的完整文件路径而只查看文件名,就好像文件来自同一目录一样?

    李>
  2. git 有没有办法真正知道不同目录中的同一文件的副本是否真的被重命名了?我不知道如何,除非它有办法以某种方式比较文件 md5s 或其他东西(可能是一个错误的猜测,哈哈)。

  3. 使用分支代替目录是否可以轻松解决此问题?如果可以,上面列出的命令的分支版本是什么?

【问题讨论】:

    标签: git version-control diff git-branch


    【解决方案1】:

    这里有多个问题,其答案相互交织。让我们从重命名和复制检测开始,然后转到分支。

    重命名检测

    但是,由于 diff 将文件路径作为文件名的一部分考虑在内,因此两个不同目录中的同名文件会导致 diff 输出带有重命名标志而不是更改。

    这不太对。 (下面的文字旨在解决您的第 1 项和第 2 项。)

    虽然您使用的是--no-index(大概是为了让Git 在存储库之外的目录上工作),但Git 的差异代码在所有情况下的行为方式都是相同的。为了区分(比较)两棵树中的两个文件,Git 必须首先确定文件身份。也就是说,有两组文件:“左侧”或树(第一个目录名称)中的文件,以及“右侧”或目标中的文件> 树(第二个目录名)。左边的一些文件与右边的一些文件同一个文件。左侧的一些文件是不同的文件,没有对应的右侧文件,即它们已被删除。最后,右侧的一些文件是新的,即它们已创建

    “相同文件”的文件不需要具有相同的路径名。在这种情况下,这些文件已被重命名

    这是它的详细工作原理。请注意,使用git diff --no-index dir1 dir2 时“完整路径名”会有所修改:“完整路径名”是去掉dir1dir2 前缀后剩下的内容。

    在比较左侧树和右侧树时,具有相同完整路径名的文件通常自动被视为“同一文件”。我们将所有这些文件放入“要区分的文件”队列中,没有一个文件会显示为被重命名。请注意这里的“正常”一词——我们稍后再讨论。

    这给我们留下了两个剩余的文件列表:

    • 存在于左侧但不存在于右侧的路径:没有目的地的源
    • 存在于右侧但不存在于左侧的路径:没有源的目的地

    天真地说,我们可以简单地声明所有这些源端文件都已删除,并且所有这些目标文件都已创建。您可以指示git diff 这样做:设置--no-renames 标志以禁用重命名检测。

    或者,Git 可以继续使用更智能的算法:设置 --find-renames 和/或 -M <threshold> 标志来执行此操作。在 Git 2.9 及更高版本中,重命名检测默认开启。

    现在,Git 应该如何确定源文件与目标文件具有相同的身份?他们有不同的路径;左侧的a/b/c.txt 对应哪个右侧文件?可能是d/e/f.bin,或d/e/f.txt,或a/b/renamed.txt,等等。实际算法比较简单,过去并没有使final name组件生效(我不确定现在是否生效,Git在不断发展):

    • 如果存在内容完全匹配的源文件和目标文件,请将它们配对。因为 Git 对内容进行哈希处理,所以这种比较非常快。我们可以通过查看左侧的 a/b/c.txt 的哈希 ID 与右侧的 每个 文件进行比较,只需查看所有 它们的 哈希 ID。因此,我们首先遍历所有个源文件,找到匹配的目标文件,将新的对放入差异队列并将它们从两个列表中拉出。

    • 对于所有剩余的源文件和目标文件,运行一个高效但不适合git diff 输出的算法来计算“文件相似度”。至少<threshold> 与某个目标文件相似的源文件会导致配对,并且该文件对被删除。默认阈值为 50%:如果您在未选择特定阈值的情况下启用了重命名检测,则此时仍在列表中且相似度为 50% 的两个文件配对。

    • 所有剩余的文件要么被删除要么被创建。

    现在我们已经找到了所有配对,git diff 继续比较配对的相同身份的文件,并告诉我们删除的文件被删除,新创建的文件被创建。如果相同身份文件的两个路径名不同,git diff 表示文件已重命名。

    任意文件配对代码很昂贵(即使同名配对代码非常便宜),因此 Git 对这些名称中的多少名有限制配对源列表和目标列表。该限制是通过git config diff.renameLimit 配置的。多年来,默认值一直在攀升,现在有数千个文件。您可以将其设置为 0(零)以使 Git 始终使用自己的内部最大值。

    断对

    上面我说过通常,同名文件会自动配对。这通常是正确的做法,因此它是 Git 的默认设置。然而,在某些情况下,名为a/b/c.txt 的左侧文件实际上与名为a/b/c.txt 的右侧文件相关,它实际上与右侧的a/doc/c.txt 相关。例如。我们可以告诉 Git打破“太不同”的文件对。

    我们看到了上面用于形成文件配对的“相似性指数”。相同的相似性索引可用于拆分文件:例如-B20%/60%。这两个数字加起来不必等于 100%,您实际上可以省略其中一个,或两者都省略:如果您设置 -B 模式,每个数字都有一个默认值。

    第一个数字是可以将默认已配对文件放入重命名检测列表的点。对于-B20%,如果文件有 20% 不相似(即只有 80% 相似),则文件将进入“重命名源”列表。如果它从未被视为重命名,它可以与它的自动目的地重新配对——但此时,第二个数字,即斜线后面的那个,将生效。

    第二个数字设置了配对肯定中断的点。例如,对于-B/70%,如果文件有 70% 不相似(即只有 30% 相似),则配对会中断。 (当然,如果文件作为重命名源被拿走,那么配对就已经失效了。)

    拷贝检测

    除了通常的配对和重命名检测之外,您还可以让 Git 查找源文件的副本。在运行所有常见的配对代码后,包括查找重命名和断开配对,如果您指定了-C,Git 将查找实际从现有源复制的“新”(即未配对)目标文件。这有两种模式,取决于您是指定两次-C 还是添加--find-copies-harder:一种只考虑经过修改 的源文件(这是单个-C 的情况),另一种是考虑 每个 源文件(即 -C--find-copies-harder 两种情况)。请注意,在这种情况下,“源文件被修改”意味着源文件已经在配对队列中——如果不是,则根据定义它没有被“修改”——它对应的目标文件具有不同的哈希 ID(同样,这是一个成本非常低的测试,有助于保持单个 -C 选项的成本较低)。

    分支无关紧要

    使用分支代替目录是否可以轻松解决此问题?如果可以,上面列出的命令的分支版本是什么?

    分支在这里没有区别。

    在 Git 中,术语 branch 是模棱两可的。请参阅What exactly do we mean by "branch"? 对于git diff,不过,分支 name 只是解析为单个提交,即该分支的 tip 提交。

    我喜欢这样画 Git 的分支:

    ...--o--o--o   <-- branch1
             \
              o--o--o   <-- branch2
    

    小圆 os 每个代表一个提交。这两个分支名称在 Git 中只是 指针:它们指向 一个特定的提交。名称branch1 指向顶行最右侧的提交,名称branch2 指向底行最右侧的提交。

    Git 中的每个提交都指向其父级或父级(大多数提交只有一个父级,而合并提交只是具有两个或多个父级的提交)。这就是形成提交的,我们也称之为“分支”。分支name 直接指向链的tip1

    当你运行时:

    $ git diff branch1 branch2
    

    Git 所做的只是将每个名称解析为其对应的提交。例如,如果 branch1 名称提交 1234567...branch2 名称提交 89abcde...,这与以下内容相同:

    $ git diff 1234567 89abcde
    

    Git 的 diff 需要两棵

    Git 甚至不在乎这些是提交,真的。 Git 只需要一个左侧或源树,以及一个右侧或目标树。这两个树可以来自一个提交,因为一个提交命名一棵树:任何提交的 tree 都是您进行该提交时拍摄的源快照。它们可以来自一个分支,因为一个分支名称命名一个提交,它命名一棵树。其中一棵树可以来自 Git 的“索引”(又名“暂存区”又名“缓存”),因为索引基本上是一棵扁平树。2其中一棵树可以是您的工作树.一棵或两棵树甚至可能不在 Git 的控制范围内(因此有 --no-index 标志)。

    当然,Git 只能区分两个文件

    如果您运行git diff --no-index /path/to/file1 /path/to/file2,Git 将简单地区分这两个文件,即将它们视为一对。这完全绕过了所有配对和重命名检测代码。如果没有对--no-renames--find-renames--rename-threshold 等的摆弄,选项就可以解决问题,您可以显式区分文件路径,而不是目录(树)路径。对于大量文件,这当然会很痛苦。


    1在那个点之后可以有更多的提交,但它仍然是其链的末端。此外,多个名称可以指向一个提交。我把这种情况画成:

    ...--o--o   <-- tip1
             \
              o--o   <-- tip2, tip3
    

    请注意,“在”多个分支名称“后面”的提交实际上是在这些分支的所有上。所以底行提交都在tip2tip3 分支上,而顶行提交都在所有三个 分支上。尽管如此,每个分支 name 都解析为一个,并且只有一个,提交。

    2事实上,为了进行 new 提交,Git 只需使用 git write-tree 将索引转换为树,就像现在一样,然后进行命名该树的提交(并将当前提交用作其父级,并具有作者和提交者以及提交消息)。 Git 使用现有索引的事实是为什么您必须在提交之前将更新的工作树文件git add 放入索引中。

    有一些便捷的快捷方式可以让您告诉git commit 将文件添加到索引中,例如git commit -agit commit &lt;path&gt;。这些可能有点棘手,因为它们并不总是产生您可能期望的索引。例如,参见git commit &lt;path&gt;--include--only 选项。它们还通过将主索引复制到新的临时索引来工作;这可能会产生令人惊讶的结果,因为如果提交成功,临时索引会被复制回常规索引。

    【讨论】:

    • 非常详细和有用的答案。您是否从 git 文档本身获得了所有这些信息?是否有一个切入点,可以从中导航以全面了解 git 及其内部工作原理?我不只是想成为该工具的消费者并真正理解它。
    • 我的问题的一部分仍未得到解答,但也许我没有详细说明它,但现在通过您的解释提供的额外理解,我可以更清楚地问它。 “如果相同身份文件的两个路径名不同,则 git diff 表示文件已重命名。” - 有没有办法将标志设置为更改而不是重命名,因此忽略相同身份文件的路径名,并且仅在同一身份配对的文件名本身发生更改时才设置为重命名?
    • @LorenShqipognja:其中一些没有在任何地方正确记录(特别是没有详细描述重命名遵循逻辑,也没有在任何地方正式定义相似性索引代码:我查看了 Git 源代码其中一些代码)。
    • @LorenShqipognja:至于最后一个问题,这里根本没有任何标志。给定配对队列中的 文件对,它们要么具有相同的(完整)路径名,要么具有不同的名称。您可以使用-M/--find-renames-B 来控制是否可以将不同名称的文件配对(-M),或者同名的文件可以不配对(-B)。当你从队列中取出对时,改变 Git 行为的任何东西都为时已晚。你唯一能做的就是首先使用--no-renames 让他们远离这个队列。
    猜你喜欢
    • 2011-12-07
    • 2017-06-08
    • 2016-05-23
    • 1970-01-01
    • 2016-07-22
    • 2013-05-16
    • 1970-01-01
    • 2017-01-29
    • 1970-01-01
    相关资源
    最近更新 更多