如果我有一个最初的一个Git仓库提交其次是改变100个文件,每一个只是做一个改变一个文件100名个小提交,我可以通过挤压节省空间的100承诺变成一个大的100文件改变的承诺?例如:
$ git checkout master
Already on branch 'master'.
$ git reset --soft HEAD~100 && git commit -m 'squash last 100 commits'
Run Code Online (Sandbox Code Playgroud)
将使用master与旧提交具有相同内容的新提交替换分支的提示,但将100个先前提交保留在其历史记录之外.这节省多少空间?
tor*_*rek 22
也许(甚至"可能")它会节省一些空间,但不会马上.事实上,起初它会使事情变得更大.
让我们来看看git如何实际存储东西.它变得复杂,但它开始非常简单:git完全保存每个文件(使用"zlib deflate"压缩,但只使用原始文件).
在git存储库中,所有内容都存储为对象.每个对象都以其SHA-1命名,SHA-1是其实际内容(其对象类型,大小和数据)的加密校验和.这使您可以执行以下两项操作之一:计算SHA-1并按名称存储对象(或发现它已存在于其中); 或者,给定SHA-1名称,找到对象,从而访问其内容.
有四种类型的对象.一个在这里无趣.1 其他三个是:
通过从提交的SHA-1 ID开始,git可以提取树,它告诉它要提取哪些blob以及提供它们的文件名(例如1234567...,要调用blob对象file1.txt).
实际对象存储在子目录中.git/objects,例如,1234567...保留对象.git/objects/12/34567....(SHA-1总是长40个字符,但我们大多将它们缩写为7加3个点,这通常就足够了.)
1为了完整起见,最后一个对象类型是"带注释的标签":它包含一个像提交一样的作者("tagger"),另一个像提交一样的SHA-1,以及一个像提交一样的消息,所以它基本上非常像提交; 但它包含的SHA-1 ID通常是提交对象的ID,而不是树对象,然后有一个指向带注释标记的轻量级标记.除此之外,这还允许您将加密签名的标记放入存储库,其他人可以检查您是否已批准该特定提交.
2模式实际上只是普通文件的一位(执行或不执行),但是git也可以存储符号链接,子树和"子模块",因此实际上只有一位而不是一位.出于我们的目的,我们可以忽略除文件之外的所有文件
假设我们创建了一个存储库,并为其提供了100个文件的初始提交,每个文件与所有其他文件不同.为了简单起见,我们还将所有100个文件放在顶层(没有子目录).然后,存储库的初始状态具有:
加上通常的git开销(一个分支文件包含最尖端的SHA-1 ID master,HEAD文件等).我们称这个回购为"hundredfile.git".100个文件只是"file1.txt"到"file100.txt".
如果我们在hundredfile.git中计算对象,那么根据上面的列表将有102个.
现在我们将克隆此存储库,以便我们可以进行100次提交或1次提交,并比较结果.首先,让我们做100次提交.下面实际上是伪代码,但足够接近真正工作(我想/希望),只要你设置make_change_to来对文件进行更改.此外,我们希望每个更改都生成一个新的唯一文件(这样所有100个文件总是彼此不同),否则下面描述中的某些项目会出错.
$ 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]
Run Code Online (Sandbox Code Playgroud)
每次我们进行新的提交时,git都会将索引(暂存区域)转换为带有新blob的新树; 但我们只修改了一个文件,因此100个blob中的99个实际上与上次相同(具有相同的SHA-1 ID).只有一个修改过的文件,file$i.txt有一个新的不同的SHA-1 ID.
因此,每次我们进行新的提交时,我们会得到一个新的提交对象("change $i"加上作者和提交者时间戳,加上树),一个新的"树"对象(列出99个相同的blob-ID加上一个新的,不同的blob-ID)和一个新的"blob"对象.
换句话说,每次提交都会向存储库添加三个对象,并重新使用99个现有的blob对象.我们重复这个过程100次,因此添加了300个对象.300 + 102 = 402,所以这个克隆method1有402个对象.
现在让我们回到原版hundredfile.git并制作一个新的克隆:
$ cd .. # up out of the "method1" repo
$ git clone ssh://host.dom.ain/hundredfile.git method2
[clone messages]
$ cd method2
Run Code Online (Sandbox Code Playgroud)
这次,让我们一次更改(并添加)所有100个文件后进行一次提交:
$ 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]
Run Code Online (Sandbox Code Playgroud)
在这里,所有100个文件都是不同的,因此git存储一个新的提交,其中包含一个新的树,其中包含100个新的blob-ID.这个repo现在有102 + 102 = 204个对象,而不是402个对象method1.
这几乎肯定会占用更少的磁盘空间.细节因系统而异,但通常任何文件都需要至少512或4096(一个"磁盘块值")字节才能存储.由于每个git对象都是一个磁盘文件,因此存储更多对象会占用更多空间.
但是有几个皱纹.
Git 真的很喜欢挂在物品上.当你将100个提交(in 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个提交压缩为一个时,分支的上一个提示(master上面问题中存储的SHA-1 )将保存在两个reflog中,一个用于HEAD分支,一个用于分支.(master当然,新的压缩提交的ID会照常进入.)
这些reflog保留旧的提交,但只有在reflog条目到期之前.默认情况下,到期时间设置为30天(某些情况下为90天,但此时为30天).一旦它们过期,git reflog expire将删除它们(或者您可以手动删除它们,但这有点棘手).
此时,旧提交变得真正未引用:无法找到先前提交的SHA-1 ID.现在Git的垃圾收集器(的一部分git gc是-和记git gc也运行git reflog expire你)可以去除承诺,一旦它不见了,还有以前的承诺,等回到第100所提交的.那些树对象没有被引用,除了最后一棵树; 而那些反过来使blob没有被引用,除了最后的blob.(最后一棵树和最后的斑点,仍然可以通过你做的壁球提交找到.)
所以现在存储库实际上缩小到与repo中相同的204个对象method2.(如果所有提交时间戳都相同,则它们只是完全相同的对象,但对象数量将缩减为204.)
但还有一个皱纹使得以前所有的皱纹大多无关紧要.
除了对象的"松散"格式外.git/objects/12/34567...,git还具有"打包"格式.打包的对象将压缩到同一包中的其他对象.
当您对某个文件进行更改时,您将获得两个不同的git blob对象.3 每个对象都是zlib压缩的,但是git在这一点上并没有将它与任何其他blob进行比较:它是"独立压缩"的,就像它一样.但是,一旦两个对象存储在一个包文件中,它们就可以相互"三角压缩".在三角洲格式的细节是相当模糊的(而不是那么重要,Git是高达打包文件格式的数字4,大多数人,当它改变了以往的时代从来没有注意到),但问题是,现在实际上Git的不存储"变化".它不一定是"改变了的原因file1.txt:例如,git可能已经压缩file39.txt了file75.txt.这完全取决于文件中实际存在的内容以及git选择压缩的对象.它甚至可以压缩其他类型的对象.
与reflogs和垃圾收集一样,git的打包(或重新打包)是通过自动完成的git gc,gc只要它认为合适,git 就会自动调用(参见设置gc.auto).
如果您愿意,可以进行手动重新打包,到期和对象收集,并且有时可以调整一些参数以获得更好的打包,但这远远超出了本答案的范围.通常自动结果很好,并且压缩得很好,以至于.git目录小于任何单独的签出提交并不罕见.
3更确切地说,新文件存储为松散对象; 存储在包中的现有对象只是保留在包中.
为了节省大量空间,您必须删除所有不能很好压缩的大文件(巨型图像或gzip'ed tar-ball或其他)的引用,即使在包文件中使用delta压缩也是如此.你可以这样做git filter-branch,虽然它很复杂; 或者你可以使用BFG cleaner.请参阅如何从Git存储库中的提交历史记录中删除/删除大文件?对于几种方法.
总的来说,在我看来,尝试为个人提交这样做是不值得的.如果结果是一个更明智的历史,那么压制一堆提交; 不要只是为了节省磁盘空间.它可能会节省一些,但不足以值得失去有用的历史.(另一方面,失去无用的历史记录,使后来的调试变得更难而不是更容易 - 即使它使存储库更大,也是一件好事!)