【问题标题】:Git internals: how does Git store small differences between revisions?Git 内部结构:Git 如何存储修订之间的微小差异?
【发布时间】:2017-09-07 15:31:57
【问题描述】:

据我了解,一些 VCS 存储修订版之间的差异,因为有时候差异很小 - 源代码中的一行被更改或在后续修订版中添加了注释。另一方面,Git 为每个修订存储压缩的“快照”。

如果只做了很小的更改(大文本文件中的一行),Git 如何处理这个?它是否存储两个几乎相同的副本?我认为这将是对空间的低效利用。

【问题讨论】:

  • 不是真正的重复,但这个问题解释了你的问题的答案,然后更进一步:Are Git's pack files deltas rather than snapshots?
  • Git 最初会存储两个几乎相同的副本。在实践中,这不是什么大问题。对象最终——没有精确的时间限制,但几乎总是在传输到另一个 Git 之前——被压缩成 确实使用 delta 编码的 pack files;请参阅@GregHewgill 的链接。

标签: git


【解决方案1】:

它是否存储两个几乎相同的副本?我认为这将是对空间的低效利用。

是的,Git 就是这样做的,至少一开始是这样。当您进行提交时,Git 会在 .git/objects/ 树下创建一个(略微压缩的)源文件副本,其名称基于内容的 SHA1(这些称为“松散”对象)。你可以去看看这些文件,如果你对格式感兴趣的话,值得去看看。

要记住的一点是,Git 是为速度 而构建的,并且不太关心存储库数据的大小。当 Git 想要获得一个旧版本来查看它时,它所要做的就是从 .git/objects/ 树中按原样读取文件。没有应用增量,只是使用 zlib 解压缩原始读取字节(非常快)。

现在,您会发现在使用存储库一段时间后,.git/objects/ 中的文件将包含您的源文件的大量副本,只是略有不同。这就是“打包”文件的用武之地。当您创建一个打包文件(自动或手动)时,Git 会将所有文件对象收集在一起,以一种可以很好压缩的方式对它们进行排序,然后使用数字将它们压缩成一个打包文件不同的技术。

创建包文件时使用的技术之一确实是增量压缩。 Git 会注意到两个对象看起来非常相似,并存储其中一个对象和它们之间的差异。请注意,这纯粹是在对象基础上作为原始数据完成的,而不考虑事情的提交顺序或分支的排列方式。就 Git 的其余部分而言,低级包文件格式是一个实现细节。

请记住,Git 仍然是为速度而构建的,因此打包文件不一定是您可以获得的绝对最佳压缩方式。在创建包文件时有很多与速度和大小之间的权衡相关的启发式方法。

当 Git 想要读取一个对象并且它不是一个“松散”的对象时,它会查看包文件(位于 .git/objects/pack/ 中)以查看是否可以在那里找到它。当 Git 找到正确的包文件时,它会从包文件中提取对象,应用所需的任何算法(增量解析、解压缩等)来重建原始文件对象。 Git 的更高级别部分并不关心包文件如何存储数据,这是一个很好的关注点分离并简化了应用程序代码。

如果你想了解更多,我建议阅读Pro Git book,特别是章节

【讨论】:

  • Greg Hewgill,谢谢你的回答。我确实正在阅读 Pro Git 书籍,并讨论了您之前链接到的问题。但如果我现在可以问一件事,那就是:是什么触发了自动创建包文件?当然,Git 并没有在后台运行,所以我假设它是在我发出一些命令(而不是显式手动创建包文件)时作为副作用(自动)完成的。
  • 我不知道确切的规则,但有类似“如果你得到 1500 个或更多松散对象,Git 将进行垃圾收集,可能创建包文件” .显然,确切的规则是更准确的,但git gc 可能会这样做。
  • @flow2k:是的,自动打包是运行其他命令的副作用。有关详细信息,请参阅 git-gc documentation(特别是 --auto 开关)。
  • 下面2个可以一起详细说明吗?你说“没有应用增量,只是使用 zlib 解压缩原始读取字节(非常快)。”然后从 Packfiles 链接它说:“但是 033b4 只占用 9 个字节。同样有趣的是,文件的第二个版本是完整存储的,而原始版本存储为 delta”。如果存储为一个detla,这不是反驳没有应用deltas的说法吗?
【解决方案2】:

git 存储实际提交文件的方式在存储库的生命周期内会有所不同,但让我们从基础开始。

当您将文件提交到存储库时,会生成一个新文件,即该文件的完整副本。 SHA1 是根据其内容计算得出的,这是该文件的“对象 ID”。

你可以在.git\objects\SH\A1-hash下找到这个文件

SH\A1-hash 是我的方式,表示 SHA1 的前两个字符用作文件夹名称,其余 38 个字符用作该目录内的文件名。

然后你修改这个文件,将它添加到索引中,并提交它。

这再次存储为一个全新的文件,索引方式与上述完全相同。

这很容易测试,但请记住,每当您提交更改 1 个文件时,您将获得 3 个 git 对象:

  • 文件的新版本
  • “树”对象,指示索引中每个文件的哪个版本用于此特定提交
  • 提交对象,存储对其父级和树的引用。

所以是的,git 将文件存储为完整的快照。请注意,这些文件是压缩的,因此它们占用的空间不及该文件的两个完整副本,但它们占用的空间与该文件的两个完整压缩副本一样多。

如果要添加的文件不能很好地压缩(想想 jpg、png 或 zip 文件),那么是的,这会占用大量空间。

