【问题标题】:What does git do when we do : git gc - git prune当我们这样做时 git 会做什么:git gc - git prune
【发布时间】:2018-10-12 15:13:32
【问题描述】:

启动时后台发生了什么,

  • 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.

这两个选项有什么区别?

谢谢

【问题讨论】:

标签: git garbage-collection git-gc


【解决方案1】:

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 是一个人类可读的名称,例如 READMEsubdir 以及另一个对象的哈希 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 定位提交HH 找到 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...

所以从提交Gtree 对象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-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 程序有 14 天的时间可以完成将这些对象连接到图表中。如果它认为这些对象不值得连接,它可以离开它们;从那时起 14 天,未来的 git prune 将删除它们。

如果您手动运行git prune,则必须选择您的--expire 参数。没有--expire 的默认不是2.weeks.ago,而是now


1树对象实际上包含三元组:名称、模式、哈希。模式为 100644100755 用于 blob 对象,004000 用于子树,120000 用于符号链接,等等。

2为了在 Linux 上的查找速度,哈希在前两个字符之后被拆分:哈希名称 ab34ef56....git/objects 目录中变为 ab/34e567...。这将每个子目录的大小保持在 .git/objects small-ish 内,从而驯服了某些目录操作的 O(n2) 行为。这与git gc --auto 相关,当一个对象目录变得足够大时,它会自动重新打包。 Git假设每个子目录的大小与哈希值大致相同,应该是均匀分布的,所以它只需要计算一个子目录。

【讨论】:

  • 感谢您提供详细信息...但是有一件事我找不到答案...假设我刚刚完成了很多工作(可能包括删除分支)。 .. 我想知道如果我长时间不理会系统(对于 reflogs、过期等),哪个文件会“消失”?
  • @DavidV.Corbin:您需要 git fsck 的修改版本,它 (a) 跳过 reflogs,(b) 找到悬空的 blob,然后 (c) 添加 reflogs 并找到引用的提交这些悬空的 blob 并累积在这些提交中看到的文件名。写这篇文章是一项非常重要的工作,无论您可能选择使用哪种语言。(我在这里假设 file that will go away 您的意思是提交中的文件将被丢弃。)
  • 但请注意,文件不会自行消失: 必须有人运行git gc(或新奇的git maintenance;参见VonC 的回答)让 Git 使旧的 reflog 过期,标记可访问对象,重新打包旧包(尽管任何标有 .keep 的文件都不会被丢弃!),最后,清除旧的未使用对象。
  • 谢谢。我希望避免数据丢失(即,即使是一次提交中包含的最微不足道的更改并在以后的提交中恢复)。有效地确保存储库是不可变的记录。我对此进行的研究越深入,我就越确信 Git(虽然对很多事情都很好)可能不适合这种情况。
  • 嗯,Git 有一个限制——没有任何分支或标签名称被“向后”移动,包括禁止 rebase 操作——可能适合这个目的,但 Git 本身不会施加这个限制。你需要一些外部的东西来做到这一点。如果有一个正式的放置点(这样提交就可以被删除,只要它们没有被正式删除),您可以使用一个简单的预接收钩子在服务器上强制执行此操作,该钩子禁止所有名称删除和任何向后运动。您可能希望在合并后允许某些名称删除,这有点复杂。
【解决方案2】:

由于最近添加了 git maintenance command(Git 2.29(2020 年第四季度)),git gc -prune 的替换将是:

git maintenance pack-refs
# for
git pack-refs --all --prune

借助 Git 2.31(2021 年第一季度),“git maintenance(man) 工具学习了新的 pack-refs 维护任务。

参见commit acc1c4dcommit 41abfe1(2021 年 2 月 9 日)Derrick Stolee (derrickstolee)
(由 Junio C Hamano -- gitster -- 合并到 commit d494433,2021 年 2 月 17 日)

maintenance: 添加 pack-refs 任务

签字人:Derrick Stolee
审核人:Taylor Blau

将松散的引用收集成更压缩的形式是很有价值的。
这通常是 packed-refs 文件,尽管这可能是未来的 reftable。
打包 refs 在带有许多标签或远程分支的 repos 中非常有价值,这些标签或远程分支未被本地用户修改,但对于其他查询仍然是必需的。

例如,对于许多分解的引用,诸如

之类的命令
git describe --tags --exact-match HEAD

