Git的包文件是增量而不是快照?

Git和大多数其他版本控制系统之间的主要区别之一是其他人倾向于将提交存储为一系列增量 – 在一次提交和下一次提交之间进行更改。 这似乎是合乎逻辑的,因为它是存储关于提交的尽可能最less量的信息。 但提交历史logging得到的时间越长,比较修订范围的计算就越多。

相比之下,Git会在每个修订版本中存储整个项目完整快照 。 这并不能使每个提交的repo大小显着增长的原因是项目中的每个文件都存储在Git子目录中的一个文件中,这个文件被命名为内容的散列。 所以如果内容没有改变,散列没有改变,提交只是指向同一个文件。 还有其他优化。

所有这些对我来说都是有意义的,直到我偶然发现有关包文件的信息 ,Git会周期性地放入数据以节省空间:

为了节省空间,Git使用packfile。 这是一种格式,Git将只保存第二个文件中已经更改的部分,并且指向与其类似的文件。

这不是基本上回到存储三angular洲? 如果不是,它有什么不同? 这是如何避免让Git遇到与其他版本控制系统相同的问题?

例如,Subversion使用deltas,回滚50个版本就意味着取消50个diff,而使用Git则可以获取相应的快照。 除非git还在包文件中存储了50个差异…是否有一些机制说:“在一些less量的三angular洲之后,我们将存储一个全新的快照”,以便我们不会堆积太多的变更集? Git还可以避免delta的缺点吗?

概要:
Git的包文件经过精心构build,可以有效地使用磁盘caching,并为常用命令和读取最近引用的对象提供“很好”的访问模式。


Git的包文件格式非常灵活(请参阅Documentation / technical / pack- format.txt或The Git Community Book中的包文件)。 包文件以两种主要方式存储对象:“不确定”(取原始对象数据并对其进行解压缩),或“已确认”(对其他对象构成增量,然后对生成的增量数据进行解压缩)。 存储在一个包中的对象可以是任何顺序(它们不必(必须)按对象types,对象名称或任何其他属性进行sorting),并且可以针对任何其他合适的相同types的对象进行分解对象。

Git的pack-objects命令使用多种启发式方式为常用命令提供极好的参考位置 。 这些启发式控制分类对象的基础对象的select和对象的顺序。 每个机制都是独立的,但是他们有一些共同的目标。

Git确实形成了三angular形压缩对象的长链,但启发式试图确保只有“旧”对象位于长链的末端。 Delta基本caching(其大小由core.deltaBaseCacheLimitconfigurationvariables控制)会自动使用,并且可以大大减less需要读取大量对象(例如git log -p )的命令所需的“重build次数”。

Delta压缩启发式

一个典型的Git仓库存储了大量的对象,所以它不能合理地将它们全部比较以find将产生最小增量表示的对(和链)。

三angular洲基地select启发式是基于这样的想法,即在具有相似文件名和大小的对象中find好的三angular洲基地。 每种types的对象都是分开处理的(即一种types的对象决不会被用作另一种types的对象的三angular形基础)。

为了select增量基数,对象按(主要)按文件名和大小sorting。 进入这个sorting列表的窗口被用来限制被视为潜在三angular洲基地的对象的数量。 如果在其窗口中的对象中没有find对象的“足够好” 1增量表示,则该对象将不会被增量压缩。

窗口大小由git pack-objects--window=选项或者pack.windowconfigurationvariables控制。 delta链的最大深度由git pack-objects--depth=选项或者pack.depthconfigurationvariables控制。 git gc--aggressive选项可以大大扩大窗口大小和最大深度,以尝试创build一个较小的包文件。

文件名sorting将具有相同名称(或至less类似结尾(例如.c ))的条目的对象聚集在一起。 大小sorting是从最大到最小,因此删除数据的增量比偏移添加数据的增量(因为删除增量具有较短的表示),并且较早的较大的对象(通常较新)倾向于用简单压缩来表示。

1符合“足够好”的标准取决于所讨论的物体的大小及其潜在的三angular洲基础,以及其产生的三angular洲链条的深度。

对象sorting启发式

对象以“最近引用的”顺序存储在包文件中。 重build最近的历史所需要的对象被放置在更早的时候,它们将靠近在一起。 这通常适用于操作系统磁盘caching。

所有提交对象按提交date(最近的第一个)sorting并存储在一起。 这种放置和sorting优化了走过历史graphics和提取基本提交信息(例如git log )所需的磁盘访问。

树和blob对象以第一次存储(最近)提交时的树开始存储。 每棵树都以深度优先方式处理,存储尚未存储的所有对象。 这将所有的树和blob所需的重build最近的提交在一个地方。 任何尚未保存但稍后提交所需的树和blob将按照已sorting的提交顺序存储。

最终的对象sorting受delta baseselect的轻微影响,如果一个对象被选为delta表示并且它的基本对象还没有被存储,那么它的基本对象被存储在被分离的对象本身之前。 这可以防止由于读取基本对象所需的非线性访问而导致的磁盘高速caching未命中错误,这些基本对象稍后将被自然地存储在包文件中。

在包文件中使用增量存储只是一个实现细节。 在那个级别上,Git不知道为什么或者如何从一个修订版本改变到下一个版本,而只是知道blob B和blob A非常相似,除了这些改变之外C.所以它只会存储blob A并且改变C (如果它select这样做 – 它也可以select存储blob A和blob B)。

从包文件中检索对象时,增量存储不会显示给调用者。 调用者仍然看到完整的斑点。 因此,Git的工作方式与没有增量存储优化的方式一样。

正如我在“ 什么是git的瘦身包 ”中提到的那样?

Git只在包装文件中进行分离

我详细说明了“ git binary diffalgorithm(delta存储)是否标准化 ”中用于包文件的delta编码。

请注意,对于Git 2.0.x / 2.1(Q3 2014),控制包文件默认大小的core.deltaBaseCacheLimitconfiguration将很快从16MB core.deltaBaseCacheLimit到96MB。

见David Kastrup的提交4874f54 (2014年5月):

碰撞core.deltaBaseCacheLimit到96米

默认的16米导致大型三angular链结合大文件严重颠簸。

这里有一些基准( git blame变种):

 time git blame -C src/xdisp.c >/dev/null 

使用git gc --aggressive (v1.9,导致窗口大小为250)位于SSD驱动器上的Emacs存储库。
有问题的文件大约有30000行,大小为1Mb,历史logging大约有2500次提交。

 16m (previous default): real 3m33.936s user 2m15.396s sys 1m17.352s 96m: real 2m5.668s user 1m50.784s sys 0m14.288s