Git log(--follow)无法显示重命名后的历史记录

5

我尝试通过gitlog显示文件的完整历史记录,但问题是该文件的父文件夹在历史记录中被重命名了,而我想查看完整的历史记录。

git-log文档表明参数--follow-M可以让git log跟踪重命名操作。

我尝试了不同组合的gitlog参数,例如:

git log -M --oneline --all -- --follow newpath/my-file.php

git log -M --oneline --all -- newpath/my-file.php,甚至使用了

git rev-list --all -- newpath/my-file.php --objects --in-commit-order | git log --no-walk --oneline --stdin

但无论我尝试什么,历史记录总是在文件所在的父文件夹被重命名的提交处结束。

我已经确认:

  • 只有文件夹在重命名提交中被重命名,文件的内容完全没有改变,所以git应该简单地发现旧路径上的文件和新路径上的文件是相同的,只是被重命名了。

  • 对于重命名提交,git short name-status显示R100 oldpath/my-file.php newpath/my-file.php(这证实了文件的内容完全相同)。

  • 历史记录的“旧半部分”和“新半部分”似乎都是正确的,两者都包括重命名提交。

  • 当我运行git log -M --oneline --all -- --follow newpath/my-file.php时,最早的提交是0979744 重命名:oldpath/ -> newpath/

  • 当我运行git log -M --oneline --all -- --follow oldpath/my-file.php时,最新的提交是0979744 重命名:oldpath/ -> newpath/

所以一切看起来都像是我的git成功理解了文件在新路径和旧路径上的重命名。 即使我使用了-M--follow选项,为什么历史记录仍然在重命名提交时出错?


5
如果运行命令 git log -M --oneline --all --follow -- newpath/my-file.php ,那会发生什么?其中,-- 标记表示选项结束。 - Niloct
我测试了一些移动文件夹中更改命令,似乎将--follow放在--后面是解决方案。 - Radon8472
1
即使下面的重命名似乎现在可以工作,但是当我添加 --grep="rename" --invert-grep 来删除“重命名”提交时,我得到了0个结果。 - Radon8472
1
好的,那是另一个问题。但也许解决方案是一样的:在 -- 部分后面不能添加任何选项。 - Niloct
1个回答

5
评论中所述--follow选项必须放在独立的--选项之前,后者表示选项列表的结束。

即使现在跟踪重命名似乎已经起作用了,当我添加--grep="rename" --invert-grep以删除 "rename" 提交时,却得到了0个结果。

这很有道理(但也是某种程度上的bug),因为--follow的工作方式。Git没有任何形式的文件历史记录。所有Git拥有的都是存储在仓库中的提交集合。提交是历史:
  • 每个提交通过其唯一的哈希ID进行编号,该哈希ID对于该特定提交是唯一的,任何其他 Git 存储库中的提交都不具有该哈希ID。

  • 每个提交都具有每个文件的完整快照。

  • 每个提交还存储先前提交(或对于合并提交,两个或多个先前提交)的哈希ID。

因此,这些数字将提交向后串在一起:
... <-F <-G <-H

这里的大写字母代表实际的提交哈希ID,Git通过这些ID找到提交记录。每个提交记录都有一个“向后指向箭头”——前一个提交记录的哈希ID,因此如果我们能记住链中最后一个提交记录的哈希ID,Git就可以沿着提交记录倒退查找。
分支名称只是告诉Git哪个提交记录是该分支的最后一个提交记录:
             I--J   <-- feature1
            /
...--F--G--H
            \
             K--L   <-- feature2