可能会很慢(几秒钟)。
终端提示特别使用此命令来显示分离的 HEAD 何时指向现有标签,因此让它变慢会导致用户显着延迟。

添加新的“pack-refs”维护任务。
它运行 'git pack-refs --all --prune'(man) 将松散的 refs 移动到打包的形式中。
目前,这是packed-refs 文件,但将来可能会调整为其他文件格式。

这是“gc”任务的几个子任务中的第一个,可以提取到它们自己的任务中。
在此过程中,我们不应更改“gc”任务的行为,因为这仍然是维护存储库的默认方式。
为这些子任务之一创建新任务只会为那些选择不使用“gc”任务的人提供更多自定义选项。
当然可以同时启用“gc”和“pack-refs”任务并定期运行。
虽然他们可能会重复努力,但他们不会以破坏性的方式发生冲突。

'auto_condition'函数指针暂时留在NULL
我们可以在未来扩展它以检查是否应该在“git maintenance run --auto”期间运行 pack-refs(man)

git maintenance 现在包含在其man page 中:

pack-refs

pack-refs 任务收集松散的参考文件并 将它们收集到一个文件中。这加快了操作 需要遍历许多引用。

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

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

签字人:Derrick Stolee
审核人:Taylor Blau

当“maintenance.strategy”配置选项设置为“incremental”时,会启用默认维护计划。
以每周的节奏将“pack-refs”任务添加到该策略中。

git config 现在包含在其man page 中:

任务,但每小时运行 prefetchcommit-graph 任务, loose-objectsincremental-repack 每日任务,pack-refs 每周任务。


git maintenance register(man) 命令在注册裸存储库时遇到问题,该问题已在 Git 2.31(2021 年第一季度)中得到纠正。

参见Eric Sunshine (sunshineco)commit 26c7974(2021 年 2 月 23 日)。
(由 Junio C Hamano -- gitster -- 合并到 commit d166e8c,2021 年 2 月 25 日)

maintenance:使用裸存储库修复不正确的 maintenance.repo 路径

报告人:Clement Moyroud
签字人:Eric Sunshine

git maintenance start配置的定期维护任务(man)调用git for-each-repo(man)运行git maintenance run(@ 987654349@) 在多值全局配置变量maintenance.repo指定的每条路径上。
因为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

请参阅commit e841a79commit a13e3d0commit 52fe41fcommit efdd2f0commit 18e449fcommit 3e220e6commit 252cfb7commit 28cb5e6(2020 年 9 月 25 日)Derrick Stolee (derrickstolee)
(由Junio C Hamano -- gitster -- 合并于commit 52b8c8c,2020 年 10 月 27 日)

maintenance: 添加松散对象任务

签字人:Derrick Stolee

后台维护作业的一个目标是允许用户禁用自动 gc (gc.auto=0),但保持其存储库处于干净状态。
如果不进行任何清理,松散的对象会使对象数据库变得混乱并降低操作速度。
此外,松散的对象会占用额外的空间,因为它们没有与类似对象的增量一起存储。

为“git maintenance run(man) 命令创建一个“松散对象”任务。
这有助于清理松散的对象,而不会使用以下事件序列中断并发 Git 命令:

  1. 运行 'git prune-packed'(man) 删除任何存在的松散对象 在一个包文件中。并发命令将更喜欢打包的 对象的版本为松散版本。 (当然,有 是特别关心的命令的例外 一个物体的位置。这些对于用户来说很少见 目的,我们希望有选择背景的用户 维护不会尝试进行前台维护。)

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

此步骤基于 similar step in Scalar 和 Git 的 VFS。

git maintenance 现在包含在其man page 中:

loose-objects

loose-objects 作业清理松散的对象并将它们放入 打包文件。

为了防止并发 Git 的竞争条件 命令,它遵循两步过程。

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

批量大小限制为 50 千个对象,以防止工作花费太长时间 包含许多松散对象的存储库。
gc 任务写入无法访问 对象作为松散的对象,只有在 它们不会重新添加到包文件中;出于这个原因,它不是 建议同时启用 loose-objectsgc 任务 同一时间。

【讨论】:

    猜你喜欢
    • 2013-12-05
    • 2016-12-15
    • 2013-09-07
    • 2021-08-10
    • 2011-11-06
    • 1970-01-01
    • 2018-05-27
    • 2016-10-10
    • 2012-10-23
    相关资源
    最近更新 更多