提交、树和blob的哈希如何计算?

3
我对 SHA-1 哈希如何为提交、树和数据块计算感到困惑。根据这篇文章,提交哈希是基于以下因素计算的:
  1. 提交的源树(展开为所有子树和数据块)
  2. 父提交SHA1
  3. 作者信息
  4. 提交者信息(没错,这些是不同的!)
  5. 提交消息
树和数据块的哈希是否也涉及相同的因素?

不,Blob(二进制大对象)没有树、父提交、作者、提交者或信息。树也没有父提交、作者、提交者或信息。 - Ry-
打个反方,如果使用了一种算法使得发生碰撞的可能性非常小,SHA-1哈希是如何计算的对你有什么影响呢? - Tim Biegeleisen
@Ryan,树确实有树(目录)和 blob,但没有父级、作者、提交者或消息。 - appu
@TimBiegeleisen 对我来说,真正理解git命令如何影响对象存储是很重要的。在我看来,如果一个人不知道事物是如何形成的,那么你就无法真正放心使用它,至少我是这样认为的。 - appu
@appu:复制粘贴错误,抱歉。 - Ry-
请参见 https://github.com/chris3torek/scripts/blob/master/githash.py(以与Git相同的方式计算文件和树的哈希值)。 - torek
2个回答

12

Git有时被称为“内容可寻址的文件系统”。哈希值是地址,它们基于各种对象的内容。因此,为了知道哈希值基于什么,我们只需要知道各种对象的内容。

Blobs

Blob就是一串八位字节流,没有其他东西。类似于Unix文件系统中的文件内容概念。

因此,blob的哈希值仅基于其内容,blob没有元数据。

Trees

Tree将名称和权限与其他对象(blob或tree)关联起来。树只是一个由四元组(权限、类型、哈希、名称)列表组成的集合。例如,一个树可能看起来像这样:

100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README
100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib

注意第三个条目是一个树形结构。

在Unix文件系统中,树形结构类似于一个“目录特殊文件”。

同样,哈希值基于树的内容,这意味着它取决于其叶子节点的名称、权限、类型和哈希值。

提交

提交记录了某个时间点上树形结构的快照以及一些元数据以及快照的生成方式。提交包括:

  • (任意数量的)父提交哈希值列表(包括零个)
  • 树的哈希值
  • 提交消息
  • 提交元数据(提交日期和提交者姓名)
  • 创作者元数据(创作日期和创作者姓名)

提交的哈希值基于这些内容计算得出。

标签

与上面的对象不同,标签并不是对象。他们不是对象存储的一部分,也没有哈希值。它们是对象的引用。(注意:不仅限于提交可以被打标签,但这是正常使用情况)。

注释标签

注释标签不同:它是对象存储的一部分。

一个注释标签包含:

  • 一个提交的哈希值
  • 标签消息
  • 标记元数据(标记者姓名和标记日期)

与所有其他对象一样,哈希值是基于它们计算得出的。

签名标签

签名标签类似于注释标签,但添加了一个加密签名。

笔记

笔记允许你将任意提交与任意Git对象关联起来。

笔记的存储方式稍微复杂一些。实际上,一个笔记只是一个提交(包含一个树形结构,其中包含笔记内容的Blob)。Git为笔记创建了一个特殊的分支,笔记提交与其“带注释对象”的关联就发生在那里。我不熟悉具体的细节。

然而,由于笔记只是一个提交,而且关联关系外部产生,所以笔记的哈希值与任何其他提交相同。


存储格式

存储格式包含一个简单的头信息。实际存储(和哈希)的内容是头信息后跟空字节(NULL octet)再后面是对象内容。

头信息包含对象内容的类型和长度,以ASCII编码表示。因此,包含ASCII编码的字符串“Hello, World”的Blob看起来像这样:

blob 12\0Hello, World

并且就是被哈希和存储的内容。

其他类型的对象具有更结构化的格式,因此树形对象将以标题tree <content长度(以八进制表示)>\0开头,后跟严格定义、结构化、序列化的树形表示。

提交也是一样的。

大多数格式都是基于简单ASCII的文本格式。例如,大小不是编码为二进制整数,而是作为十进制整数进行编码,每个数字都用相应的ASCII字符表示。

压缩

计算哈希值后,包括标头的字节流使用zlib-deflate进行压缩,并将生成的字节流存储在基于哈希值的文件中;默认情况下存储在目录中。

.git/objects/<first two characters of the hash>/<remaining hash>

存储包

上述存储格式称为散装对象格式(loose object format),因为每个对象都被单独存储。还有一种更高效的存储格式(也用作网络传输格式),称为打包文件(packfile)

打包文件是一个重要的速度和存储优化,但它们相当复杂,因此我不会详细描述它们。

简单来说,打包文件由所有未压缩的对象连接成的单个文件和第二个文件组成,该文件包含索引,指示包文件中各个对象所在的位置。然后整个打包文件被压缩,这样可以获得更好的压缩比,因为算法可以发现对象之间的冗余,而不仅限于单个对象内部的冗余。(例如,如果您有两个几乎相同的 blob 版本,则这是 SCM 中的常态。)

它不使用 zlib-deflate,而是使用二进制 delta 压缩算法。它还使用某些启发式算法来确定如何将对象放置在打包文件中,以便相似性很大的对象彼此靠近。(delta 算法实际上不能完全看到整个打包文件,否则会消耗太多内存,而是在打包文件上滑动一个窗口运行;这些启发式算法试图确保类似的对象落在同一个窗口内。) 其中一些规则是:查看树关联的 blob 的名称,并尝试将具有相同名称的 blob 放置在靠近一起的位置,尝试将拥有相同文件扩展名的对象放置在靠在一起的位置,尝试使后续版本彼此靠近等等。

查看

散装(即未打包)对象只需使用 zlib 解压缩即可。解压后查看它们的结构。请注意,未压缩的字节流 恰好就是被哈希的内容;这些对象在压缩之前被存储为压缩格式并进行了哈希。

这里有一个简单的 Perl 单行命令可以对流进行解压缩(也称作膨胀):

perl -MCompress::Zlib -e 'undef $/; print uncompress(<>)'

一个小修正:笔记是作为提交存储的,其中包含了文件树。refs/notes/commits 指向链中最顶部的提交。与此最顶部提交相关联的树有一个文件对应每个具有笔记的提交:例如,如果有一个针对提交 badf00... 的注释,则在“尖端注释”中会有一个名为 ba/df00...ba/df/00... 等文件。该文件(一个 blob)包含了提交 badf00... 的注释。这里的扇出度取决于被注释的提交数量。 - torek
1
一个标签不需要指向一个提交。任何 Git 对象都可以被打上标签。 - ElpieKay

3

我认为了解每种git对象的最佳方法是自己探索。

您可以使用以下命令轻松完成:

git cat-file -p <a_sha1>

从一个提交的sha1开始。你会得到树的sha1,取其中之一并应用相同的命令一直向下,以blob结尾。

每次都会看到存储在git对象数据库中的内容。

你需要知道的唯一其他事项是,内容由对象类型、内容长度前缀和压缩组成。


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