Git内部:Git如何存储修订版之间的小差异?

时间:2017-04-12 03:11:09

标签: git

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

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

3 个答案:

答案 0 :(得分:12)

  

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

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

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

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

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

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

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

如果您想了解更多相关信息,建议您阅读Pro Git book,特别是

部分

答案 1 :(得分:5)

git如何存储实际提交的文件在存储库的生命周期中有所不同,但让我们从基础知识开始。

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

您可以在.git\objects\SH\A1-hash

下找到此文件

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

然后修改此文件,将其添加到索引并提交。

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

这很容易测试,但请记住,无论何时进行更改1个文件的提交,都会获得3个git对象:

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

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

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

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

现在,各种命令总是会隐藏这一点,因为你使用的大多数git命令,如果实现得好,会隐藏你,开发人员的所有底层抽象和优化,而是关注您可能希望看到的内容。

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

如果你去使用管道命令,你会看到更多的blob。

如果你想看看这一切在实践中是如何运作的,你只需要知道一个命令,那就是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,您将看到您添加的文件的内容。
  7. Git的存储模型根本不是神奇的或复杂的,但是在那里有很多优化和抽象,以避免浪费空间,重复数据删除等等。

答案 2 :(得分:2)

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

  

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

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

为了简单起见 - 它比下面解释的要复杂得多,使它变得简单,这样你就可以更容易地理解它。

扫描代码后,git会搜索之前提交的更改,如果发现更改,git会将旧更改拆分为 hunk 。 如果您在文件的中间添加了代码,那么它将被分割为3个帅哥(顶部=旧代码,中间代码,旧代码,底部旧代码),现在您将拥有3个帅哥。下次git将扫描你的代码,他将使用这3个帅哥来搜索变化。

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

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

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

enter image description here

补丁本身如下:enter image description here

如上所述hunk是一个差异,这里有一点点。 hunk 是与diff相关的术语,以下是git如何在视觉上显示它(补丁):

  

格式以与上下文格式相同的两行标题开头,但原始文件前面有---,新文件前面有+++

     

在此之后是一个或多个包含文件中行差异的更改   未更改的上下文行前面有空格字符,附加行前面带有加号,删除行前面带有减号。

更多信息:

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