Git日志(--follow)无法显示重命名之外的历史记录

Posted

技术标签:

【中文标题】Git日志(--follow)无法显示重命名之外的历史记录【英文标题】:Git log (--follow) not working to show histrory beyond renames 【发布时间】:2021-02-03 20:10:57 【问题描述】:

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

git-log documentation 表示参数 --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

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

我已经可以确认:

rename commit 中只有文件夹被重命名,文件内容100%不变,所以git应该简单地发现旧路径上的文件和新路径上的文件是相同的,然后重命名了。

git shot name-status 用于重命名提交显示 R100 oldpath/my-file.php newpath/my-file.php(确认文件内容 100% 相同)

历史的“旧一半”和“新一半”似乎是正确的,都包括rename-commt

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

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

所以看起来我的 git 成功理解了新路径中的文件和旧路径中的文件已重命名。 谁能告诉我为什么即使我使用 -M--follow 选项,重命名提交的历史仍然会中断?

【问题讨论】:

如果:git log -M --oneline --all --follow -- newpath/my-file.php 怎么办? -- 标记选项结束。 我测试了我移动文件夹中一些文件的更改命令,将--follow移动到--后面似乎是解决方案 即使它下面的重命名现在似乎工作了,当我添加 --grep="rename" --invert-grep 以删除“重命名”提交时,我得到 0 个结果 嗯,这是另一个问题。但也许解决方案是一样的:您不能在-- 部分之后放置任何选项。 【参考方案1】:

作为noted in comments,--follow 选项必须指示选项列表结束的独立-- 之前。

即使跟随重命名现在似乎工作了,当我添加 --grep="rename" --invert-grep 以删除“重命名”提交时,我得到 0 个结果

这是有道理的(但这是一个错误),1 因为 方式 --follow 有效。这里的问题是 Git 根本没有任何类型的文件历史。 Git 所拥有的只是存储库中的一组提交。 提交是历史记录:

每个提交都通过其大而丑陋的哈希 ID 进行编号,该 ID 对于该特定提交是唯一的。 any Git 存储库2 中没有其他提交具有该哈希 ID。

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

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

所以这些数字串在一起,向后提交:

... <-F <-G <-H

这里的大写字母代表实际的提交哈希 ID,Git 通过它找到提交。每个提交都有一个“向后指向的箭头”——存储的 previous 提交的哈希 ID——所以如果我们能记住 last 的哈希 ID > 在链中提交,我们可以让 Git 在链中向后工作。

branch name 只是告诉 Git 哪个提交是该分支中的 last 提交:

             I--J   <-- feature1
            /
...--F--G--H
            \
             K--L   <-- feature2

这里,commit Jlast 提交功能分支之一,commit Llast 另一个提交。请注意,通过H 的提交都在两个分支上(很可能也在主分支或主分支上)。

git log 命令简单地处理提交,一次一个,从您选择的任何“最后一次提交”开始。默认的“最后一次提交”是您现在签出的任何分支的尖端。这个过程是逆向工作的:Git 从最后一次提交开始,然后逆向工作,一次提交一个。

git diff-M 选项是--find-renames 的缩写,可在git diff 中启用重命名检测。 --follow 选项对git log 的作用与git log 相同,但也采用单个文件的名称 来查找。 (将-M 选项赋予git log 使其在每个差异中使用重命名检测器,但由于它不是在寻找一个特定的 文件,这只会影响-p--name-status 样式输出。--followgit log 正在寻找那个特定的文件,我们稍后会看到。)

重命名检测器是这样工作的:

你给 Git 两次提交,beforeafteroldnew 或者说,@987654346 @ 和 G。 (您可以将新提交放在左侧,将旧提交放在右侧,但git log 本身总是将旧提交放在左侧,新提交放在右侧。)

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

这些提交中的某些文件是 100% 相同的:它们具有相同的名称​​和相同的内容。 Git 的内部存储系统已经对这些文件进行了重复数据删除,这使得git diffgit log 很容易确定这些文件相同,因此它可以在适当的时候直接跳过它们。

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

有时,左侧提交有一个名为fileL 的文件,而右侧提交根本没有该文件。显然,在从旧(左)到新(右)的变化中,该文件被删除。使用--name-status,您将获得D 的状态。

有时,右侧提交有一个名为 fileR 的文件,而左侧提交则没有。显然,该文件是新添加的,使用--name-status,您将获得A 的状态。

但是如果左边的fileL 和右边的fileR 应该被认为是“同一个文件”呢?也就是说,如果我们重命名 fileLfileR 会怎样?这就是 Git 的重命名检测器的用武之地。鉴于这样的删除/添加对,可能 fileLcontent 足够接近或完全相同内容来自fileR。如果:

您已打开重命名检测器,它实际上会执行此内容检查,并且 内容检查显示“完全相同”(由于重复数据删除,很快就知道)或“足够相似”(慢得多,但由相同的重命名检测器开关启用),

然后然后-Git 将声明fileL重命名 成为fileR--name-status 输出将包括 R相似性索引 值和 两个 文件名,而不是在左侧和右侧都匹配的单个文件名提交。

