Git内部原理:Git如何存储版本间的小差异?

20

据我所知,一些版本控制系统存储版本之间的差异,因为有时候差异很小——源代码中的一行被更改或者在随后的版本中添加了注释。另一方面,Git为每个版本存储了压缩的“快照”。

如果只有一个小的更改(大文本文件中的一行),Git会如何处理?它会存储两个几乎相同的副本吗?这将是一种低效的空间利用,我想。


1
这并不是一个重复的问题,但这个问题解释了你的问题的答案,并进一步深入讨论:Git的打包文件是增量而不是快照吗? - Greg Hewgill
3
Git最初会存储两个几乎完全相同的副本。实际上,这并不是什么大问题。这些对象最终——没有确切的时间限制,但几乎总是在传输到另一个Git之前——会被压缩成pack文件,这些文件使用差分编码;请参见@GregHewgill的链接。 - torek
3个回答

35

它存储两个几乎相同的副本吗?我认为这将是一种低效的空间利用方式。

是的,Git 在开始时确实这样做。当您进行提交时,Git 会在 .git/objects/ 树下创建一个(稍微压缩过的)源文件的复制品,名称基于内容的 SHA1(这些称为“loose”对象)。您可以查看这些文件,如果您对格式感到好奇,这样做是值得的。

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

现在,你可能会注意到,在使用存储库一段时间后,.git/objects/中的文件将包含许多源文件的副本,它们都略有不同。这就是“pack”文件发挥作用的地方。当您创建打包文件(自动或手动)时,Git会将所有文件对象收集在一起,并按照压缩良好的方式对它们进行排序,并使用多种不同的技术将它们压缩成一个打包文件。
创建打包文件时使用的技术之一确实是增量压缩。Git会注意到两个对象看起来非常相似,并存储其中一个对象和它们之间的增量差异。请注意,这是基于纯粹的对象数据完成的,而不考虑提交顺序或分支排列方式。低级别的打包文件格式在Git的其余部分看来只是一个实现细节。
请记住,Git仍然建立在速度上,因此打包文件不一定是您可以获得的最佳压缩效果。与速度和大小之间的权衡相关的打包文件创建中有许多启发式方法。
当Git想要读取一个对象并且它不是“loose”对象时,它会在包文件中查找(这些文件位于.git/objects/pack/),看看是否可以在那里找到。当Git找到正确的包文件时,它从包文件中提取对象,并应用任何算法(增量解析、解压缩等)来重建原始文件对象。Git的高级部分不关心包文件如何存储数据,这是责任分离的好处,简化了应用程序代码。
如果您想了解更多信息,我建议阅读Pro Git书,特别是以下章节:

1
Greg Hewgill,感谢您的回答。我确实正在阅读《Pro Git》一书,并讨论您之前链接的问题。但如果我现在可以问一件事,那就是:是什么触发了自动创建打包文件?当然,Git不会在后台运行,因此我认为它是作为副作用(自动)在我发出某些命令时完成的(而不是显式的手动创建打包文件)。 - flow2k
1
我不知道确切的规则,但有一些类似于“如果您获得1500个或更多的松散对象,Git将执行垃圾回收,这可能会创建包文件”。显然,确切的规则更加精确,但是git gc可以执行此操作。 - Lasse V. Karlsen
3
是的,自动打包是运行其他命令时的副作用。请参阅git-gc文档(特别是--auto开关)以获取更多信息。 - Greg Hewgill
可以详细说明以下两个问题吗?你说“没有使用增量,只是使用zlib解压缩原始读取字节(非常快)”,但从Packfiles链接中可以看到:“但是033b4只占用9个字节。有趣的是,文件的第二个版本是完整存储的,而原始版本则存储为增量”。如果它被存储为增量,这不是反驳了没有使用增量的说法吗? - mfaani