在某些时候,Git 可能会决定打包您的存储库,并且在这里 Git 可能会决定在此打包文件中使用 delta-compression(压缩并存储文件之间的差异)。然而,Git 的其余部分并没有看到这一点,因为这是对 Git 内部底层文件访问的抽象。各种 Git 命令的实现还是会看到“un-deltified”(如果有这个词的话)文件。

现在,各种命令总是会向您隐藏这一点,因为您使用的大多数 git 命令,如果实施得当,会对您(开发人员)隐藏所有底层抽象和优化,而是专注于你可能想看到什么。

因此,如果您查看这些文件,一些命令将显示差异,其中底层文件没有存储为差异,这仅仅是因为差异对您(开发人员)更有意义。

如果您改为使用管道命令,您将看到更多的 blob。

如果您想了解这一切在实践中的效果,您只需要知道 1 个命令,那就是 git cat-file -p SHA1

这是一种测试方法:

  1. 初始化新的存储库
  2. 添加文件并提交
  3. 执行git log并复制提交的SHA1
  4. 执行git cat-file SHA1-of-commit,你会看到这样的:

    tree d7d68c5b2ecc58da225c953e35b0797a4805b844
    author Lasse Vågsæther Karlsen <lassevagsaether.karlsen@visma.com> 1491986419 +0200
    committer Lasse Vågsæther Karlsen <lassevagsaether.karlsen@visma.com> 1491986419 +0200
    
    First copy
    
  5. 现在复制tree之后的SHA1 id,这是树对象的对象id,然后执行git cat-file SHA1-of-tree-object,你会看到这样的:

    100644 blob 3b5d02884e6a17f20ed7938bf9e534f1bd0d195e    Temp.7z
    

    这告诉您索引包含 1 个文件(1 行),文件名为 Temp.7z,并告诉您它的 SHA1 id。复制此 ID。

  6. 执行git cat-file -p SHA1-of-blob,你会看到你添加的文件的内容。

Git 的存储模型一点也不神奇也不复杂,但是里面有很多优化和抽象,避免浪费空间、去重等等。

【讨论】:

  • 这很有启发性 - 谢谢 Lasse V. Karlsen。
  • 这是最好的答案,伙计。
【解决方案3】:

Git 使用补丁或hunks。它计算两个版本之间引入的差异并存储它。

存储两个几乎相同的副本?我认为这将是对空间的低效利用。

Git 会扫描您的代码(启发式)并且只存储一次差异。如果 git 在多个文件中找到相同的代码,它会为相似的代码生成 hunk 并将指向它的指针存储在原始位置。

为了简单起见 - 它比下面的解释要复杂得多,让它变得简单,以便您更容易理解。

扫描您的代码后,git 搜索先前提交的更改,如果发现更改,则 git 将旧更改拆分为 hunk
如果您在文件中间添加了代码,那么它将被拆分为 3 个大块(顶部 = 旧代码,中间 - 新代码,底部 - 旧代码),现在您将拥有 3 个大块。下次 git 扫描您的代码时,他将使用这 3 个大块来搜索更改。

例如:假设您有一堆文件,每个文件的顶部都有许可协议,而这在您的所有文件中都是相同的。
Git 将扫描文件,第一个块将存储为补丁,在所有其他文件上,git 将放置一个指向该块的 指针

这样 git 以一种非常有效的方式存储信息。


如果您想查看它的操作,请使用git add -p 并选择s 进行拆分。


补丁本身看起来像:


如上所述,hunk 是一个差异,这里有一点关于它的内容。 hunk 是一个与 diff 相关的术语,下面是 git 如何直观地显示它(补丁):

格式以与上下文格式相同的两行标题开头,不同之处在于原始文件以--- 开头,新文件以+++ 开头。

接下来是一个或多个更改块,其中包含文件中的行差异。
未更改的上下文行前面有一个空格字符,添加行前面有一个加号,删除行前面有一个减号。


更多信息:

https://github.com/mirage/ocaml-git/blob/master/doc/pack-heuristics.txt

【讨论】:

  • 这是git add -p 的输出,你看到的是一个格式化的补丁。如果您希望看到该补丁本身,请在下一次提交时使用:git commit --verbose,您将看到确切的补丁
  • 这在技术上在两个方面是不正确的:(1)每个快照是逻辑上独立的,并且最初,修改后的大文本文件单独存储的(这 有点低效)。 Git 不存储差异。 (2) 当 Git确实 选择将对象压缩成包文件并将对象转换为 delta 压缩版本时,修改一个版本以产生另一个版本的指令不是文本差异块;它们是 Xdelta (en.wikipedia.org/wiki/Xdelta) 的自定义变体。
  • 恐怕这根本不是 Git 在存储库中存储数据的方式。 git add -p 提供的用户界面与实际的磁盘存储库存储格式无关。据我所知,您对“指向帅哥的指针”的描述实际上并没有出现在 Git 中。由于这些原因,我必须否决你的答案。
  • 如果您想查看实际的存储对象,请不要使用git show,而是使用git cat-file -p,这不会通过尝试找出您可能想要查看的内容来破坏输出,而是显示实际的底层文件(虽然它会解压缩但它不做差异或类似的事情)。
  • 是的,如果谈论存储模型,这个答案是完全错误的。不同的 git 命令将 show 差异和类似的东西,但底层存储模型不会“像这样”存储差异。一个包文件可能会使用增量压缩,但这有点相同,但也有很大不同,因为这是在二进制级别上。
猜你喜欢
  • 1970-01-01
  • 2012-01-18
  • 1970-01-01
  • 1970-01-01
  • 2016-10-06
  • 2015-12-12
  • 2012-10-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多