现在您知道重命名检测器的工作原理——并且必须打开——你可以看到--follow 的工作原理。请记住,使用git log,您可以给它一个文件名,并告诉它显示提交不要修改该特定文件。3 结果是您只看到确实修改该文件的提交:git log 访问的所有提交集合的子集。所以假设你运行git log --follow -- newpath/my-file.php

git log 遍历历史,一次提交一次,像往常一样倒退。

在每次提交时,它会将 this 提交(较新,在右侧)与其父提交(较旧,在左侧)进行比较。如果没有--follow,它仍然会这样做,但只需查看您命名的文件是否更改M 状态,来自git diff --name-status)或已添加已删除AD)。4但对于--follow,它还会查找R 状态。

如果文件改变了——有MAD状态——git log打印出这个提交,但如果没有,它只是抑制打印输出。使用--follow,我们添加R 状态,如果发生这种情况,添加两个文件名。如果状态R,那么git log 之前一直在寻找newpath/my-file.php。但现在它知道,在提交 parent 时,该文件被称为 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从技术上讲,只要您从未引入两人互相承诺。但实际上,您不会找到。

3--follow 选项只能跟随一个文件名。如果没有--follow,您可以给git log 提供多个名称,或者一个“目录”的名称,即使Git 根本不存储目录。如果没有--followgit log 代码将在通用路径规范上运行。 --follow,它只处理一个文件名。这是 Git 在这里使用的算法所施加的限制。

4它也可以有T,类型改变了,我认为这很重要。完整的状态字母集是ABCDMRTUX,但X 表示Git 中的一个错误,U 只能在未完成的合并期间出现,B 只能与git diff-B 选项一起出现,@ 987654412@ 和 R 只能在启用 --find-copies--find-renames-C-M)选项的情况下发生。请注意,git diff 可能会根据您的diff.renames 设置自动启用--find-renames,但git log 不会。


--follow 中的错误

git log 的输出显示中删除一些提交的过程称为历史简化。 the documentation 中有一个很长的部分对此进行了描述,并以这个相当奇怪的声明开头:

有时您只对部分历史感兴趣,例如 修改特定 的提交。但是有两个部分 历史简化,一部分是选择提交,另一部分是 是如何做到这一点,因为有各种策略来简化 历史。

这个奇怪的措辞——“一部分是选择提交,另一部分是如何去做”——试图理解的是,启用历史简化后,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重命名回K并且M重命名回L,或者如果 Git 碰巧一开始就走在正确的腿上并且你不关心另一条腿,一切正常。但这是需要注意的。 (这不是--grep 给您带来的问题,或者如果没有--grep 就会发生。)

认为您看到的错误是--grep 正在“过早”启动,实际上。 --grep 选项的工作原理是从 git log 的输出中消除在其提交中具有 (--invert-grep) 或缺少 (--grep 而没有 --invert-grep) 某些特定文本的提交 message。然后,假设重命名提交(导致git log --follow 知道使用名称oldpath/my-file.php 的提交)被您的--grep 选项跳过。 Git 不会看到 R 状态,也不知道将名称从 newpath/my-file.php 更改为 oldpath/my-file.php。所以git log --follow 将继续寻找 new 路径,并且您只会得到那些同时满足 grep 条件的提交并且使用新名称修改文件。

这个错误可以通过让git log --follow 运行差异引擎来修复,即使它会因为其他原因跳过提交。但更一般地说,--follow 需要完全重写:它有一堆奇怪的特殊情况代码通过差异引擎线程化,只是为了使这个案例工作。它需要处理多个路径名和/或路径规范,并使用--reverse 和其他选项。它需要一种将新旧名称堆叠到提交路径上的方法,以便使用--full-history,沿着合并的两条腿,它知道要寻找哪条路径。请注意,这还有其他含义:如果在合并的两条腿上都有不同的重命名怎么办?如果有人在合并中手动修复了重命名/重命名冲突,我们该如何处理用那个?

【讨论】:

哇,一大堆信息。我现在明白什么问题了。现在最大的问题是:有没有办法从 git 日志中隐藏“重命名”提交,但让 --follow 选项仍然检查此提交中的重命名操作? 没有。但是请注意,您可以单独获取您关心的提交哈希 ID 列表(使用 git log --follow,格式为 %H),删除一个您不想访问的特定提交,然后查看那些带有git log --no-walk --stdin的提交重定向到包含剩余哈希ID的文件。 我尝试使用 git ref-list 命令来执行此操作,但 git ref 没有 --follow 选项。我认为使用 git log 两次和 grep 之间听起来像是一个很好的工作方法。

以上是关于Git日志(--follow)无法显示重命名之外的历史记录的主要内容,如果未能解决你的问题,请参考以下文章

带有重命名检测的 git show 文件(相当于 git log --follow)

在 Git 中重命名文件后没有预期的日志 [重复]

在 git 文件重命名后使 Xcode 5 跟随历史

git gui app 显示检测到的重命名

Windows上的Git:重命名文件后无法切换分支(仅更改大小写)

git status 显示重命名但不正确