TL;博士
git prune
只删除松散的、无法访问的、陈旧的对象(对象必须具有所有三个属性才能被修剪)。无法访问的打包对象保留在其打包文件中。可触及的松散对象保持可触及和松散。无法访问但尚未陈旧的对象也保持不变。陈旧的定义有点棘手(请参阅下面的详细信息)。
git gc
做更多:它打包引用,打包有用的对象,过期 reflog 条目,修剪松散的对象,修剪删除的工作树,以及修剪/gc 的旧git rerere
数据。
长
我不确定你上面所说的“在后台”是什么意思(背景在 shell 中具有技术意义,这里的所有活动都发生在 shell 的前台,但我怀疑你不是指这些术语)。
所做的就是git gc
编排一整套收藏活动,包括但不限于git prune
. 下面的列表是由前台运行的命令集,gc
没有--auto
(省略它们的参数,这在某种程度上取决于git gc
参数):
git pack-refs
: 紧凑的引用(将.git/refs/heads/...
和.git/refs/tags/...
条目转换为 中的条目.git/packed-refs
,消除单个文件)
git reflog expire
: 过期旧的 reflog 条目
git repack
: 将松散的对象打包成打包的对象格式
git prune
:删除不需要的松散物体
git worktree prune
:删除用户已删除的已添加工作树的工作树数据
git rerere gc
:删除旧的rerere记录
还有一些单独的文件活动git gc
可以自己完成,但以上是主要顺序。请注意,这git prune
发生在(1) 过期 reflog 和 (2) running之后git repack
:这是因为删除的过期 reflog 条目可能会导致对象变得未引用,因此不会被打包然后被修剪以使其完全消失。
在我们查看重新包装和修剪之前要知道的东西
在进入更多细节之前,最好先在 Git 中定义对象是什么,以及对象松散或打包意味着什么。我们还需要了解对象可达性意味着什么。
每个对象都有一个哈希 ID(git log
例如,您在 中看到的那些又大又丑的 ID 之一),即该对象的名称,用于检索目的。Git 将所有对象存储在一个键值数据库中,其中名称是键,对象本身就是值。因此,Git 的对象是 Git 存储文件和提交的方式,实际上,有四种对象类型: 提交对象包含实际提交。树对象包含成对的集合,1一个人类可读的名称,例如README
或subdir
与另一个对象的哈希 ID 一起。另一个对象是一个blob如果树中的名称是文件名,则为对象;如果名称是子目录的名称,则它是另一个树对象。blob 对象包含实际的文件内容(但请注意,文件名在链接到 blob 的树中!)。最后一个对象类型是带注释的标签,用于带注释的标签,这里不是特别有趣。
一旦制成,任何物体都不能改变。这是因为对象的名称(它的哈希 ID)是通过查看对象内容的每一位来计算的。将任何一位从零更改为一,反之亦然,哈希 ID 会发生变化:您现在拥有一个不同的对象,具有不同的名称。这就是 Git 检查是否没有文件被弄乱过的方式:如果文件内容发生了变化,对象的哈希 ID 也会发生变化。对象 ID 存储在树条目中,如果树对象发生更改,树的 ID 也会更改。树的 ID 存储在提交中,如果树 ID 更改,则提交的哈希值也会更改。因此,如果您知道提交的哈希是a234b67...
并且提交的内容仍然哈希为a234b67...
,提交中没有任何变化,并且树 ID 仍然有效。如果树仍然哈希到它自己的名字,它的内容仍然有效,所以 blob ID 是正确的;所以只要 blob 内容散列到它自己的名字,这个 blob 也是正确的。
对象可以是松散的,这意味着它们被存储为文件。文件名就是哈希 ID。2 松散对象的内容是 zlib-deflate。或者,可以打包对象,这意味着许多对象存储在一个打包文件中。在这种情况下,内容不仅仅是放气,它们首先是delta-compressed。Git 挑选出一个基础对象——通常是某个 blob(文件)的最新版本——然后找到可以表示为一系列命令的其他对象:获取基础文件,删除该偏移处的一些文本,在另一个处添加其他文本偏移量,等等。包文件的实际格式记录在这里,如果有点轻。请注意,与大多数版本控制系统不同,增量压缩发生在存储对象抽象以下的级别:Git 存储整个快照,然后在底层对象上进行增量压缩。Git 仍然通过其哈希 ID 名称访问对象;只是读取该对象涉及读取包文件,查找对象及其底层增量基础,并即时重建完整的对象。
关于包文件有一条通用规则,即包文件中的任何 delta 压缩对象都必须在同一个包文件中具有所有基础。这意味着一个包文件是自包含的:永远不需要打开多个额外的包文件来从包含该对象的包中取出一个对象。(可以故意违反此特定规则,产生 Git 所谓的瘦包,但这些规则仅用于通过网络连接将对象发送到已经拥有基本对象的另一个 Git。另一个 Git 必须“修复”或在将其留给 Git 的其余部分之前,“增肥”瘦包以制作一个普通的包文件。)
对象可达性有点棘手。让我们先看看提交可达性。
请注意,当我们有一个提交对象时,该提交对象本身包含多个哈希 ID。它有一个用于保存与该提交相关的快照的树的哈希 ID。它还具有一个或多个父提交的哈希 ID ,除非此特定提交是根提交。根提交被定义为没有父提交的提交,所以这有点循环:提交有父提交,除非它没有父提交。不过很清楚:给定一些提交,我们可以将该提交绘制为图中的一个节点,箭头从节点出来,每个父节点一个:
<--o
|
v
这些父级箭头指向提交的父级或父级。给定一系列单亲提交,我们得到一个简单的线性链:
... <--o <--o <--o ...
这些提交之一必须是链的开始:那是根提交。其中之一必须是end,这就是提示提交。所有内部箭头都指向后(向左),所以我们可以在没有箭头的情况下绘制它,知道根在左边,尖端在右边:
o--o--o--o--o
现在我们可以添加一个分支名称,例如master
. 该名称只是指向提示提交:
o--o--o--o--o <--master
嵌入在提交中的箭头永远不会改变,因为任何对象中的任何东西都不会改变。然而,分支名称中的箭头master
实际上只是某个提交的哈希 ID,并且可以更改。让我们用字母来表示提交哈希:
A--B--C--D--E <-- master
该名称master
现在只存储 commit 的提交哈希E
。如果我们向 中添加一个新的提交master
,我们会写出一个提交,它的父E
节点是我们的快照,它的树是我们的快照,给我们一个全新的哈希,我们可以调用它F
。将点提交F
回E
. 我们将 GitF
的哈希 ID 写入master
其中,现在我们有:
A--B--C--D--E--F <-- master
我们添加了一个提交并更改了一个名称,master
. 从 name 开始可以访问所有以前的提交master
。我们读出了 hash IDF
并读取了 commit F
。它的哈希 ID 为E
,所以我们已经提交了E
。我们读取E
以获取 的哈希 ID D
,从而达到D
。我们重复,直到我们阅读A
,发现它没有父母,并完成。
如果有分支,那只意味着我们有另一个名称找到的提交,其父级也是该名称找到的提交之一master
:
A--B--C--D--E--F <-- master
\
G--H <-- develop
名称develop
位于 commit H
;H
发现G
;并G
指回E
. 所以所有这些提交都是可访问的。
如果提交本身是可访问的,则与多个父级的提交(即合并提交)使其所有父级都可访问。因此,一旦您进行了合并提交,您可以(但不必)删除标识已合并提交的分支名称:现在可以从您执行合并操作时所在的分支的尖端访问它. 那是:
...--o--o---o <-- name
\ /
o--o <-- delete-able
这里底行的提交可以name
通过合并从 到达,就像顶行的提交总是可以从 到达name
。删除名称delete-able
使它们仍然可以访问。如果合并提交不存在,如本例所示:
...--o--o <-- name2
\
o--o <-- not-delete-able
然后删除not-delete-able
有效地放弃了底行的两个提交:它们变得无法访问,因此有资格进行垃圾收集。
同样的可达性属性适用于树和 blob 对象。例如,提交G
中有一个tree
,它tree
有 <name, ID> 对:
A--B--C--D--E--F <-- master
\
G--H <-- develop
|
tree=d097...
/ \
README=9fa3... Makefile=0b41...
所以从 commit G
,树对象d097...
是可达的;从那棵树中,blob对象9fa3...
是可访问的, blob 对象也是如此0b41...
。提交H
可能具有相同的README
对象,在相同的名称下(尽管是不同的树):这很好,这只是使9fa3
双重可访问,这对 Git 来说并不有趣:Git 只关心它是否完全可访问。
外部引用——分支和标签名称,以及在 Git 存储库中找到的其他引用(包括 Git索引中的条目和通过链接添加的工作树的任何引用),提供了对象图中的入口点。从这些入口点,任何对象要么是可到达的——有一个或多个可以通向它的名称——要么是不可到达的,这意味着没有可以找到对象本身的名称。我从这个描述中省略了带注释的标签,但它们通常是通过标签名称找到的,并且带注释的标签对象具有它找到的一个对象引用(任意对象类型),如果标签对象本身是可访问的,则使该对象可访问.
因为引用只引用一个对象,但有时我们使用分支名称做某事后想要撤消,Git 会记录引用所具有的每个值以及时间。这些参考日志或reflogs让我们知道昨天master
有什么,或者上周有什么。最终,这些 reflog 条目是旧的和陈旧的,不太可能再有用了,并将丢弃它们。develop
git reflog expire
重新包装和修剪
在高层次上,现在git repack
应该相当清楚的是:它将许多松散对象的集合转换为一个包含所有这些对象的包文件。不过,它可以做的更多:它可以包含前一个包中的所有对象。以前的包变得多余,之后可以删除。它还可以从包中省略任何无法到达的对象,将它们变成松散的对象。git gc
运行时git repack
,它使用依赖于git gc
选项的选项来执行此操作,因此此处的确切语义有所不同,但前台的默认设置git gc
是使用git repack -d -l
,它已git repack
删除冗余包并运行git prune-packed
。这prune-packed
程序会删除也出现在包文件中的松散对象文件,因此这会删除进入包的松散对象。该repack
程序将-l
选项传递给git pack-objects
(这是构建包文件的实际主力),这意味着省略从其他存储库借来的对象。(最后一个选项对于大多数正常的 Git 使用并不重要。)
无论如何,它是git repack
——或者从技术上讲git pack-objects
——打印计数、压缩和写入消息。完成后,您将拥有一个新的包文件,而旧的包文件已消失。新的包文件包含所有可达对象,包括旧可达的打包对象和旧的可达松散对象。如果松散的对象从一个旧的(现在已被拆除和删除的)包文件中弹出,它们会加入其他松散(且无法访问)的对象,使您的存储库变得混乱。如果它们在拆卸过程中被破坏,则只剩下现有的松散和无法到达的对象。
现在是git prune
: 这会找到松散的、无法到达的对象并将它们删除。但是,它有一个安全开关,--expire 2.weeks.ago
: 默认情况下,由 运行,如果这些对象不是至少两周大git gc
,它不会删除这些对象。这意味着任何正在创建新对象但尚未连接它们的 Git 程序都有一个宽限期。新对象可能松散且无法访问(默认情况下)十四天,然后git prune
才会删除它们。因此,一个忙于创建对象的 Git 程序有 14 天的时间可以完成将这些对象连接到图表中。如果它认为这些对象不值得连接,它可以离开它们;从那时起的 14 天,未来git prune
将删除它们。
如果您git prune
手动运行,则必须选择您的--expire
参数。默认没有--expire
is not2.weeks.ago
而是 just now
。
1树对象实际上包含三元组:名称、模式、哈希。模式是100644
或100755
用于 blob 对象、004000
子树、120000
符号链接等等。
2对于 Linux 上的查找速度,哈希在前两个字符后拆分:哈希名称ab34ef56...
变为目录中ab/34e567...
。.git/objects
这将每个子目录的大小保持在.git/objects
较小的范围内,从而驯服了某些目录操作的 O(n 2 ) 行为。这与git gc --auto
当一个对象目录变得足够大时自动重新打包有关。Git 假设每个子目录的大小与哈希值大致相同,应该是均匀分布的,因此它只需要计算一个子目录。