TL;DR
git prune 仅删除 松散、无法访问、陈旧 对象(对象必须具有所有三个属性才能被修剪)。无法访问的打包对象保留在其打包文件中。可触及的松散对象保持可触及和松散。无法访问但尚未过时的对象也保持不变。 stale 的定义有点棘手(详见下文)。
git gc 做的更多:它打包引用、打包有用对象、过期 reflog 条目、修剪松散对象、修剪已删除的工作树,以及修剪 / gc 的旧 git rerere 数据。
长
我不确定你上面所说的“在后台”是什么意思(background 在 shell 中具有技术意义,这里的所有活动都发生在 shell 的 前台 em> 但我怀疑你的意思不是这些术语)。
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) 运行 git repack:这是因为删除的过期 reflog 条目可能会导致对象变为未引用,因此无法获取打包然后修剪,使其完全消失。
在我们重新打包和修剪之前需要了解的东西
在进入更多细节之前,最好先在 Git 中定义什么是 object,以及对象是 loose 或 意味着什么em>打包。我们还需要了解对象可达意味着什么。
每个对象都有一个哈希 ID(例如,您在 git log 中看到的那些丑陋的 ID 之一),即该对象的名称,用于检索目的。 Git 将所有对象存储在一个键值数据库中,其中名称是键,对象本身就是值。因此,Git 的对象是 Git 存储文件和提交的方式,事实上,有四种 对象类型: commit 对象包含一个实际的提交。 tree 对象包含一组对,1 是一个人类可读的名称,例如 README 或 subdir 以及另一个对象的哈希 ID。如果树中的名称是文件名,则该其他对象是 blob 对象,或者如果名称是子目录的名称,则它是另一个树对象。 blob 对象包含实际的文件内容(但请注意,文件的 name 位于链接到 blob 的树中!)。最后一个对象类型是annotated tag,用于带注释的标签,这里不是特别感兴趣。
一旦制作完成,任何物品都无法更改。这是因为对象的名称(它的哈希 ID)是通过查看对象内容的每一位来计算的。将任何一位从零更改为一,反之亦然,哈希 ID 也会发生变化:您现在有一个 不同的 对象,具有一个 不同的名称。这就是 Git 检查是否没有文件被弄乱过的方式:如果文件内容发生了变化,对象的哈希 ID 也会发生变化。对象 ID 存储在树条目中,如果树对象发生更改,树的 ID 也会更改。树的 ID 存储在提交中,如果树 ID 更改,则提交的哈希值也会更改。因此,如果您知道提交的哈希是 a234b67... 并且提交的内容仍然哈希到 a234b67...,那么提交中没有任何更改,并且树 ID 仍然有效。如果树仍然哈希到它自己的名字,它的内容仍然有效,所以 blob ID 是正确的;所以只要 blob 内容散列到它自己的名字,这个 blob 也是正确的。
对象可以是松散的,这意味着它们被存储为文件。文件名只是哈希 ID。2 松散对象的内容是 zlib-deflated。或者,对象可以打包,这意味着许多对象存储在单个打包文件中。在这种情况下,内容不仅仅是放气,它们首先是delta-compressed。 Git 挑选出一个 base 对象——通常是一些 blob(文件)的最新版本——然后找到可以表示为一系列命令的其他对象:获取基本文件,在此删除一些文本偏移,在另一个偏移处添加其他文本,等等。包文件的实际格式是documented here,如果有点轻。请注意,与大多数版本控制系统不同,增量压缩发生在存储对象抽象以下的级别:Git 存储整个快照,然后在稍后进行增量压缩底层对象。 Git 仍然通过其哈希 ID 名称访问对象;只是读取该对象涉及读取包文件、查找对象及其底层 delta 基础,并即时重建完整的对象。
关于包文件有一条通用规则,规定包文件内中的任何增量压缩对象都必须在同一个包文件中具有其所有基础。这意味着一个包文件是自包含的:永远不需要打开多个额外的包文件来从包含该对象的包中取出一个对象。 (此特定规则可能会被故意违反,从而产生 Git 所谓的 thin pack,但这些规则仅用于通过网络连接将对象发送到已具有基本对象的另一个 Git。其他 Git 必须“修复”或“增肥”瘦包以制作正常的包文件,然后再将其留给 Git 的其余部分。)
对象可达性有点棘手。让我们先看看提交可达性。
请注意,当我们有一个提交对象时,该提交对象本身包含多个哈希 ID。它有一个用于保存与该提交相关的快照的树的哈希 ID。它还具有一个或多个 父提交 的哈希 ID,除非此特定提交是 root 提交。根提交被定义为没有父提交的提交,所以这有点循环:提交有父提交,除非它没有父提交。不过很清楚:给定一些提交,我们可以将该提交绘制为图中的一个节点,箭头从节点出来,每个父节点一个:
<--o
|
v
这些父级箭头指向提交的父级或父级。给定一系列单亲提交,我们得到一个简单的线性链:
... <--o <--o <--o ...
其中一个提交必须是链的 start:即 root 提交。其中之一必须是 end,这就是 tip 提交。所有内部箭头都指向后(向左),所以我们可以在没有箭头的情况下绘制它,知道根在左边,尖端在右边:
o--o--o--o--o
现在我们可以添加一个分支名称,例如master。该名称只是指向提示提交:
o--o--o--o--o <--master
嵌入中的任何箭头都不会改变,因为任何对象中的任何东西都不会改变。然而,分支名称 master 中的箭头实际上只是某个提交的哈希 ID,并且这个 可以 更改。让我们用字母来表示提交哈希:
A--B--C--D--E <-- master
名称master 现在只存储提交E 的提交哈希。如果我们向master 添加一个新的提交,我们会写出一个提交,其父级是E,其树是我们的快照,给我们一个全新的哈希,我们可以称之为F。提交F 指向E。我们让 Git 将 F 的哈希 ID 写入 master,现在我们有了:
A--B--C--D--E--F <-- master
我们添加一个提交并更改一个名称,master。所有之前的提交都可访问,从名称master 开始。我们读出F的哈希ID并读取提交F。这有E 的哈希ID,所以我们已经达到提交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 仍然可以访问它们。如果合并提交是 not 那里,在这种情况下:
...--o--o <-- name2
\
o--o <-- not-delete-able
然后有效地删除not-delete-able放弃底行的两个提交:它们变得无法访问,因此有资格进行垃圾回收。
同样的可达性属性适用于树和 blob 对象。例如,提交G 有一个tree,而这个tree 有 对:
A--B--C--D--E--F <-- master
\
G--H <-- develop
|
tree=d097...
/ \
README=9fa3... Makefile=0b41...
所以从提交G,tree 对象d097... 是可达的;从那棵树中,blob 对象 9fa3... 是可访问的,blob 对象 0b41... 也是如此。提交H 可能具有相同的README 对象,在相同的名称下(尽管是不同的树):这很好,这只是使9fa3 双重可达,这对Git 来说并不有趣:Git 只关心它是完全可以到达。
外部引用——分支和标签名称,以及在 Git 存储库中找到的其他引用(包括 Git 的 index 中的条目以及通过链接添加的工作树的任何引用),提供进入对象图的入口点.从这些入口点,任何对象要么是可到达的——有一个或多个可以通向它的名称——要么是不可到达,这意味着没有可以找到对象本身的名称。我已经从这个描述中省略了带注释的标签,但它们通常是通过标签名称找到的,并且带注释的标签对象具有它找到的一个对象引用(任意对象类型),如果标签对象本身是可访问的,则使该对象可访问.
因为引用只引用 one 对象,但有时我们使用分支名称做一些事后想要撤消的操作,Git 会为每个值保留一个 log 作为引用有,什么时候。这些参考日志或 reflogs 让我们知道master 昨天 中有什么,或者上周develop 中有什么。最终,这些 reflog 条目会变得陈旧且过时,不太可能再有用了,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 的默认不是2.weeks.ago,而是now。
1树对象实际上包含三元组:名称、模式、哈希。模式为 100644 或 100755 用于 blob 对象,004000 用于子树,120000 用于符号链接,等等。
2为了在 Linux 上的查找速度,哈希在前两个字符之后被拆分:哈希名称 ab34ef56... 在 .git/objects 目录中变为 ab/34e567...。这将每个子目录的大小保持在 .git/objects small-ish 内,从而驯服了某些目录操作的 O(n2) 行为。这与git gc --auto 相关,当一个对象目录变得足够大时,它会自动重新打包。 Git假设每个子目录的大小与哈希值大致相同,应该是均匀分布的,所以它只需要计算一个子目录。