我可以通过压缩提交来节省git仓库中的空间吗?

时间:2014-09-27 23:53:19

标签: git

如果我有一个带有初始提交的git存储库,然后是100个改变100个文件的小提交,每个文件只对一个文件进行一次更改,我可以通过将这100个提交压缩成一个大的100个文件来节省空间 - 改变了提交?例如:

$ git checkout master
Already on branch 'master'.
$ git reset --soft HEAD~100 && git commit -m 'squash last 100 commits'

将使用与旧提交具有相同内容的新提交替换分支master的提示,但将100个先前提交保留在其历史记录之外。这节省了多少空间?

1 个答案:

答案 0 :(得分:20)

也许(甚至"可能")它会节省一些空间,但不会马上。事实上,起初它会使事情变得更大。

让我们来看看git如何实际存储东西。它变得复杂,但它开始很简单:git完整地存储每个文件(使用" zlib deflate"压缩,但原始文件除外)。

git对象模型

在git存储库中,所有内容都存储为对象。每个对象都以其SHA-1命名,SHA-1是其实际内容(其对象类型,大小和数据)的加密校验和。这使您可以执行以下两项操作之一:计算SHA-1并按名称存储对象(或发现它已存在于其中);或者,给定SHA-1名称,找到对象,从而访问其内容。

有四种类型的对象。一个是无趣的。 1 其他三个是:

  • "提交对象",它保存提交数据,包括提交消息本身加上a&#34树的SHA-1 ID"对象;
  • "树"存储东西列表的对象:SHA-1,文件名和文件模式; 2
  • "斑点" (文件)对象,用于存储实际文件。 (顺便说一下,单词" blob"可能来自数据库术语BLOB,它是"backronym" for "Binary Large OBject"。)

