当我们运行命令:git gc - git prune时,git会做什么?

14

启动时后台正在进行的操作:

  • git gc
  • git prune

git gc 的输出:

Counting objects: 945490, done. 
Delta compression using up to 4 threads.   
Compressing objects: 100% (334718/334718), done. 
Writing objects: 100%   (945490/945490), done. 
Total 945490 (delta 483105), reused 944529 (delta 482309) 
Checking connectivity: 948048, done.

git prune 命令的输出:

Checking connectivity: 945490, done.

这两个选项有什么区别?

谢谢


请参见 https://dev59.com/nloU5IYBdhLWcg3wLEwM#37734293 - phd
2个回答

26

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存储文件和提交的方式,实际上,有四种对象类型:一个提交对象保存了一个实际的提交。一个对象保存了一组成对的数据,一个是可读的名称,如READMEsubdir,另一个是另一个对象的哈希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--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。这具有哈希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-packedprune-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对象是100644100755,对于子树是004000,对于符号链接是120000等。

2为了在Linux上提高查找速度,在第二个字符后分割哈希值:哈希名ab34ef56....git/objects目录中变成ab/34e567...。这使得每个子目录的大小都比较小,从而控制了一些目录操作的O(n2)行为。这与git gc --auto相关联,当一个对象目录变得足够大时,它会自动重新打包。Git假设每个子目录的大小大致相同,因为哈希值应该基本上是均匀分布的,所以只需要计算一个子目录。


感谢提供详细信息...但是有一件事我找不到答案...假设我刚完成了一堆工作(可能包括删除分支)...我想知道如果我长时间不管系统会“消失”的文件是什么(足够长,以至于reflogs过期等)。 - David V. Corbin
@DavidV.Corbin:你需要一个修改版的 git fsck,它可以(a)跳过 reflogs,(b)查找悬空的 blobs,然后(c)添加 reflogs 并查找引用这些悬空 blobs 的提交,并累积在这些提交中看到的文件名。无论你选择用什么语言编写,这都是一项非常复杂的工作。(我在这里假设你所说的 将要消失的文件 是指将被删除的提交中的文件。) - torek
请注意,文件不会自动消失:必须有人运行 git gc(或新的 git maintenance;请参见 VonC 的答案)来使 Git 过期旧的 reflogs、标记可达对象、重新打包旧的包(尽管任何带有 .keep 文件的包都不会被丢弃!),最后清理掉旧的未使用对象。 - torek
数据丢失(即使是在一个提交中包含的最微小的更改,并在稍后的提交中撤消)是我想要避免的。有效地确保存储库是一个不可变记录。随着我进一步研究,我越来越确信 Git(虽然非常适合许多事情)可能不是这种情况下的适当工具。 - David V. Corbin
嗯,Git有一个限制——即不允许将任何分支或标签名称“向后”移动,包括禁止rebase操作——可能适合这个目的,但Git本身不会强制执行该限制。您需要使用外部工具来实现这一点。如果有官方的删除点(以便提交可以在它们被正式删除之前被删除),则可以使用简单的pre-receive hook在服务器上强制执行此操作,禁止所有名称删除和任何向后运动。您可能希望允许某些名称删除后合并,这会使它变得有点复杂。 - torek

3
自从最近加入 git maintenance 命令(Git 2.29 (Q4 2020)),替代 git gc -prune 的命令将会是:
git maintenance pack-refs
# for
git pack-refs --all --prune

在2021年第一季度发布的Git 2.31版本中, "git maintenance"(man) 工具学会了一个新的 pack-refs 维护任务。

查看 提交 acc1c4d提交 41abfe1(2021年2月9日)由Derrick Stolee (derrickstolee)
(由Junio C Hamano -- gitster --提交 d494433合并,2021年2月17日)

维护: 添加pack-refs任务

经过Derrick Stolee签名
由Taylor Blau审核

It is valuable to collect loose refs into a more compressed form.
This is typically the packed-refs file, although this could be the reftable in the future.
Having packed refs can be extremely valuable in repos with many tags or remote branches that are not modified by the local user, but still are necessary for other queries.

For instance, with many exploded refs, commands such as

git describe --tags --exact-match HEAD

can be very slow (multiple seconds).
This command in particular is used by terminal prompts to show when a detatched HEAD is pointing to an existing tag, so having it be slow causes significant delays for users.

Add a new 'pack-refs' maintenance task.
It runs 'git pack-refs --all --prune'(man) to move loose refs into a packed form.
For now, that is the packed-refs file, but could adjust to other file formats in the future.

This is the first of several sub-tasks of the 'gc' task that could be extracted to their own tasks.
In this process, we should not change the behavior of the 'gc' task since that remains the default way to keep repositories maintained.
Creating a new task for one of these sub-tasks only provides more customization options for those choosing to not use the 'gc' task.
It is certainly possible to have both the 'gc' and 'pack-refs' tasks enabled and run regularly.
While they may repeat effort, they do not conflict in a destructive way.

The 'auto_condition' function pointer is left NULL for now.
We could extend this in the future to have a condition check if pack-refs should be run during 'git maintenance run --auto'(man).

