TL;DR
Git只是没有显示历史记录,但历史记录并没有消失。在Git中,历史记录是一组提交。运行git log dir/sub/file.ext
命令时,Git会合成一个(临时)文件历史记录,从真实的历史记录中提取一些子历史记录。默认情况下,它通过“历史简化”来删除更多提交。
每个提交都有一个唯一的哈希ID,包含文件快照和父提交的列表。当处理普通的非合并提交时,可以从分支名开始向后查看历史记录。
... <--1234567... <--master
如果提交1234567
是master
的顶端,git log
可以向您展示提交1234567
……而提交1234567
内部包含着位于其之前的提交的哈希ID。
如果我们将真实哈希ID替换为单个字母,以便事情更简单,我们会得到类似这样的内容:
A <-B <-C <-D <-E <-F <-G <--master
将提交点G
回溯到提交点F
,然后回溯到E
,以此类推,直到我们到达第一个提交点A
。这个提交点不指向任何地方——它不能,因为它是第一个提交点;它不能有父节点——因此这就是历史的起点(或终点)。Git将A
称为根提交:没有父节点的提交。
展示线性历史很容易,从时间的末尾开始,一直延伸到开头。Git只需逐个选择每个提交点并显示即可。这就是:
git log master
做法:从一个名为master
的提交开始,显示该提交,然后显示该提交的一个父提交,再显示前一个提交,以此类推。
当您让Git显示一个提交时,通常情况下您可以让Git将其显示为一个补丁而不是快照。例如,git log --patch
就会这样做。要将提交显示为补丁,Git只需先查看该提交的父提交的树,然后查看该提交的树,并比较两者。由于两者都是快照,从父快照到子快照所做的任何更改,必须是制作子提交的人实际执行的操作。
非线性历史更难理解
现在我们知道了Git是如何向后工作的,让我们来看看更复杂的历史记录,包括包含实际合并提交的历史记录。(不要因为git merge
并不总是进行合并而迷失方向!)
一个合并提交只是一个具有至少两个父提交的提交。在大多数情况下,您不会看到具有三个或更多父提交的提交——Git将它们称为章鱼合并,它们不会做任何普通合并做不到的事情,因此章鱼合并主要是为了展示您的Git技巧。 :-)
通常我们通过执行git checkout somebranch; git merge otherbranch
来获得合并提交,并且我们可以像这样绘制结果提交链:
...--E--F--G------M <-- master
\ /
H--I--J <-- feature
现在假设您运行git log master
(注意:没有--patch
选项)。Git应该首先显示提交M
。但是Git接下来会显示哪个提交?J
还是G
?如果它显示了其中一个,那么接下来应该显示哪一个?
Git对此问题有一个通用的解决方案:当它向您显示合并提交时,它可以将提交的两个父级添加到“尚未显示的提交”队列中。当它向您显示一个普通非合并提交时,它将单个父级添加到相同的队列中。然后它可以循环遍历队列,逐个向您显示提交并将它们的父级添加到队列中。
当历史是线性的时候,队列每次只有一个提交:该提交被移除并显示,队列现在有一个父级,您将看到该父级。
当历史有一个合并时,队列以一个提交开始,Git从队列中弹出提交并显示它,并将两个父级放入队列中。然后Git选择其中之一并向您显示G
或J
,并将F
或I
放入队列中。队列仍然有两个提交。Git从其中弹出一个并显示该提交,另一个提交被加入队列中。
最终,当Git尝试将F
放入队列时,而F
已经在队列中时,Git会避免重复添加它,因此队列深度再次减少到一个提交,在这种情况下,它会显示F
,E
,D
等。(这里的细节有点复杂:队列是特定的优先队列,其优先级由附加的git log
排序参数确定,因此可以通过不同的方式进行操作。)
您可以使用git log --graph
查看连接
如果您在git log
命令中添加--graph
,Git将绘制一个粗糙的ASCII图形,将子提交与其父提交连接起来。这非常有助于告诉您,您正在查看的提交历史实际上并不是线性的,尽管git log
每次仅显示一个提交(因为必须如此)。
显示合并提交
我之前提到,使用-p
或--patch
选项,git log
将通过比较父提交的快照/树与子提交的快照/树来显示提交所做的更改。但对于合并提交,有两个(甚至更多)父级:无法显示出父级与子提交的比较,因为至少有两个父级。
默认情况下,git log
会放弃显示补丁。其他命令会执行更复杂的操作,你也可以说服git log
执行这些操作,但让我们注意,git log
的默认操作是在此处放弃。
历史简化(这是一个指向git log
文档的可点击链接)
当你运行git log file.ext
时,Git将故意跳过任何未触及file.ext
的非合并提交的差异(通过比较父提交和子提交获得)。这很自然:如果你有像这样的链:
A
假如你在提交 A
和 E
时修改(或首次创建)了 file.ext
文件,你想仅查看这两个提交。Git 可以通过计算 D
和 E
的补丁,并查看是否有 file.ext
的更改(因此应该“显示”E
),然后移动到 D
。比较 C
和 D
显示文件没有变化,所以 Git 不会显示 D
,但它将把 C
放入优先队列并继续访问 C
。同样地,C
对该文件没有任何更改,因此 Git 最终转移到 B
,它也没有更改,Git 移动到 A
。为了比较,根提交中的所有文件始终是新的 - 这是任何根提交的规则,所有文件都被添加 - 因此 Git 也会将 A
显示给您。
然而,正如我们所见,git log
默认情况下不喜欢计算合并的补丁。这太难了!因此,git log
通常不会显示此处的合并。但是,它确实试图简化提交图的任何部分。正如文档所述, 默认模式:
如果最终结果相同,则修剪一些旁支...
如果提交是一个合并,并且 [文件与] 其中一个父项相同,请仅跟随该父项。...否则,跟随所有父项。
因此,在我们的图中像 M
这样的合并提交上,Git 将进行快速检查:在 M
中的 file.ext
是否与 G
中的相同?如果是,请将 G
添加到队列中。如果不是,在 M
中是否与 J
中的相同?如果是,请将 J
添加到队列中。否则 - 即,在 M
中,file.ext
与 G
和 J
都不相同 - 将 G
和 J
都添加到队列中。
有其他的历史简化模式,可以使用各种标志进行选择。由于本答案已经太长了,因此我将它们留给文档(参见上面的链接)。
结论
您不能从 git log -- path
显示的内容中推断出太多信息,因为 Git 执行了历史简化。如果您想看到 所有 的内容,请考虑运行 git log --full-history -m -p -- path
。选项 -m
用于拆分每个合并以供 git diff
使用(与 -p
选项一起使用),而 --full-history
指示 Git 始终遵循所有父提交。