从提交的SHA-1 ID开始,git可以提取树,告诉它要提取哪些blob以及提供它们的文件名(blob对象1234567...将被称为{{1例如)。

实际对象存储在file1.txt的子目录中,例如,对象.git/objects保存在1234567...中。 (SHA-1总是长40个字符,但我们大多将它们缩写为7加3个点,这通常就足够了。)


1 为了完整起见,最后一个对象类型是"带注释的标签":它包含一个作者(" tagger"),如提交,另一个SHA-1类似于提交,而消息类似于提交,因此它基本上非常像提交;但它包含的SHA-1 ID通常是提交对象的ID,而不是树对象,然后是指向带注释标记的轻量级标记。除此之外,这允许您将加密签名的标记放入存储库,例如,其他人可以检查您是否已批准该特定提交。

2 对于常规文件,模式实际上只是一位(执行或不执行),但是git也可以存储符号链接,子树和"子模块",所以实际上只有一点点。出于我们的目的,我们可以忽略所有文件。


一个例子

假设我们创建了一个存储库并为其提供了100个文件的初始提交,每个文件与所有其他文件不同。为了简单起见,我们还将所有100个文件放在顶层(没有子目录)。然后,存储库的初始状态具有:

  • 一个提交对象
  • 一个树对象
  • 100 blobs

加上通常的git开销(一个包含.git/objects/12/34567...最尖端SHA-1 ID的分支文件,master文件,依此类推)。我们将这个回购称为“#file; hundredfile.git"”。 100个文件只是" file1.txt"通过" file100.txt"。

如果我们计算了100.file中的对象,那么根据上面的列表将有102个。

现在我们将克隆此存储库,以便我们可以进行100次提交或1次提交,并比较结果。首先,让我们做100次提交。下面实际上是伪代码,但足够接近真正工作(我想/希望),只要你设置make_change_to来对文件进行更改。此外,我们希望每次更改都能生成一个新的唯一文件(这样所有100个文件总是彼此不同),否则下面描述中的某些项目就会出错。

HEAD

每次我们进行新的提交时,git都会将索引(暂存区域)转换为带有新blob的新树;但是我们只修改了一个文件,因此100个blob中的99个实际上与上次相同(具有相同的SHA-1 ID)。只有一个修改过的文件$ git clone ssh://host.dom.ain/hundredfile.git method1 [clone messages] $ cd method1 $ for i in $(jot 100); do # note: jot 100 => print list of values 1, 2, ... 100 > make_change_to file$i.txt; git add file$i.txt; git commit -m "change $i" > done [100 commit results come out here] 有一个新的不同的SHA-1 ID。

因此,每次我们进行新的提交时,我们会得到一个新的提交对象(file$i.txt加上作者和提交者的时间戳,加上树),一个新的"树" object(列出99个相同的blob-ID加上一个新的,不同的blob-ID),以及一个新的" blob"对象

换句话说,每次提交都会向存储库添加三个对象,并重新使用99个现有的blob对象。我们重复这个过程100次,因此添加了300个对象。 300 + 102 = 402,因此"change $i"中的此克隆有402个对象。

现在让我们回到原来的method1并制作一个新的克隆:

hundredfile.git

这次,让我们一次更改(并添加)所有100个文件后进行一次提交:

$ cd .. # up out of the "method1" repo
$ git clone ssh://host.dom.ain/hundredfile.git method2
[clone messages]
$ cd method2

这里,所有100个文件都不同,因此git存储一个新的提交,其中一个新树有100个新的blob-ID。此repo现在有102 + 102 = 204个对象,而不是$ for i in $(jot 100); do > make_change_to file$i.txt; git add file$i.txt > done $ git commit -m 'change all' [one commit result comes out here] 中的402个对象。

这几乎肯定会占用更少的磁盘空间。细节因系统而异,但通常任何文件至少需要512或4096(一个#34;磁盘块值为#34;)字节才能存储。由于每个git对象都是一个磁盘文件,因此存储更多对象会占用更多空间。

但是有几个皱纹。

Git就像Borg:它试图添加到它的集体

Git 真的喜欢挂在商品上。当你将100个提交(在method1中)压缩成一个时,git所做的就是一个新的提交添加到其存储库中。这个新提交有你的提交消息(无论它是什么)加上通常的日期和树ID等。它有一棵树,与前一次提交的最终树完全相同,因为该树存储每个blob的名称和SHA-1,这也与以前的blob为同名文件。 (也就是说,树" file1.txt是1234567 ..."在新提交中与原始的tip-of-branch提交相同,这对每个人都是如此文件,所以树是相同的,所以它的校验和是相同的,所以它的SHA-1 ID是相同的。)

所以你在method1得到的是402对象变成了403个对象:原始的402加上一个新的提交,它重新使用了前一个树及其所有以前的blob。存储库变得更大(可能是一个文件的一个磁盘块)。

最终,"未参考"对象是垃圾收集

如果git从未丢弃过任何东西,那么存储库会变得非常臃肿,因此有一种方法可以删除对象。这是基于"参考",这是一个很好的词,用于找到事物"的方法。分支是最明显的引用形式:分支引用文件包含分支尖端的SHA-1 ID。标签也算数,"远程分支"而且 - 这个特殊情况下的关键 - " reflogs"。

当您将100个提交压缩为一个时,分支的上一个提示(上面问题中存储在method1中的SHA-1)将保存在两个reflog中,一个用于master,另一个用于HEAD为分支。 (当然,新的压缩提交的ID会像往常一样进入master。)

这些reflog保留旧的提交,但只有在reflog条目到期之前。默认情况下,到期时间设置为30天(某些情况下为90天,但此时为30天)。一旦它们过期,git reflog expire将删除它们(或者您可以手动删除它们,但这有点棘手)。

此时,旧提交变得真正未引用:无法找到先前提交的SHA-1 ID。现在git的垃圾收集器(git gc的一部分 - 并注意git gc也为你运行git reflog expire)可以删除提交,一旦它消失,也上一次提交,依此类推回100次提交中的第一次。那些树对象没有被引用,除了最后一棵树;而那些反过来使blob没有被引用,除了最后的blob。 (最后一棵树,以及最后的斑点,通过你所做的壁球提交仍然可以找到。)

所以现在存储库实际上缩小为与repo method2中相同的204个对象。 (如果所有提交时间戳相同,则它们只是完全相同的对象,但对象的数量将缩小到204.)

但是还有一个皱纹使得以前所有的皱纹大多无关紧要。

Git打包对象

除了"松散"对象的格式,.git/objects/12/34567...,git有一个" packed"格式。打包的对象将压缩到同一包中的其他对象。

当您对某个文件进行更改时,会得到两个不同的git blob对象。 3 每个对象都是zlib压缩的,但是git并没有将它与其他任何blob进行比较要点:它是独立的压缩",就像它一样。但是,一旦两个对象存储在一个包文件中,它们就可以进行三角形压缩"互相攻击。有关delta格式的详细信息相当模糊(并非所有重要的git都取决于打包文件格式的数字4,并且大多数人在更改之前的时间时从未注意到),但重点是现在 git实际上 存储"更改"。它不一定是file1.txt中的变化:例如,git可能会file39.txtfile75.txt进行压缩。这完全取决于文件中的实际内容以及git选择压缩的对象。它甚至可以压缩其他类型的对象。

与reflogs和垃圾收集一样,git的打包(或重新打包)是通过git gc自动完成的,git会在它认为时自动为您调用gc。适当的(参见setting for gc.auto)。

如果您愿意,可以手动重新打包,过期和收集对象,并且可以调整一些参数以便有时更好地打包,但这远远超出了这个答案。通常自动结果很好,并且压缩得很好,.git目录比任何单独的签出提交都要小。


3 更确切地说, new 文件存储为松散对象;存储在包中的现有对象只是保留在包中。


底线

为了节省大量空间,您必须删除所有对大型文件(巨型图像或gzip' ed tar-balls或其他)的所有引用,这些文件不能很好地压缩,甚至包文件中的delta压缩。您可以使用git filter-branch执行此操作,尽管它相当复杂;或者您可以使用BFG cleaner。有关多种方法,请参阅How to remove/delete a large file from commit history in Git repository?

总的来说,在我看来,尝试为个人提交这样做是不值得的。如果结果是一个更明智的历史,那么压制一堆提交;不要只是为了节省磁盘空间。它可能会节省一些,但不足以值得失去有用的历史。 (另一方面,失去无用的历史记录 - 这使得以后的调试变得更难而不是更容易 - 即使它使存储库更大也是一件好事!)