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
但是无论我尝试什么,历史总是在文件的父文件夹被重命名的提交处结束。
我已经可以确认:
只有文件夹在重命名提交中被重命名,文件内容100%不变,所以git应该简单发现旧路径上的文件和旧路径上的文件新路径完全相同,只是重命名了。
git shot name-status
重命名提交显示 R100 oldpath/my-file.php newpath/my-file.php
(确认文件内容 100% 相同)
历史的“旧一半”和“新一半”似乎是正确的,都包括重命名命令
当我 运行 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
选项,历史记录仍然在重命名提交时中断?
与一样,--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 J
是 last commit 特性分支之一,commit L
是 last 提交另一个。请注意,通过 H
的提交在 两个分支 上(很可能也在主分支或 master 分支上)。
git log
命令简单地完成提交,一次一个,从您选择的任何“最后一次提交”开始。默认的“最后一次提交”是您现在签出的任何分支的尖端。此过程向后工作:Git 从最后一次提交开始并向后工作,一次一个提交。
git diff
的 -M
选项是 --find-renames
的缩写,可在 git diff
中启用重命名检测。 git log
的 --follow
选项对 git log
的作用相同,但也采用 单个文件 的名称来查找。 (将 -M
选项赋予 git log
使其在每个 diff 处使用重命名检测器,但由于它不是在寻找 一个特定的 文件,这只会影响 -p
或 --name-status
输出样式。使用 --follow
、git log
是 寻找那个特定的文件,正如我们将在稍等。)
重命名检测器是这样工作的:
您给 Git 两个提交,before 和 after 或 old 和 new 或者说 F
和 G
。 (你 可以 把新的提交放在左边,旧的放在右边,但是 git log
本身总是把旧的放在左边,较新的放在右边。)
您Git比较了这两个提交中的快照。
这些提交中的某些文件 100% 相同:它们具有相同的名称 和 相同的内容。 Git 的内部存储系统有 de-duplicated 这些文件,这使得 git diff
或 git log
很容易确定这些文件 相同,所以它可以在适当的时候跳过它们。
其他文件具有相同的名称但不同的内容。默认情况下,Git 假定如果两个文件具有相同的 名称 — 例如 path/to/file.ext
:请注意,嵌入的斜线只是文件名的一部分——它们代表“同一个文件”,即使内容已经改变。因此该文件被修改,从旧的 / left-side 提交到新的 / right-side 提交。如果您要求 --name-status
,您将得到 M
、modified 作为该文件名的状态。
有时,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 fileL
与 conten 足够接近或完全相同 个 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
) 或 added 或 deleted (A
, D
).4 但对于 --follow
,它还会查找 R
状态。
如果文件 被 改变了——具有 M
或 A
或 D
状态——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 根本不存储目录。如果没有 --follow
,git log
代码将在通用路径规范上运行。 和 --follow
,它只处理一个文件名。这是算法 Git 在这里使用的限制。
4也可以有T
、type-changed,我认为也算。完整的状态字母集是 ABCDMRTUX
但 X
表示 Git 中的错误,U
只能在未完成的合并期间发生,B
只能发生在git diff
与 -B
选项,并且 C
和 R
只能与 --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
,然后 K
和 L
(按某种顺序),然后 K
之前的所有提交和 L
之前的所有提交回到 C
和 D
,然后在提交时重新加入单个线程 B
并从那里继续向后。
如果我们不打算显示 所有 提交,虽然,也许——只是 可能——在提交 M
,我们可以回到 仅提交 K
或仅提交 L
并完全忽略合并的另一“面”。这将节省 很多 的工作和时间,并避免向您展示不相关的内容。这通常是一件非常好的事情。
然而,对于 --follow
,这通常是一件非常糟糕的事情。这是 --follow
的问题之一:有时 Git 在进行这种简化时会走上“错误的道路”。添加 --full-history
可以避免这种情况,但我们会立即遇到另一个问题。 --follow
选项只有一个文件名。如果我们在提交的两个分支中有一个重命名,但在另一个分支中没有,并且 git log
首先沿着重命名分支进行,它可能会查找 错误的名称 当它从另一条腿上下来时。
如果文件在both个分支中重命名,那么它从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
选项 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 冲突,我们如何处理?
我尝试通过 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
但是无论我尝试什么,历史总是在文件的父文件夹被重命名的提交处结束。
我已经可以确认:
只有文件夹在重命名提交中被重命名,文件内容100%不变,所以git应该简单发现旧路径上的文件和旧路径上的文件新路径完全相同,只是重命名了。
git shot name-status
重命名提交显示R100 oldpath/my-file.php newpath/my-file.php
(确认文件内容 100% 相同)历史的“旧一半”和“新一半”似乎是正确的,都包括重命名命令
当我 运行
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
选项,历史记录仍然在重命名提交时中断?
与--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 J
是 last commit 特性分支之一,commit L
是 last 提交另一个。请注意,通过 H
的提交在 两个分支 上(很可能也在主分支或 master 分支上)。
git log
命令简单地完成提交,一次一个,从您选择的任何“最后一次提交”开始。默认的“最后一次提交”是您现在签出的任何分支的尖端。此过程向后工作:Git 从最后一次提交开始并向后工作,一次一个提交。
git diff
的 -M
选项是 --find-renames
的缩写,可在 git diff
中启用重命名检测。 git log
的 --follow
选项对 git log
的作用相同,但也采用 单个文件 的名称来查找。 (将 -M
选项赋予 git log
使其在每个 diff 处使用重命名检测器,但由于它不是在寻找 一个特定的 文件,这只会影响 -p
或 --name-status
输出样式。使用 --follow
、git log
是 寻找那个特定的文件,正如我们将在稍等。)
重命名检测器是这样工作的:
您给 Git 两个提交,before 和 after 或 old 和 new 或者说
F
和G
。 (你 可以 把新的提交放在左边,旧的放在右边,但是git log
本身总是把旧的放在左边,较新的放在右边。)您Git比较了这两个提交中的快照。
这些提交中的某些文件 100% 相同:它们具有相同的名称 和 相同的内容。 Git 的内部存储系统有 de-duplicated 这些文件,这使得
git diff
或git log
很容易确定这些文件 相同,所以它可以在适当的时候跳过它们。其他文件具有相同的名称但不同的内容。默认情况下,Git 假定如果两个文件具有相同的 名称 — 例如
path/to/file.ext
:请注意,嵌入的斜线只是文件名的一部分——它们代表“同一个文件”,即使内容已经改变。因此该文件被修改,从旧的 / left-side 提交到新的 / right-side 提交。如果您要求--name-status
,您将得到M
、modified 作为该文件名的状态。有时,left-side 提交有一个名为
fileL
的文件,而 right-side 提交根本没有该文件。该文件 已删除 ,显然,在从旧(左)到新(右)的变化中。使用--name-status
您将获得D
的状态。有时,right-side 提交有一个名为
fileR
的文件,而 left-side 提交则没有。该文件显然是 新添加的 ,使用--name-status
您将获得A
的状态。但是如果左边的
fileL
和右边的fileR
应该算是“同一个文件”呢?也就是说,如果我们将fileL
重命名为fileR
会怎样?这就是 Git 的重命名检测器发挥作用的地方。给定这样的 deleted/added 对,maybe contentfileL
与 conten 足够接近或完全相同 个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
) 或 added 或 deleted (A
,D
).4 但对于--follow
,它还会查找R
状态。如果文件 被 改变了——具有
M
或A
或D
状态——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 根本不存储目录。如果没有 --follow
,git log
代码将在通用路径规范上运行。 和 --follow
,它只处理一个文件名。这是算法 Git 在这里使用的限制。
4也可以有T
、type-changed,我认为也算。完整的状态字母集是 ABCDMRTUX
但 X
表示 Git 中的错误,U
只能在未完成的合并期间发生,B
只能发生在git diff
与 -B
选项,并且 C
和 R
只能与 --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
,然后 K
和 L
(按某种顺序),然后 K
之前的所有提交和 L
之前的所有提交回到 C
和 D
,然后在提交时重新加入单个线程 B
并从那里继续向后。
如果我们不打算显示 所有 提交,虽然,也许——只是 可能——在提交 M
,我们可以回到 仅提交 K
或仅提交 L
并完全忽略合并的另一“面”。这将节省 很多 的工作和时间,并避免向您展示不相关的内容。这通常是一件非常好的事情。
然而,对于 --follow
,这通常是一件非常糟糕的事情。这是 --follow
的问题之一:有时 Git 在进行这种简化时会走上“错误的道路”。添加 --full-history
可以避免这种情况,但我们会立即遇到另一个问题。 --follow
选项只有一个文件名。如果我们在提交的两个分支中有一个重命名,但在另一个分支中没有,并且 git log
首先沿着重命名分支进行,它可能会查找 错误的名称 当它从另一条腿上下来时。
如果文件在both个分支中重命名,那么它从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
选项 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 冲突,我们如何处理?