分支合并后的Git历史记录

6

我对git合并后如何存储历史记录有些困惑。

我已经成功将分支A合并到分支B中。现在,当我进入分支B中的一个文件时,该文件是合并的一部分,我可以看到所有分支A的历史记录,但我看不到任何分支B的历史记录。我的分支B的历史记录去哪了?

我是通过git merge <branch>方式进行合并的,因此,在这种情况下,我在分支B中使用git merge A


例如,在分支A中,我有以下提交:aaaaaa,对应不同的文件。

在分支B中,我有以下提交:bbbbbb,对应不同的文件。

现在,当我将分支A合并到分支B中时,我在分支B git log中只看到了aaaaaa的历史记录。我看不到我的b的历史记录。

实际上,我希望我的合并是线性的,当我将A合并到B时,我希望历史记录中有所有分支B的历史记录,并在其之上是刚刚发生的合并,类似于SVN的方式。

我的当前git log历史记录非常混乱。


2
请问您能否分享一些之前在B中的提交记录,现在也在A中,并且B现在显示的提交记录。 - Vinay Prajapati
2个回答

12

TL;DR

Git只是没有显示历史记录,但历史记录并没有消失。在Git中,历史记录是一组提交。运行git log dir/sub/file.ext命令时,Git会合成一个(临时)文件历史记录,从真实的历史记录中提取一些子历史记录。默认情况下,它通过“历史简化”来删除更多提交。

每个提交都有一个唯一的哈希ID,包含文件快照和父提交的列表。当处理普通的非合并提交时,可以从分支名开始向后查看历史记录。

... <--1234567...   <--master

如果提交1234567master的顶端,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选择其中之一并向您显示GJ,并将FI放入队列中。队列仍然有两个提交。Git从其中弹出一个并显示该提交,另一个提交被加入队列中。

最终,当Git尝试将F放入队列时,而F已经在队列中时,Git会避免重复添加它,因此队列深度再次减少到一个提交,在这种情况下,它会显示FED等。(这里的细节有点复杂:队列是特定的优先队列,其优先级由附加的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--B--C--D--E   <-- master

假如你在提交 AE 时修改(或首次创建)了 file.ext 文件,你想仅查看这两个提交。Git 可以通过计算 DE 的补丁,并查看是否有 file.ext 的更改(因此应该“显示”E),然后移动到 D。比较 CD 显示文件没有变化,所以 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.extGJ 都不相同 - 将 GJ 都添加到队列中。

有其他的历史简化模式,可以使用各种标志进行选择。由于本答案已经太长了,因此我将它们留给文档(参见上面的链接)。

结论

您不能从 git log -- path 显示的内容中推断出太多信息,因为 Git 执行了历史简化。如果您想看到 所有 的内容,请考虑运行 git log --full-history -m -p -- path。选项 -m 用于拆分每个合并以供 git diff 使用(与 -p 选项一起使用),而 --full-history 指示 Git 始终遵循所有父提交。


谢谢!对于线性历史记录,使用git rebase而不是git merge可以吗?我真的不喜欢git merge显示的历史记录。它很令人困惑。 - Robin
1
为了在其他人添加提交时保持自己的历史线性,您确实可以使用基于变基的工作流程。有许多页面介绍如何执行此操作,何时执行此操作,何时执行此操作,为什么它很好,为什么它很糟糕,以及为什么没有人应该这样做等等:换句话说,这有点是一种观点问题。阅读有关变基内部工作原理的信息,并自行决定! :-) - torek

2

所有的内容都应该还在。尝试使用 git log --graph 查看它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接