当我们这样做时git会做什么:git gc - git prune

问题描述 投票:5回答:1

启动时后台发生了什么,

  • 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个回答
9
投票

TL;DR

git prune只删除松散,无法到达的陈旧对象(对象必须具有所有三个属性才能被修剪)。无法访问的打包对象保留在其包文件中。可到达的松散物体仍然可以到达并且松散。无法访问但尚未过时的对象也保持不变。陈旧的定义有点棘手(详见下文)。

git gc做得更多:它打包引用,包装有用的对象,到期reflog条目,修剪松散的对象,修剪删除的工作树,以及prunes / gc的旧git rerere数据。

Long

我不确定你在上面的“在后台”是什么意思(背景在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)到期reflogs和(2)运行git repack之后:这是因为删除的过期reflog条目可能导致对象变为未引用,因此不会被打包然后被修剪以使它完全消失。

Stuff to know before we look at repack and prune

在详细介绍之前,最好在Git中定义对象是什么,以及对象是松散还是打包的含义。我们还需要了解对象可以访问的含义。

每个对象都有一个哈希ID - 例如你在git log中看到的那些丑陋的ID之一 - 就是该对象的名称,用于检索目的。 Git将所有对象存储在键值数据库中,其中名称是键,对象本身就是值。因此,Git的对象是Git如何存储文件和提交,实际上,有四种对象类型:提交对象包含实际提交。树对象包含多组对,1是人类可读的名称,如READMEsubdir以及另一个对象的哈希ID。如果树中的名称是文件名,则另一个对象是blob对象;如果名称是子目录的名称,则它是另一个树对象。 blob对象保存实际的文件内容(但请注意,文件的名称位于链接到blob的树中!)。最后一个对象类型是带注释的标记,用于带注释的标记,这里不是特别有趣。

一旦制成,就不会有任何对象被改变。这是因为对象的名称 - 它的哈希ID - 是通过查看对象内容的每个位来计算的。将任何一位从零更改为一,反之亦然,并且哈希ID会更改:您现在拥有另一个具有不同名称的对象。这就是Git检查没有文件被搞乱的方式:如果文件内容被更改,对象的哈希ID就会改变。对象ID存储在树条目中,如果更改了树对象,则树的ID将更改。树的ID存储在提交中,如果树ID已更改,则提交的哈希值将更改。因此,如果您知道提交的散列是a234b67...并且提交的内容仍然散列到a234b67...,则提交中没有任何更改,并且树ID仍然有效。如果树仍然哈希到自己的名称,其内容仍然有效,因此blob ID是正确的;所以只要blob内容哈希到它自己的名字,blob也是正确的。

对象可以松散,这意味着它们存储为文件。文件名只是哈希ID.2松散对象的内容是zlib-deflated。或者,可以打包对象,这意味着许多对象存储在单个包文件中。在这种情况下,内容不仅仅是放气,它们首先是delta-compressed。 Git选择一个基础对象 - 通常是某个blob(文件)的最新版本 - 然后找到可以表示为一系列命令的其他对象:获取基本文件,删除此偏移处的一些文本,在另一个文本中添加其他文本偏移,等等。包文件的实际格式是documented here,如果有点轻微。请注意,与大多数版本控制系统不同,增量压缩发生在低于存储对象抽象的级别:Git存储整个快照,然后在底层对象上执行增量压缩。 Git仍然通过其哈希ID名称访问对象;它只是读取该对象涉及读取包文件,找到对象及其底层的delta基础,并在运行中重建完整的对象。

有一个关于包文件的一般规则,它规定包文件中的任何增量压缩对象必须在同一包文件中包含所有基础。这意味着包文件是自包含的:从不需要打开多个附加包文件来从包含该对象的包中获取对象。 (这个特殊的规则可以被故意违反,产生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中的箭头实际上只是某些提交的哈希ID,这可能会发生变化。让我们用字母来表示提交哈希:

A--B--C--D--E   <-- master

名称master现在只存储commit 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有 对:

A--B--C--D--E--F   <-- master
             \
              G--H   <-- develop
              |
         tree=d097...
            /   \
 README=9fa3... Makefile=0b41...

所以从提交G,树对象d097...是可达的;从那棵树,blob对象9fa3...是可达的,blob对象0b41...也是如此。提交H可能有相同名称的README对象(虽然是不同的树):这很好,只是让9fa3双倍可达,这对Git来说并不重要:Git只关心它是否可以访问。

外部引用 - 分支和标记名称以及在Git存储库中找到的其他引用(包括Git索引中的条目以及通过链接的已添加工作树的任何引用),为对象图提供了入口点。从这些入口点开始,任何对象都是可访问的 - 具有一个或多个可以导致它的名称 - 或者无法访问,这意味着没有可以通过其找到对象本身的名称。我已经从这个描述中省略了带注释的标签,但它们通常是通过标签名称找到的,带注释的标签对象有一个它找到的对象引用(任意对象类型),如果标签对象本身可以到达那么一个对象可以到达。

因为引用只引用一个对象,但有时我们使用我们想要在之后撤消的分支名称来执行某些操作,Git会保留引用所具有的每个值的日志,以及何时。这些参考日志或reflogs告诉我们昨天master的内容,或者上周develop的内容。最终这些reflog条目陈旧且陈旧,不再可能有用,git reflog expire将丢弃它们。

Repack and prune

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(这是构建pack文件的实际主力),这意味着省略从其他存储库借用的对象。 (对于大多数正常的Git用法,这最后一个选项并不重要。)

在任何情况下,它都是git repack或技术上的git pack-objects,它打印计数,压缩和写入消息。完成后,您有一个新的包文件,旧的包文件已经消失。新的pack文件包含所有可访问的对象,包括旧的可到达的打包对象和旧的可到达的松散对象。如果松散的对象从旧的(现在已拆除和已删除)包文件中弹出,则它们会加入其他松散(且无法访问)的对象,这些对象会使您的存储库混乱。如果它们在拆除过程中被摧毁,则只剩下现有的松散且无法到达的物体。

现在是时候了git prune:这会找到松散的,无法访问的对象并将其删除。但是,它有一个安全开关,--expire 2.weeks.ago:默认情况下,由git gc运行,如果它们不是至少两周,它不会删除这些对象。这意味着正在创建新对象的任何Git程序(尚未连接它们)都有一个宽限期。在git prune删除它们之前的十四天,新对象可能是松散的,无法访问(默认情况下)。因此,一个忙于创建对象的Git程序有十四天,在此期间它可以完成将这些对象连接到图形中。如果它决定那些物品不值得挂钩,它就可以离开它们;从那时起14天,未来的git prune将删除它们。

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


1Tree对象实际上包含三元组:名称,模式,哈希。模式是100644100755用于blob对象,004000用于子树,120000用于符号链接,依此类推。

2对于Linux上的查找速度,哈希在前两个字符后分割:哈希名称ab34ef56...ab/34e567...目录中变为.git/objects。这使得每个子目录的大小保持在.git/objects small-ish中,这样可以驯服某些目录操作的O(n2)行为。这与git gc --auto有关,当一个对象目录变得足够大时,qazxswpoi会自动重新打包。 Git假设每个子目录的大小与散列的大小大致相同,因此它只需要统计一个子目录。

© www.soinside.com 2019 - 2024. All rights reserved.