8
Git是如何存储实际提交的文件的在你的存储库的生命周期内会有所不同,但让我们从基础知识开始。当您提交一个文件到您的存储库时,会创建一个新文件,这个文件的完整副本被复制了一份。SHA1是从其内容计算出来的,这是此文件的“对象ID”。您可以在.git\objects\SH\A1-hash下找到此文件。这里的SH\A1-hash是我表示SHA1的前两个字符用作文件夹名称,其余38个字符用作该目录中的文件名的方式。然后您修改此文件,将其添加到索引中并进行提交。这又被存储为一个完全新的文件,以与上述相同的方式进行索引。这很容易测试,但请记住,每当您进行更改1个文件的提交时,您会得到3个git对象:文件的新版本,表示要在此特定提交中使用索引中每个文件的哪个版本的“tree”对象以及存储其父项和树的提交对象。因此,是的,Git将文件存储为完整的快照。请注意,这些文件已经压缩,因此它们占用的空间并不像两个完整副本的文件那么多,但它们占用的空间就像两个完整的压缩副本的文件一样多。如果要添加的文件不太适合压缩(考虑jpg、png或zip文件),那么是的,这将占用大量空间。在某些时候,Git可能会决定打包您的存储库,在此处Git可能会决定在packfile内使用增量压缩(压缩和存储文件之间的差异)。但是,Git的其余部分并不认为这是一个抽象层,因为这是在Git内部文件访问之上的抽象层。如果实现得好,各种Git命令实现将仍然看到“未解压”的文件。现在,各种命令肯定会将其隐藏起来,因为大多数你使用的git命令,如果实现得好,都会将所有底层的抽象和优化从你这个开发者中隐藏起来,而是专注于你可能想要看到的内容。因此,如果您查看这些文件,则某些命令将显示差异,其中底层文件未被存储为差异,只是因为差异对您这个开发者来说更有意义。如果您改用plumbing命令,则将看到更多的blob。如果您想了解所有这些内容如何在实践中运作,只需要知道1个命令,即git cat-file -p SHA1。以下是测试此操作的方法:
  1. Initialize a new repository
  2. Add a file and commit it
  3. Execute git log and copy the SHA1 of the commit
  4. Execute git cat-file SHA1-of-commit and you will see something like this:

    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. Now make a copy of the SHA1 id after tree, this is the object id of the tree object, then execute git cat-file SHA1-of-tree-object, and you will see something like this:

    100644 blob 3b5d02884e6a17f20ed7938bf9e534f1bd0d195e    Temp.7z
    

    This tells you that the index contains 1 file (1 line), with the filename Temp.7z, and it tells you its SHA1 id. Copy this id.

  6. Execute git cat-file -p SHA1-of-blob and you will see the contents of the file you added.

Git的存储模型并不神奇或复杂,但其中存在很多优化和抽象,以避免浪费空间、去重等问题。


这非常有启发性 - 谢谢Lasse V. Karlsen。 - flow2k
这是最好的答案,伙计。 - Deepak Tatyaji Ahire

0

Git使用补丁或“hunks”来进行操作。它计算引入的差异并将其存储在两个版本之间。

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

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

简单来说,它比下面解释的要复杂得多,这样您就可以更容易地理解它。

一旦扫描了您的代码,git会搜索与上一个提交的更改,如果发现更改,则将旧更改拆分为“hunk”。
如果您在文件的中间添加了代码,则它将被拆分为3个“hunk”(顶部=旧代码,中间=新代码,底部=旧代码),现在您将有3个“hunk”。下次git扫描您的代码时,它将使用这3个“hunk”来搜索更改。

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

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


如果你想看到它的效果,请使用git add -p并选择s进行拆分。

enter image description here


补丁本身的样子如下所示:在此输入图片描述

如上所述,hunk是一个diff术语,以下是一些相关信息。 hunk是与diff相关的术语,以下是git如何在可视化(补丁)中显示它:

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

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


更多信息:

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


2
这是 git add -p 命令的输出,你看到的是一个格式化的补丁。如果你想要查看补丁本身,请在下一次提交时使用 git commit --verbose 命令,你将会看到精确的补丁。 - CodeWizard
7
从技术上讲,这里有两个不正确的地方:(1)每个快照在逻辑上是独立的,并且最初修改后的大型文本文件也是分开存储的(这样有点低效)。Git 不会存储差异。 (2) 当 Git 决定将对象压缩成包文件并将对象转换为增量压缩版本时,修改一个版本以产生另一个版本的指令不是文本差异块;它们是 Xdelta 的一种定制变体。 (https://en.wikipedia.org/wiki/Xdelta) - torek
3
抱歉,这与 Git 存储库中的数据完全不同。git add -p 提供的用户界面与实际的磁盘存储格式无关。据我所知,你所描述的“Hunk 指针”实际上并未出现在 Git 中。因此,出于这些原因,我必须对你的回答进行投票反对。 - Greg Hewgill
1
如果您想查看实际的存储对象,请不要使用 git show,而应改用 git cat-file -p。这样可以避免输出被篡改,因为它不会尝试猜测您可能想要查看什么,而是显示实际的底层文件(虽然它会解压缩文件,但不会进行差异或类似操作)。 - Lasse V. Karlsen
1
如果你遇到了这样的讨论:“git跟踪内容而不是文件”,那么你会得出同样的结论,即“断开连接”。实际上,Git跟踪的是文件,但是各种基于存储文件的Git命令将在文件之间进行分析。例如,如果git blame显示的行来自另一个文件,则完全是git blame的结果,而不是以任何方式存储在仓库中。 - Lasse V. Karlsen
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接