git maintenance现在在其手册页面中包括:

pack-refs

pack-refs任务会收集松散的引用文件并将它们收集到单个文件中。这可以加速需要遍历许多引用的操作。

并且它可以按计划运行,作为其新的pack-refs任务的一部分:

maintenance:增量策略每周运行pack-refs

签署者:Derrick Stolee
评审者:Taylor Blau

当"maintenance.strategy"配置选项设置为"incremental"时,会启用默认的维护计划。在每周计划中添加'pack-refs'任务到该策略中。 现在git config已经包含在man page中: 任务,但是每小时运行'prefetch'和'commit-graph'任务,每天运行'loose-objects'和'incremental-repack'任务,并且每周运行'pack-refs'任务。
"git maintenance register"命令(man)曾经在注册裸仓库时出现问题,但已在Git 2.31(2021年第一季度)中得到修正。

请参见 提交 26c7974(2021年2月23日),作者为Eric Sunshine(sunshineco
该提交已于2021年2月25日被Junio C Hamano -- gitster --合并至提交 d166e8c

维护:修复裸仓库中不正确的maintenance.repo路径

报告者:Clement Moyroud
签名者:Eric Sunshine

git maintenance start(man)配置的定期维护任务会调用git for-each-repo(man)在多值全局配置变量maintenance.repo指定的每个路径上运行git maintenance run(man)
由于git for-each-repo可能在需要定期维护的存储库之外运行,因此强制要求由maintenance.repo指定的存储库路径是绝对路径。
然而,git maintenance register(man)未执行任何操作以确保分配给maintenance.repo的路径确实是绝对路径,并且实际上可能(特别是在裸仓库的情况下)将相对路径分配给maintenance.repo
通过在将路径分配给maintenance.repo之前将所有路径转换为绝对路径来解决此问题。
同时,还要修复git maintenance unregister(man)将路径转换为绝对路径,以便确保它可以正确地从maintenance.repo中删除通过git maintenance register分配的路径。

在Git 2.30(2020年第四季度)中, "git maintenance"(man)作为"git gc"(man)的扩展版本,新增了一个命令来替代 git gcgit prune:

请查看由Derrick Stolee (derrickstolee)于2020年9月25日提交的提交 e841a79提交 a13e3d0提交 52fe41f提交 efdd2f0提交 18e449f提交 3e220e6提交 252cfb7提交 28cb5e6
(由Junio C Hamano -- gitster --在2020年10月27日合并,提交 52b8c8c)

维护:添加松散对象任务

已签署:Derrick Stolee

后台维护任务的一个目标是允许用户禁用自动垃圾回收 (gc.auto=0),但保持其存储库处于干净状态。
没有任何清理,松散对象将使对象数据库混乱,并减慢操作速度。
此外,松散对象将占用额外空间,因为它们未与相似对象的增量一起存储。
为 'git maintenance run'(man) 命令创建一个 'loose-objects' 任务。
这有助于通过以下事件序列清理松散对象,而不会干扰正在使用 Git 的并发命令:
  1. 运行 'git prune-packed'(man) 来删除存在于包文件中的任何松散对象。 并发命令将优先选择该对象的打包版本而不是松散版本。(当然,对于特定关心对象位置的命令,有例外情况。这些很少由用户有意运行,并且我们希望选择后台维护的用户不会尝试执行前台维护。)

  2. 在一批松散对象上运行 'git pack-objects'(man)
    这些对象按字典顺序扫描松散对象目录进行分组,直到列出所有松散对象或达到 50,000 个为止。 如果仅由用户进行正常开发创建松散对象,则这已足够。我们注意到有用户拥有数百万个松散对象,因为 VFS for Git 在文件读取操作需要填充虚拟文件时按需下载 blob。

此步骤基于 Scalar 和 VFS for Git 中的 类似步骤

git maintenance现在在其手册页面中包含:

loose-objects

loose-objects任务清理松散的对象并将它们放入pack文件。

为了防止与Git命令并发时出现竞争条件,该任务采用两步处理方法。

  • 首先,删除任何已存在于pack文件中的松散对象;并发的Git进程将从pack文件中检查对象数据而非松散对象。
  • 其次,创建一个新的pack文件(以“loose-”开头),其中包含一批松散的对象。

批量大小限制为50,000个对象,以防在具有许多松散对象的存储库上任务花费太长时间。
如果不重新添加到pack文件中,则gc任务将不可访问的对象作为松散对象写入以供稍后清理;因此,不建议同时启用loose-objectsgc任务。


谢谢您详细的回答,您肯定赢得了我的点赞!我唯一缺少的是:如果我“只想清理”我的存储库,我需要键入什么简洁的git maintenance run命令?到目前为止,我的方法遵循了_您自己_的SO答案,使用git gc && git repack -Ad && git prune。那么git maintenance的等效命令是什么? - claudio
1
@claudio 感谢您的反馈和提问。我猜测命令应该是:git maintenance run --task=gc --task=loose-objects --task=pack-refs && git prune。虽然不是非常简洁,但应该会产生类似的效果。 - VonC

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