首页 > 解决方案 > 如果对象已经被压缩,为什么 git pack-objects 会“压缩对象”?

问题描述

据我了解,存储在 [bare] git 存储库中的松散对象已被压缩......

...那么为什么git pack-objects(以及所有相关的repack命令gc)有一个很长的 Compressing objects阶段?不应该只是抄袭吗?

例如:

objects/75/f0debd8e421ab3f9cc8b6aeb539796ae86b705已经被压缩了。在包文件中,这个文件应该在它的头部之后立即按字节复制到该位置,因为包文件格式指定压缩数据到那里......那么,如果它已经被压缩了,为什么还需要重新压缩呢?

如果它可能试图使用不同的压缩......我怎么能告诉它不要,而只是按原样使用文件?

更新说明:

标签: git

解决方案


据我了解,存储在 [bare] git 存储库中的松散对象已被压缩......

他们是。但它们是zlib-deflate压缩的。

...那么为什么 git pack-objects (以及所有相关的 repack 和 gc 命令)有一个非常长的 Compressing objects 阶段?

这些命令——<code>git pack-objects 和git repack,无论如何;git gcgit repack为你运行——将许多对象组合到一个包文件中。

包文件是压缩对象的一种方式。松散对象是独立的:Git 只需要读取松散对象并在其上运行 zlib 膨胀传递即可获取该对象的未压缩数据。相比之下,一个包文件包含许多对象,其中一些对象首先是delta-compressed

Delta 压缩实际上是这样说的:要产生这个对象,首先要产生另一个对象。然后在此处添加这些字节和/或在此处删除 N 个字节。重复此添加和/或删除操作,直到完成增量列表。 (delta 指令本身也可以被 zlib-deflate。)你可能会认为这是一种 diff,事实上,一些非 Git 版本控制系统确实使用 diff 或它们自己的内部 diff 引擎来生成它们的增量压缩文件。

传统上,这使用观察到某些文件(例如,foo.ccfoo.py)倾向于通过在文件中的某处添加和/或删除几行而随时间变化,但保持大部分相同。如果我们可以说:获取所有以前的版本,然后添加和/或删除这些行,我们可以将两个版本存储在比存储其中一个版本要少得多的空间中。

当然,我们可以在之前的 delta 压缩文件之上构建一个 delta 压缩文件:获取扩展之前的 delta 压缩文件的结果,并应用这些 delta。 这些构成了delta 链,可以任意长,也许可以一直追溯到第一次创建文件的位置。

一些(非 Git)系统停在这里:每个文件都存储为对先前版本的更改,或者,每次存储文件时,系统都会存储最新的,并转换先前的完整副本(以前是最新的,因此完整的副本)到将“最新”转换为“以前”所需的增量中。第一种方法称为正向增量存储,而第二种方法当然是反向增量存储。前向增量往往处于一个可怕的劣势,因为它提取了最新的文件的版本需要提取第一个版本,然后应用很长的增量序列,这需要很长时间。因此,RCS 使用反向增量,这意味着获取最新版本很快;它得到了一个非常旧的版本,速度很慢。(但是,出于技术原因,这只适用于 RCS 所谓的主干。RCS 的“分支”使用正向增量。) Mercurial 使用正向增量,但偶尔会存储文件的新完整副本,以保持增量链长度短。一个系统 SCCS 使用一种 SCCS 称为interleaved deltas的技术,该技术为提取任何文件提供了线性时间(但更难生成)。

但是,Git 不会将文件存储为files。您已经知道文件数据存储为blob 对象,它最初只是 zlib-deflate,否则是完整的。给定一组对象,其中一些是文件数据,而另一些不是(提交、树或带注释的标记对象),哪些数据属于哪个文件完全不明显。因此,Git 所做的就是找到一个可能的候选对象:某个对象似乎与其他对象有很多相似之处,最好的表达方式可能是从另一个对象开始,然后进行这些增量更改。

压缩中花费的大部分 CPU 时间都花在寻找好的链上。如果版本控制系统选择文件(或对象)不佳,则压缩效果不会很好。Git 使用了一堆启发式方法,包括窥视树对象以重建文件名(仅基本名称 - 不是完整路径名),否则时间复杂度会变得非常疯狂。但即使使用启发式方法,找到好的三角链也是昂贵的。通过“窗口”和“深度”设置,究竟有多昂贵是可调的。

有关(更多)关于打包文件的信息,这些文件随着时间的推移经历了多次修订,请参阅Git 中的 Documentation/technical 目录


推荐阅读