这里,提交J是一个特性分支的最后一次提交,而提交L是另一个特性分支的最后一次提交。请注意,提交H及其之前的提交在两个分支上(很可能也在主分支或master分支上)。 git log命令只是逐个遍历提交,从您选择的“最后提交”开始。默认的“最后提交”是您当前检出的任何分支的末尾提交。这个过程是向后工作的:Git从最后的提交开始,逐个提交向后工作。 -M选项是git diff的缩写,表示启用git diff中的重命名检测。--follow选项是git log的缩写,对git log执行相同的操作,但还需要指定要查找的一个特定文件的名称。(给git log添加-M选项会使其在每个diff中使用重命名检测器,但由于它不在寻找一个特定的文件,因此这只影响输出的-p--name-status样式。使用--followgit log 正在查找那个特定的文件,我们马上就会看到。)
重命名检测器的工作方式如下:
  • 您需要提供Git两个提交,之前之后,或者是旧的新的,或者像FG.(您可以把新提交放在左边,旧的放在右边,但是git log总是把老的放在左边,新的放在右边。)

  • 您需要让Git比较这两个提交中的快照。

  • 这些提交中的一些文件完全相同:它们具有相同的名称相同的内容。 Git的内部存储系统已经去重了这些文件,这使得git diffgit log可以很容易地决定这些文件是相同的,因此如果需要,它就可以直接跳过它们。

  • 其他文件具有相同的名称,但具有不同的内容。默认情况下,Git假设如果两个文件具有相同的名称(例如path/to/file.ext:请注意,嵌入式斜杠只是文件名的一部分),它们代表“相同的文件”,即使内容已经发生了改变。因此,该文件已经从旧/左侧提交更改为新/右侧提交。如果您要求--name-status,您将得到M,表示该文件名的状态已经修改过的

  • 有时,左侧的提交具有一个名为,例如,fileL的文件,而右侧的提交则根本没有这个文件。显然,在从旧(左)到新(右)的更改中删除了该文件。使用--name-status,您将得到D的状态。

  • 有时,右侧的提交具有一个名为,例如,fileR的文件,而左侧的提交根本没有这个文件。显然,该文件是新增的,并且使用--name-status,您将得到A的状态。

  • 但是,如果左侧的fileL和右侧的fileR应该被认为是“相同的文件”怎么办?也就是说,我们把fileL重命名为fileR?这就是Git的重命名检测器派上用场了。给定像这样的删除/添加对,也许fileL内容fileR内容足够接近,或者完全相同。如果:

    • 您已经打开了重命名检测器,它将实际执行此内容检查,
    • 内容检查说“完全相同”(由于去重非常快速,因此可以很快知道)或“足够相似”(比较慢,但由相同的重命名检测器开关启用),

    那么,仅当Git将声明fileL已被重命名为fileR时,--name-status输出将包括R,相似性指数值和两个文件名,而不是

    现在你知道了重命名检测器的工作原理——它必须被“开启”,那么你就可以看到--follow是如何工作的。请记住,通过git log,你可以给它一个文件名,并告诉它不要显示不修改该特定文件的提交。3结果是,你只会看到修改了该文件的提交:这是git log访问所有提交的子集。所以让我们假设你运行git log --follow --newpath/my-file.php:
    • git log像往常一样,向后遍历提交历史中的每个提交。

    • 在每个提交中,它将这个提交(较新,在右侧)与其父提交(较旧,在左侧)进行比较。没有--follow,它仍然会这样做,但只是查看你命名的文件是否已更改(M状态,来自git diff --name-status)或添加或删除(A,D)。4但是有了--follow,它还会查找R状态。

    • 如果文件已更改——具有M、A或D状态——git log会打印出此提交,但如果没有更改,则只会抑制打印输出。使用--follow,我们添加了R状态,如果出现这种情况,则添加两个文件名。如果状态是R,那么git log之前一直在寻找newpath/my-file.php。但是现在它知道,截至父提交时,该文件被称为oldpath/my-file.php。(请再次注意,此处没有文件夹。文件名是整个字符串,包括所有斜杠。)

    因此,使用--follow参数启用重命名探测器后,git log可以获取重命名状态,因此可以看到文件已被重命名。它还在寻找一个特定的文件名,例如,在本例中是newpath/my-file.php。如果检测到重命名,git log不仅会打印提交记录,还会更改正在查找的文件名。现在,从父提交开始向后,它将寻找oldpath/my-file.php而不是newpath/my-file.php
    1--follow选项本身并不是很好;整个实现需要重新编写,这可能比我想的简单hack更好地解决了问题。 2技术上,其他Git存储库可能有一个使用该哈希ID的不同提交记录,只要您不将两个提交记录相互引入即可。但实际上,你不会找到一个这样的。 3--follow选项只能跟踪一个文件名。没有--follow,您可以给git log提供多个名称,或者甚至是“目录”的名称,尽管Git根本不存储目录。没有--followgit log代码操作通用路径规范。使用--follow,它只处理一个文件名。这是Git在此处使用的算法所施加的限制。 4它还可能有T,类型更改,我认为那也算。完整的状态字母集是ABCDMRTUX,但X表示Git中的错误,U只能在未完成的合并期间出现,B只能在使用-B选项的git diff中出现,并且CR只能使用启用了--find-copies--find-renames(-C-M)选项时出现。请注意,根据您的diff.renames设置,git diff可能会自动启用--find-renames,但git log不会。

    --follow中的错误问题

    这个过程,从git log的输出中删除一些提交记录被称为历史简化。有一篇很长的文章在文档中描述了这个过程,并以一个相当奇怪的说法开始:

    有时你只对历史的部分感兴趣,例如修改特定

    这个奇怪的措辞试图表达的是,在启用历史简化的情况下,git log有时甚至不会遍历某些提交记录。特别是考虑到一个合并提交,其中两个提交记录串联在一起:
              C--...--K
             /         \
    ...--A--B           M--N--O   <-- branch
             \         /
              D--...--L
    

    要显示所有提交,需要使用git log遍历提交O,然后是N,再然后是M,然后是KL(以某种顺序),然后是K之前的所有提交和L之前的所有提交,直到回到CD,然后在提交B处重新连接单个线程,并从那里继续向后倒退。

    但是,如果我们不打算显示所有提交,也许只需在提交M时返回到仅提交K或仅提交L并完全忽略合并的另一侧。这将节省大量工作和时间,并避免显示与您无关的内容。通常情况下,这是一件非常好的事情。

    然而,对于--follow选项来说,这通常是一件很糟糕的事情。这是--follow的一个问题:有时Git会在进行此类简化时走错“错误的路径”。添加--full-history可以避免这种情况,但我们立即遇到另一个问题。--follow选项只有一个文件名。如果我们在两个路径中的其中一个中重命名了文件,但在另一个路径中没有重命名,而git log首先沿着重命名路径走,那么当它沿着另一个路径走时,它可能会寻找错误的名称

    如果文件在两个路径中都重命名了,以便从M重新命名为KM重新命名为L,或者Git碰巧首先沿着正确的路径走并且您不关心另一个路径,那么一切都可以正常工作。但这是需要注意的问题。(这不是使用--grep时遇到的问题,否则将在没有--grep的情况下发生。)

    我认为你看到的错误是--grep在“太早”触发。 --grep选项通过从git log的输出中消除任何提交(使用--invert-grep)或缺少其提交消息中某些特定文本的提交(使用--grep,但不使用--invert-grep)来工作。然后假设重命名提交-使得git log --follow知道使用名称oldpath/my-file.php-被您的--grep选项跳过。Git将不会看到R状态,并且不会知道将名称从newpath/my-file.php更改为oldpath/my-file.php。因此,git log --follow将继续寻找路径,并且您将仅获得符合grep条件并修改具有新名称文件的提交。
    可以通过让git log --follow无论如何运行差异引擎来修复此错误,即使它将因其他原因跳过提交。但更一般地,--follow需要完全重写:它具有一堆奇怪的特殊情况代码穿过差异引擎,只是为了使这个案例工作。它需要处理多个路径名称和/或路径规范,并与--reverse和其他选项一起工作。它需要一种方法将旧名称和新名称堆叠到提交路径上,以便在--full-history下,沿着合并的两条腿向下走时,它知道要查找哪个路径。请注意,这还有其他影响:如果沿着合并的两条腿向下走,有不同的重命名?如果在合并中手动修复了重命名/重命名冲突,我们该如何处理?

哇,好多信息啊。 我现在明白问题所在了。 现在的大问题是:有没有一种方法可以隐藏git log中的“重命名”提交,但仍然使--follow选项检查此提交中的重命名操作? - Radon8472
1
请注意,您可以使用 git log --follow 命令获取您关心的提交哈希 ID 列表(格式为 %H),然后 删除 您不想访问的特定提交,最后将这些提交用 git log --no-walk --stdin 命令查看,并将其重定向到包含剩余哈希 ID 的文件中。 - torek
我在我的问题中尝试使用git ref-list命令来完成这个操作,但是git ref没有--follow选项。我认为使用两次git log并在它们之间使用grep听起来像一个不错的解决方法。 - Radon8472

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接