这里的一切看起来都很正常。
我们可以从这些观察开始:
在大多数情况下,Git 存储提交。即commit是Git存储的基本单位。(可以将提交分成几个部分,Git 有时会这样做,但大多数情况下我们一次处理整个提交。)
在创建每个提交时,它都会获得一个唯一的哈希 ID。这个哈希 ID 永远为这次提交保留,不仅在这个Git 存储库中,而且在任何地方的每个Git 存储库中。从某种意义上说,它甚至在你提交之前就被保留了,除了提交哈希 ID 取决于你提交时的时间,直到第二个。
(这些哈希 ID 非常大且笨拙。它们必须是,像这样独一无二。没有人能正确地理解它们,但没关系:毕竟我们有一台计算机。)
每个提交都包含所有文件的快照。这是提交中的主要数据,但每个提交还包含一些元数据:关于提交本身的一些信息。元数据包括提交者的姓名和电子邮件地址 - 作为两个条目,作者和提交者- 以及一些日期和时间戳,以及您或任何人记录提交原因的日志消息。
但是,此元数据中的一个关键项是每个提交都存储其直接前一个提交或多个提交的实际哈希 ID。也就是说,每个提交都有一定数量的父提交——通常只有一个——并且子提交存储其父或父的原始哈希 ID。
每当我们在某处存储了提交的哈希 ID 时,我们就说持有该哈希 ID 的任何东西都指向该提交。因此,如果您在某处写了一些哈希 ID H——例如在纸上或白板上——我们可能会说 this指向commit H。由于每个提交都存储其父级的哈希 ID,因此每个提交都指向其父级。我们可以这样画:
... <-F <-G <-H
where His some commit with some hash ID H,它指向它的 parent G,它又指向它的 parent F,依此类推。
如果我们将某个长链提交的最后一个哈希 ID 记录在一个分支名称中,例如master,这足以让 Git 找到所有这些提交。该名称指向最后一次提交:
...--F--G--H <-- master
从那里,Git 可以向后工作:从 开始H,它找到G,然后F,依此类推。这一直重复,直到 Git 找到第一个提交,这不会进一步指向,因为它不能。
要进行新的提交,我们首先将提交提取H到工作区。(所有提交都是 100% 只读的,所以我们无法更改H,但我们可以更改我们复制到工作区的内容。)然后我们像往常一样编辑文件,使用git add将它们复制到索引- 另一个关键项目,但我们赢了此处不详述——并用于git commit进行新的提交。Git将我们所有的文件打包到一个新的快照中,添加元数据——例如我们的姓名和电子邮件地址以及当前日期和时间——并将新提交的父级设置为 commit H。这个新的提交获得了它的新 ID,这对我们来说太大了,我们无法管理,但我们将称之为I。因此,新提交I指向现有提交H.
最后,git commit 将新提交的哈希 ID 写入当前分支名称。如果当前分支名称是master,Git 会将I哈希 ID 写入master,因此master现在指向I. 由于I点回到H,这就是我们得到的:
...--F--G--H--I <-- master
该git log命令从最后开始并向后工作
一个常规的git log——<em>not git reflog!——从某个结束点提交哈希 ID 开始,例如通过查看 name 内部master。每当git log查看分支名称时,根据定义,这就是分支中的最后一次提交。1 然后git log向我们展示这个 commit——commit I,在这种情况下——现在git log简单地移动到提交的父级。所以你看到的下一个提交是上一个提交。
换句话说,日志会自动从头到尾反向工作。这是 Git 的一个主题:它所有的内部箭头都指向后方。Git总是必须从头开始并向后工作。所以git log默认显示最新的提交,因为默认情况下,它从您当前的分支名称开始,这是该分支上的最新提交。
请注意,如果您使用git checkout— 或新的 Git 2.23+ git switch— 切换到不同的分支名称,git log将从该其他分支名称开始,而不是从master. 或者,如果您使用git checkout或git switch切换到分离 HEAD模式,其中特殊名称HEAD直接指向提交,git log将默认从该历史提交开始。
1分支名称的定义是它保存了最后一次提交的哈希 ID。这就是git branch -f,git reset等可以做的:通过将不同的哈希 ID 强制到分支名称中,这会更改哪个提交是最后一次提交。这也是为什么git commit将新提交的哈希 ID 写入分支名称的原因:根据定义,这个新提交是最后一次提交,并且根据定义,分支名称必须包含最后一次提交的哈希 ID ,因此git commit使两个定义一起工作.
什么时候git log更复杂
通过绘制每个提交和从它到其父级的箭头来绘制提交图通常很重要:
I--J <-- branch1
/
...--G--H
\
K--L <-- branch2
这里我们有两个不同的分支名称,每个都指向一个提交——<code>J for和branch1for——它们的一个提交像往常一样向后指向,但是在某些时候,两个分支在历史上合并:提交并且都指向提交。Lbranch2IK H
该git log命令通常从当前分支提示开始并向后工作。例如,如果我们选择branch1当前分支,我们将J首先看到 commit,然后I是 ,然后H是 ,然后G是 ,依此类推。如果我们选择branch2当前分支,我们将看到L, then K, then H, 以此类推。请注意,无论哪种方式,我们都看不到其中两个提交。
然而,我们可以告诉git log从和 branch1 开始 branch2。但git log一次只能显示一个提交。它会首先向我们展示J,还是L首先向我们展示?
这个问题的答案很复杂。有时 Git 会J先显示我们,有时它会L先显示。要选择两者之一,请将两个提交放入git log优先级队列。默认优先级是提交者的 date-and-time ,所以无论是哪个或后来的都是我们首先看到的。您可以提供不同的排序选项,从而改变优先级;在这种情况下,您可能会首先看到另一个提交。JLgit log
从队列中取出两个提交之一并显示它,git log现在将该提交的父级放入优先级队列。然后它将显示最高优先级的提交——无论是哪一个——下一个。然后它将提交的父级放入优先级队列。这个队列中还有两个提交,所以再一次,你在前两个之后看到的哪个提交是由优先级队列决定的。
最终,很可能——或者,取决于你给出的标志git log,必要的——在显示 commit之前git log会显示J, I, L, 。此时只有提交本身将在优先级队列中,因此现在将显示并将提交放入队列中。Now将是队列中唯一的提交,因此将显示,并将的父级放入队列中,依此类推。K HHgit logHGGgit logGG
换句话说,git log 有时 对提交进行排序。它仅在其优先级队列中包含多个条目时才执行此操作,如果您为其提供多个起点提交,则会自动发生这种情况,例如J和L此处。但是看看我们合并一些提交后会发生什么:
I--J
/ \
...--G--H M--N <-- master
\ /
K--L
现在,如果我们开始master并返回,git log 必须先显示N——这是队列中唯一的提交——然后是M. 但是 commitM有两个父母,所以git log现在将两者都J放入L队列中。现在它有两个提交要显示,所以它会根据优先级选择一个。换句话说,它现在根据我们给它的选项对提交进行排序。当我们有两个分支名称时,我们又回到了同样的情况,指向提交J和L,并被告知git log显示两个提交/分支。
git log一次向我们展示一个提交,即使历史更复杂
我们从中看到的历史git log可能会产生误导。它可能会按顺序向我们显示N, then M, thenL和K, then I andJ`。如果我们不知道存在分支合并的事实,我们可能会认为提交是这样线性进行的。
出于这个原因,您可以git log绘制一个粗略的基于文本的图表。该图并不总是很漂亮,但它会显示提交L并且K与提交平行J并且I:它们在历史记录中形成分支和合并结构,如提交中记录的那样。
历史就是提交;提交是历史
请注意,在所有这些过程中,没有提交更改。git log --sort=...我们可以使用或以不同的顺序一次查看一个提交git log --topo-order,和/或让 Git 使用 绘制图表git log --graph。或者,我们可以在白板、纸上或使用各种绘图程序绘制我们的图表。但无论我们做什么,提交本身都不会改变。这些提交是您存储库中的历史记录。从您进行提交的那一刻起,他们的联系就存在:子提交指向他们的父母。合并提交是具有两个(或更多)父级的提交。当父母有两个或更多孩子时分支“分叉”。
您始终可以向图表添加更多提交。Git通过使用分支名称来查找提交,根据定义,分支名称是该分支中的最后一个提交。您始终可以添加新的分支名称,也可以删除分支名称。如果你删除了唯一允许你找到某个提交的名称——例如,如果你有:
...--o--o--o--o--...--o <-- master
\
H <-- branch
然后你删除了这个名字branch——在这种情况下H,一个特定的提交变得难以找到,或者可能是不可能的。但是,Git 有其他方法来查找提交。例如,标签名称可以指向提交。Git 还会记录特定分支和其他名称的先前值。此日志称为reflog,可以让您找到 commit H。
某些操作,例如git rebase,通过将现有的一系列提交复制到新的和改进的提交,然后移动分支名称来工作。由于 name 包含最后一次提交的哈希 ID,如果我们复制提交然后移动name,看起来我们以某种方式更改了提交:
...--o--o--o--o--...--o <-- master
\
H--I--J <-- branch
变成:
...--o--o--o--o--...--o <-- master
\ \
H--I--J H'-I'-J' <-- branch
H'的副本在哪里,是 的副本H,是 的副本。不过,我们实际上并没有更改提交。原件仍然存在,在存储库中。您只是无法轻易找到它们,因为我们从名称开始现在查找提交,然后从那里向后工作。I'IJ'J branchI'
(名称的 reflogbranch保存了 commit 的哈希 ID J。如果我们决定要“撤消”变基操作,我们可以强制再次branch记住该名称J,而不是J'。然后 reflog forbranch将保存 commit 的哈希 ID J'。由于 reflogs 包含多个条目,我们最终将有多个 reflog 条目保存J和/或J'。尽管 reflog 条目最终会过期,所以最终我们会J完全忘记提交,只要名称branch仅指向J'. 分支名称只能指向一个提交,事实上,必须始终指向一个提交。)