git 日志没有 return 正确的文件历史记录

git log does not return the history of a file correctly

我对 git 日志命令有一个奇怪的问题。虽然这个命令:

git log --pretty=format: --name-only --diff-filter=A

returns .xyz.yml 文件在列表中,但是当我尝试 运行 这个命令时:

git log --pretty="%ad" --diff-filter=A -- .xyz.yml

检索此文件添加到此存储库的时间,它 returns 为空。

有什么解决办法吗?

如有任何线索,我将不胜感激。

编辑:

当我尝试获取完整的历史记录时:

git log --full-history -- .xyz.yml

输出显示了简短且不完整的历史记录

commit b26d833b9da805d5d58c429a4af2d1a5c5b0bad9
Author: author name
Date:   Mon Dec 19 14:07:17 2016 -0500

Code config (#606)

* Create .xyz.yml

Created a Code config that uses your setup (also, enabled our Duplication engine).

* Update .xyz.yml

* Update .xyz.yml

* Update .xyz.yml

* Update .xyz.yml

* Update .xyz.yml

* Update .xyz.yml

* Update .xyz.yml

即使文件不再出现在头部,历史记录也没有显示任何删除...

我还查看了 GitHub 用户界面中的提交历史,我在那里看到了一个完全不同的世界:

首次提交日期甚至与我在 --full-history 中找到的日期不同。

(注意:如果您还不知道提交如何将 每个 文件和 link 的完整快照通过它们的元数据存储在一起 parent 信息,参见,例如,.)

编辑前后的问题是不同的,但是两个问题在发生的事情上是相关的。 git log 命令可以执行一些 Git 调用 历史简化 。在 the git log documentation 中搜索此 two-word 短语,您将找到以 strangely-worded 段落开头的部分:

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.

在我们解决这里的奇怪措辞之前,请注意只有在您要求 时才会简化历史记录。索取方式有:

  • 要使用本节中描述的显式选项之一,and/or
  • 列出一些path参数,如git log -- .xyz.yml:这里的.xyz.yml是一个路径。 (-- 在某些情况下是可选的,并将其余参数标记为路径。如果命名路径存在于当前提交中 与其他 git log 选项,-- 不是必需的。不过,养成始终使用它的习惯是个好主意,这样您就不必弄清楚这个特定 git log 是否需要它调用。)

因为在你的麻烦案例中,你确实使用了-- .xyz.yml,你确实要求历史简化,即使你没有意识到你要求它。这就是为什么我添加 ;您的回复说使用 --full-history 解决了问题证明 default-mode 简化实际上是问题所在。

你接着问:

full history returns introduction commit. Whats the reason?

答案在于the documentation调用Default mode:

Simplifies the history to the simplest history explaining the final state of the tree. Simplest because it prunes some side branches if the end result is the same (i.e. merging branches with the same content)

不过这还是比较费解。开头的段落讨论了 select 提交 ,然后是 如何做 。我认为这里缺少的是文档从未讨论过 git log 如何真正 works.

我们需要知道的是——文档没有说明——git log 的工作方式是扫描提交队列。这个队列是一个优先级队列,即“更高优先级”的提交浮到队列的前面并首先被检查;已经在队列中的“低优先级”提交被这个更高优先级的提交推到队列的后面。因此,git log 命令一次只处理 一个提交 出这个队列。

队列本身最初是从您在命令行上指定的任何提交加载的。例如,您可以 运行:

git log branch1 branch2 branch3

这使用 git rev-parse 将每个 branch1branch2branch3 转换为提交哈希 ID。生成的三个提交哈希 ID(假设我们得到三个不同的哈希 ID)被加载到队列中。如果我们得到重复项,此时队列中有两个甚至只有一个提交哈希 ID。例如,如果名称 branch2branch3 select 是相同的提交,而 branch1 select 是不同的提交,队列中现在只有两个提交.

(如果您不选择任何起始提交,git log 将使用 HEAD 作为起始点提交。它的姊妹命令 git rev-list 没有这个特殊的功能,所以任何时候你使用 git rev-list 而不是 git log,确保你给它一个明确的起点。)

git log 代码现在进入其主循环。这个循环:

  • 从队列中获取 top-priority 提交;
  • 根据一些git log参数决定是否打印它;和
  • 根据其他 git log 参数决定是否将其 parent 提交放入队列

当我们要求 git log 说出像 .xyz.yml 这样的文件时,关于是否 打印 提交的决定必须比较提交的快照到其 parent 的快照。我们现在想在 the documentation 中向下扫描到此部分:

A more detailed explanation follows.

Suppose you specified foo as the <paths>. We shall call commits that modify foo !TREESAME, and the rest TREESAME. (In a diff filtered for foo, they look different and equal, respectively.) [snip]

(在阅读此答案的其余部分之前或之后阅读其余部分并完成他们的示例。)

Git 在内部真正要做的是为这次提交拍摄快照——无论它是什么——并删除所有文件 除了 你列出的文件.在这种情况下,您列出的一个文件是 .xyz.yml;在他们的例子中,一个文件被命名为 foo 。但是你可以在这里给出一个目录路径,Git 将删除除该目录或多个路径中的文件之外的所有文件,并且 Git 也会删除除这些路径之外的所有文件。这一切都适用于 so-called TREESAME 测试。当我们查看一个文件时,最容易理解,因为要么提交有文件的某些特定版本,要么提交完全缺少文件:这是仅有的两种可能性。因此,如果两个提交都 缺少 文件,或者如果 b拥有文件并使用相同版本的文件。

如果我们有一个正常的、每天的 non-merge 提交和一个 parent 提交,这一切都非常简单。考虑以下简单的提交链:

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

在这里,提交 H 有一些快照。 H 的 parent,提交 G,有一些快照。 G 的 parent F 当然也有一些快照,等等。可能每个快照都是不同的,但如果我们将它们剥离到只有一个感兴趣的文件——文件 foo 或文件 .xyz.yml——提交 GH 可能具有相同的文件。 GH 是相同的。但是,提交 F 中的副本可能不同:FG 不是 TREESAME。

这意味着 Git 不会提及提交 H。它没有对文件进行 更改 。 Git 提及提交G:与[=468=相比,它对文件有更改 ] F这是第一次使用 TREESAME 概念:通过询问 Git 关于特定文件,它只打印不是 TREESAME 的提交到他们的 parent 提交:至少有一个文件我们问的是,改了。

合并提交很棘手

这只处理简单的普通提交,如 FGH合并提交怎么样? 我们的分支可能有这些提交:

       I--J
      /    \
...--H      M--N--...
      \    /
       K--L

当 Git 对 (M, N) 对进行 TREESAME 测试时,该部分很简单。虽然 M 是一个合并提交,但它有一个快照,就像任何提交一样。因此,我们将 MN 中的快照缩减为感兴趣的文件,并判断结果是否为 TREESAME。如果是这样,我们 不打印 N,然后继续 M;如果不是,我们 打印 N,然后继续 M.

现在我们必须确定提交 M 是否与其 parent 相同。不过等等,M 没有 a parent。 M 两个 parent、JL。我们应该比较哪一个?

Git 的答案是比较 所有 :尝试 TREESAME(J, M) 一棵树(LM)。 Git 现在知道 M 是否是 all parents 或 some parents,或 no parents。如果 Many parent 的 TREESAME,它是 not 打印的;否则,它 打印出来。 现在真正的麻烦来了。

在合并提交时,git log 可以将部分或全部 parent 放入队列

已打印或未打印提交 Mgit log 现在必须决定:

  • 我是否将提交 J 放入队列?
  • 我是否将提交 L 放入队列?

进行历史简化时,Git会将两个parent都放入队列。 (好吧,如果你使用 --first-parent 选项就不会。但是,因为你没有,我们将完全忽略该选项。)但是在进行历史简化时,default选项是:

  • 对于 --full-history,所有 parent 都进入队列。
  • 如果没有 --full-history,选择一个 parent TREESAME(从所有可能的 TREESAME parent 中随机选择)。如果没有 parent 是 TREESAME,则选择所有 parent。将这些放入队列中。

(请注意,某些合并提交可能有 3 个或更多 parent;相同的规则适用于这些 many-parent 合并提交。这里我们只有一个 two-parent 合并,所以短语“all parents”的意思是“both parents”。)

现在,假设我们的 file-of-interest 是在提交 KL 中引入的。它在提交 HIJ 中不存在,重要的是,它在 M 中也不存在:合并 省略了文件 。由于提交 M,在剥离除我们的 file-of-interest 之外的所有内容后, TREESAME 在相同的 stripping-away 之后提交 J,Git 跟随 M 回到 J 完全忽略提交 L。 (注意:可能是 L 中也没有我们的文件,但无论出于何种原因,Git 选择跟随提交 J 而不是 L 作为其单个 TREESAME 提交,同时进行历史简化。)

在这种情况下,历史简化代码 完全停止 git log 查看提交的底行 。队列从不 包含 提交 K,文件首次引入的位置。查找文件的扫描永远找不到它,因为 Git 永远不会细读文件 的历史记录。

历史简化的目标

这种简化背后的想法是解释为什么您拥有现在拥有的文件。通过 not 跟随合并提交的历史 M 返回到 comit L,在我们的示例中,我们从未找到文件 .xyz.yml。但这就是我们 想要的 ,因为文件 .xyz.yml 不在 当前提交 N 中,或者它所在的任何地方我们开始了。我们已要求 Git 解释 那里的文件。文件 .xyz.yml 不存在,因此关于它存在的原因的解释是 从未 在 history-of-interest 中:历史解释了为什么它仍然存在没有。

你的目标不同

你的目标当然是弄清楚它是从哪里引入的它是从哪里丢失的。事实是它在合并时丢失了,当时有人决定:我们的合并结果中不需要这个愚蠢的 .xyz.yml!让我们把它放在外面! 也就是说,它 在某个提交 L 中,而不是在它的直接后继合并 M 中。

我知道这是从你最终的 git log 输出,当你去掉 --diff-filter 选项时:

git log --full-history -- .xyz.yml

我们看到一个添加文件的提交,以及一些修改它的提交,但我们没有看到任何删除它的提交。我们看不到此提交的原因是因为我们的合并 M TREESAME 到至少其 parent 之一:J,在我们的例子。所以 merge commit M 根本就没有打印出来。

如果 打印出来,我们仍然有一个潜在的问题,因为打印合并的方式有点古怪。所有提交都打印了它们的哈希 ID 和日志消息。如果您请求 --name-status--patch,您可能 得到某种 git diff 的结果。对于普通提交,这是与(单个)parent 提交的差异。但是,对于 git log,有一个问题:

  • 如果您不要求 -c--cc-mgit log 将完全跳过打印差异;和
  • 如果你要求-c--cc,你会得到一个组合差异

合并差异省略了一些文件。特别是,他们省略了任何文件,其中至少有一个 parent 和合并具有相同版本的文件——或者在这种情况下,两者都缺少文件。因此,合并差异不会提及 LM 之间的文件 已删除 。只有 -m 样式差异会在此处提及 删除

-m 选项对合并提交进行“虚拟拆分”。如果 merge X 有 parents P1, P2, P3, ..., Pn,你得到 n 差异:P1-vs-XP2-vs-XP3-vs-X 和依此类推 Pn-vs-X。那么,对于我们的特殊情况,我们将得到两个差异:JM 以及 LM。对于 .xyz.ymlJ-vs-M diff 不会显示任何内容,但 L-vs-M 会显示删除。

(请注意,-m 还修改了 git log 决定是否打印合并的方式:现在它已被拆分,如果它不是 TREESAME,则会打印到 至少一个 parent。这也很重要,在这里。)

底线

如果您试图找出某些文件在何处被 删除,您可能需要 git 日志 --full-history --diff-filter=D -m -- <em>路径</em>。这会强制 git log 遍历每个合并的所有 parent 并检查 合并本身 是否是文件不存在于从你开始的地方提交。