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

Git log (--follow) not working to show histrory beyond renames

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

git-log documentation 表示参数 --follow-M 显示重命名后生成 git 日志。

我尝试了 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 成功理解新路径中的文件和旧路径中的文件已重命名。 谁能告诉我为什么即使我使用 -M--follow 选项,历史记录仍然在重命名提交时中断?

一样,--follow选项必须先于表示选项列表结束的stand-alone-- .

Even it the follow renames seems to work now, when I add --grep="rename" --invert-grep to remove the "rename" commit, I get 0 results

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

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

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

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

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

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

这里的大写字母代表实际的提交哈希 ID,Git 通过它找到提交。每个提交都有一个“backwards-pointing 箭头”——存储的 上一个 提交的哈希 ID——这样如果我们能记住 [=] 的哈希 ID 235=]last 在链中提交,我们可以 Git 通过链向后工作。

一个分支名称只是告诉Git哪个提交是那个分支中的最后提交:

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

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

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

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

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

  • 您给 Git 两个提交,beforeafterold new 或者说 FG。 (你 可以 把新的提交放在左边,旧的放在右边,但是 git log 本身总是把旧的放在左边,较新的放在右边。)

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

  • 这些提交中的某些文件 100% 相同:它们具有相同的名称 相同的内容。 Git 的内部存储系统有 de-duplicated 这些文件,这使得 git diffgit log 很容易确定这些文件 相同,所以它可以在适当的时候跳过它们。

  • 其他文件具有相同的名称但不同的内容。默认情况下,Git 假定如果两个文件具有相同的 名称 — 例如 path/to/file.ext:请注意,嵌入的斜线只是文件名的一部分——它们代表“同一个文件”,即使内容已经改变。因此该文件被修改,从旧的 / left-side 提交到新的 / right-side 提交。如果您要求 --name-status,您将得到 Mmodified 作为该文件名的状态。

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

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

  • 但是如果左边的fileL和右边的fileR应该算是“同一个文件”呢?也就是说,如果我们fileL重命名为fileR会怎样?这就是 Git 的重命名检测器发挥作用的地方。给定这样的 deleted/added 对,maybe content fileLconten 足够接近或完全相同fileR。如果:

    • 您已打开重命名检测器,它实际上会执行此操作 content-checking,并且
    • content-checking 表示“完全相同”(由于 de-duplication 很快就能知道)或“足够相似”(慢得多,但由相同的 rename-detector 启用开关),

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

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

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

  • 在每次提交时,它将 this 提交(较新,在右侧)与其父(较旧,在左侧)进行比较。没有 --follow 它仍然会这样做,但只是看看你 命名的文件 是否被 更改 M状态,来自 git diff --name-status) 或 addeddeleted (A, D).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 不仅会打印提交,还会 更改它正在寻找的名字 。现在,它不是 newpath/my-file.php,而是从父提交向后查找 oldpath/my-file.php.


1--follow 代码本身...不是很好;整个实现需要重新设计,这可能比我正在考虑的更简单的 hack 更好地解决这个问题。

2从技术上讲,其他一些 Git 存储库 可能 有一个 不同的 提交 re-uses 那个哈希 ID,只要你从不将两个提交相互介绍。但在实践中,您找不到。

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

4也可以有T、type-changed,我认为也算。完整的状态字母集是 ABCDMRTUXX 表示 Git 中的错误,U 只能在未完成的合并期间发生,B 只能发生在git diff-B 选项,并且 CR 只能与 --find-copies--find-renames 一起出现(-C-M) 选项已启用。请注意,git diff 可能会根据您的 diff.renames 设置自动启用 --find-renames,但 git log 不会。


--follow

中的错误

这个从 git log 的输出显示中删除一些提交的过程称为 历史简化 the documentation 中有一长段描述了这一点,它以这个相当奇怪的声明开头:

Sometimes you are only interested in parts of the history, for example the commits modifying a particular <path>. But there are two parts of History Simplification, one part is selecting the commits and the other is how to do it, as there are various strategies to simplify the history.

这个奇怪的措辞——“一部分是选择提交,另一部分是如何做”——试图表达的是,启用历史简化后,git log 有时甚至 walk 一些提交。特别是,考虑一个 merge commit,其中两个 strings-of-commits 在一起:

          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 首先沿着重命名分支进行,它可能会查找 错误的名称 当它从另一条腿上下来时。

如果文件在both个分支中重命名,那么它从M重命名回KM 回到 L,或者如果 Git 恰好首先沿着正确的路段 你不 关心另一条腿,一切正常。但这是需要注意的事情。 (这不是使用 --grep 时遇到的问题,否则如果没有 --grep 也会发生。)

认为 您看到的错误是 --grep 可以说是“过早”触发。 --grep 选项通过从 git log 的输出中消除任何具有(--invert-grep)或缺少(--grep 没有 --invert-grep)某些特定文本的提交来工作在其提交 message 中。那么,假设重命名提交——导致 git log --follow 知道使用名称 oldpath/my-file.php 的提交——被你的 --grep 选项 skipped . Git 不会 看到 R 状态,也不会知道将名称从 newpath/my-file.php 更改为 oldpath/my-file.php。所以 git log --follow 会继续寻找 new 路径,你只会得到那些同时满足 grep 条件 修改的提交具有新名称的文件。

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