TL;DR
git prune
仅删除松散的、不可达的、过时的对象(对象必须具备这三个属性才能被修剪)。不可达的打包对象仍然保留在它们的打包文件中。可达的松散对象仍然是可达和松散的。不可达,但尚未过时的对象也保持不变。 过时的定义有点棘手(请参见下面的详细信息)。
git gc
做了更多的事情:它打包引用、打包有用的对象、过期的reflog条目、修剪松散的对象、修剪已删除的工作树,以及修剪/gc旧的git rerere
数据。
Long
我不确定你上面所说的“在后台”是什么意思(background在shell中有一个技术含义,而所有这里的活动都发生在shell的foreground中,但我猜你并不是指这些术语)。
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
:将松散对象打包成packed object格式
git prune
:删除不需要的松散对象
git worktree prune
:删除用户已删除的添加的工作树的工作树数据
git rerere gc
:删除旧的rerere记录
git gc
自己还有一些其他的单独文件活动,但上述是主要顺序。请注意,git prune
发生在过期reflogs和运行git repack
之后:git prune
之后会导致被删除的过期reflog条目可能导致某个对象变得无法访问,因此不会被打包然后被修剪以完全删除。
在查看repack和prune之前需要了解的事项
在进一步了解之前,最好定义一下在Git中什么是对象,以及松散或紧凑的对象意味着什么。我们还需要了解什么是可达对象。
每个对象都有一个哈希ID,就像你在
git log
中看到的那些大而丑陋的ID一样,这是该对象的名称,用于检索。Git将所有对象存储在一个键值数据库中,其中名称是键,对象本身是值。因此,Git的对象是Git存储文件和提交的方式,实际上,有四种对象类型:一个
提交对象保存了一个实际的提交。一个
树对象保存了一组成对的数据,一个是可读的名称,如
README
或
subdir
,另一个是另一个对象的哈希ID。如果树中的名称是文件名,则另一个对象是一个
blob对象,如果名称是子目录的名称,则是另一个树对象。blob对象保存实际的文件内容(但请注意,文件的
名称在链接到blob的树中!)。最后一个对象类型是
注释标签,用于注释标签,在这里不是特别有趣。
一旦创建,任何对象都无法更改。这是因为对象的名称——它的哈希ID——是通过查看对象内容的每一个位来计算的。将任何一个位从零改为一或反之,则哈希ID会发生变化:现在你拥有了一个不同的对象,具有不同的名称。这就是Git检查文件是否被篡改的方式:如果文件内容发生了变化,对象的哈希ID也会发生变化。对象ID存储在树条目中,如果树对象发生变化,则树的ID也会发生变化。树的ID存储在提交中,如果树ID发生变化,则提交的哈希值也会发生变化。因此,如果你知道提交的哈希值是a234b67...,并且提交的内容仍然哈希为a234b67...,则提交中没有任何变化,树ID仍然有效。如果树仍然哈希为自己的名称,则其内容仍然有效,因此blob ID是正确的;只要blob内容哈希为自己的名称,blob也是正确的。
对象可以是松散的,这意味着它们被存储为文件。文件的名称只是哈希 ID。
2 松散对象的内容是 zlib-deflated。或者,对象可以是打包的,这意味着许多对象存储在单个 pack 文件中。在这种情况下,内容不仅仅是缩小了,而是首先进行了
delta-compressed。Git 选择一个基本对象——通常是某个 blob(文件)的最新版本——然后查找可以表示为一系列命令的其他对象:取出基本文件,在此偏移处删除一些文本,在另一个偏移处添加其他文本,等等。打包文件的实际格式在
此处记录,尽管有点简略。请注意,与大多数版本控制系统不同,增量压缩发生在存储对象抽象化的
下面的级别上:Git 存储整个快照,然后在底层对象上稍后进行增量压缩。Git 仍然通过其哈希 ID 名称访问对象;只是读取该对象涉及读取 pack 文件,找到对象及其底层增量基础,并即时重建完整对象。
关于打包文件,有一个通用规则,即打包文件中的任何增量压缩对象必须在同一个打包文件中具有所有基础内容。这意味着打包文件是自包含的:从打包文件获取对象时,永远不需要打开多个附加的打包文件。(可以故意违反此特定规则,生成 Git 称为“瘦包”的东西,但这些仅旨在用于将对象通过网络连接发送到已具有基本对象的另一个 Git。其他 Git 必须“修复”或“肥化”瘦包以生成正常的打包文件,然后才能留下它供 Git 的其余部分使用。)
对象可达性有点棘手。首先看一下提交可达性。
请注意,当我们有一个提交对象时,该提交对象本身包含几个哈希 ID。它有一个哈希 ID 用于保存与该提交相关联的快照的树。除非此特定提交是根提交,否则它还具有一个或多个父提交的哈希 ID。根提交被定义为没有父级的提交,因此这有点循环:提交具有父级,除非它没有父级。尽管如此,它足够清楚:给定某个提交,我们可以将该提交绘制为图形中的节点,并从节点中出现箭头,每个箭头代表一个父级。
<--o
|
v
这些父级箭头指向提交的父级或父级们。对于一系列单父级提交,我们得到一个简单的线性链:
... <--o <--o <--o ...
其中一个提交必须是链的起点:这就是根提交。其中一个必须是结束点,那就是尖端提交。所有内部箭头都指向后面(向左),因此我们可以在不知道箭头末端的情况下绘制它,知道根在左侧,尖端在右侧:
o--o--o--o--o
现在我们可以添加一个类似于 master
的 分支名称。这个名称只是指向了最新的提交:
o--o--o--o--o <--master
嵌入在提交中的箭头永远不会改变,因为任何对象中的内容都不会改变。但是,分支名称master
中的箭头实际上只是某个提交的哈希值,这个值是可以更改的。让我们使用字母来表示提交哈希值:
A
现在,名称为master
的分支只存储提交E
的哈希值。如果我们向master
添加新的提交,则需要写出一个父提交为E
且快照为我们快照的提交,这将得到一个全新的哈希值,我们可以称之为F
。提交F
指向E
。我们让Git将F
的哈希ID写入master
,现在我们有:
A--B--C--D--E--F <-- master
我们添加了一个提交并更改了一个名称,即master。所有以前的提交都可以通过从名称master开始到达。我们读取F的哈希ID并读取提交F。这具有哈希ID E,因此我们已经到达提交E。我们读取E以获取D的哈希ID,从而到达D。我们重复直到读取A,发现它没有父级,并完成。
如果有分支,那就意味着我们有通过另一个名称找到的提交,其父提交也是通过名称master找到的提交之一。
A--B--C--D--E--F <-- master
\
G--H <-- develop
名称
develop
位于提交
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...
从提交
G
开始,对象
d097...
是可达的;从该树中,对象
9fa3...
和
0b41...
也是可达的。提交
H
可能有相同的
README
对象,名称相同(尽管树不同):这没关系,这只是让
9fa3
成为了双重可达,对于Git来说并不重要:Git只关心它是否可以被访问到。
外部引用——分支和标签名称以及在Git存储库中找到的其他引用(包括Git的索引中的条目和通过链接添加的工作树引用),提供了进入对象图的入口点。从这些入口点,任何对象都是可达的——具有一个或多个可以导致其到达的名称——或者是不可达的,这意味着没有名称可以找到该对象本身。我省略了注释标签的描述,但它们通常通过标签名称找到,并且带有一个对象引用(任意对象类型),如果标签对象本身是可达的,则使该对象可达。
因为引用只引用一个对象,但有时我们会对分支名称进行某些操作,希望在之后撤销,Git会记录每个引用的每个值以及时间。这些引用日志或reflogs让我们知道昨天master中有什么,或者上周develop中有什么。最终这些reflog条目会变得陈旧和无用,
git reflog expire
将丢弃它们。
重打包和修剪
高层次上,
git repack
的作用应该是比较清晰的:它将许多松散的对象集合转换为一个包含所有这些对象的打包文件。除此之外,它还可以包括先前打包中的所有对象。先前的打包变得无关紧要并且可以被删除。它还可以省略打包中任何不可达的对象,将它们转换成松散的对象。当
git gc
运行
git repack
时,它会使用依赖于
git gc
选项的选项,因此确切的语义在这里会有所不同,但是前台
git gc
的默认选项是使用
git repack -d -l
,它会删除冗余的包并运行
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程序有十四天的时间将这些对象连接到图形中。如果它决定这些对象不值得连接,它就可以把它们留下;从那时起,未来的
git prune
将在14天后删除它们。
如果您手动运行
git prune
,则必须选择
--expire
参数。默认情况下,没有
--expire
参数,过期时间不是
2.weeks.ago
,而是
now
。
1树对象实际上保存三元组:名称、模式和哈希值。模式对于blob对象是100644
或100755
,对于子树是004000
,对于符号链接是120000
等。
2为了在Linux上提高查找速度,在第二个字符后分割哈希值:哈希名ab34ef56...
在.git/objects
目录中变成ab/34e567...
。这使得每个子目录的大小都比较小,从而控制了一些目录操作的O(n2)行为。这与git gc --auto
相关联,当一个对象目录变得足够大时,它会自动重新打包。Git假设每个子目录的大小大致相同,因为哈希值应该基本上是均匀分布的,所以只需要计算一个子目录。