代码托管从业者 Git 指南 (3)

在 Git 中,除了有 blob 对象,还有 commit ,tag,以及 tree ,commit 对象存储了用户的提交信息,tree 顾名思义,存储的是目录结构。下面是一个 commit 对象的内容:

tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 author Scott Chacon <schacon@gmail.com> 1243040974 -0700 committer Scott Chacon <schacon@gmail.com> 1243040974 -0700 First commit

下面是 tree 对象的内容:

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

解析松散对象非常容易,我们只需要使用能够解析 zlib 的库就可以完成这一操作,这里有一个例子可以参考 https://gist.github.com/fcharlie/2dde5a491d08dbc45c866cb370b9fa07。

想要了解更多的 Git 对象的细节可以参考: Git Internals - Git Objects。

站在文件系统的角度上看,数量巨大的小文件性能通常会急剧下降,而松散对象就是这样的小文件,Git 的解决方案是引入了打包文件,打包文件就是将多个松散对象依次存储到打包文件的存储空间之中,相关的布局如下:

代码托管从业者 Git 指南

Pack 文件的路径正则为 objects\/pack\/pack-[0-9a-f]{40}.pack$,当存储库使用 SHA256 哈希算法时,相应的路径正则为objects\/pack\/pack-[0-9a-f]{64}.pack$,Pack 文件的魔数是 'P','A','C','K',随后的 4 字节是版本信息,版本可以为 2,也可以为 3,后者是 SHA256 支持的前提。我们在读取 Pack 文件版本的时候需要注意,Git 使用网络字节序存储数据,也就是常说的大端,目前 Windows 全部使用小端字节序,macOS/iOS 等也是这样,Linux x86/AMD64 也是小端,ARM/ARM64 事实上也使用小端,使用大端的平台非常少。版本后紧接着是 4 字节的数字,用于表示这个包中有多少个 Git 对象,4 字节意味着单个 Pack 中最多只能有 232-1 个 Git 对象。接下来的事情就稍微复杂一些,Git 存储对象时使用 3-bit表示对象类型,(n-1)*7+4 bit 表示文件长度,这种机制主要是支持大于 4G 的文件和支持 OBJ_OFS_DELTA ,也就是说,尽管 Git 是基于快照的,但是在 pack 文件中,我们依然可以看到一些对象使用差异存储,这样的好处是节省空间,坏处就是查看对象复杂度上升,因此,Git 会倾向于将历史久远的用 OBJ_OFS_DELTA 存储,以降低影响,不管怎么说,都是权衡利弊,保证存储和读取的平衡。最后是 20 字节的 checksum SHA1,当然如果是 SHA-256 存储库,则需要使用 SHA-256 计算 checksum。

上图一目了然,如果没有其他措施,我们要在 Pack 文件中查找某个对象是非常难的,所幸这个问题一开始就被重视了,在 Pack 文件的同级目录下存在文件后缀名为 .idx 的文件,就是 Pack Index,其布局如下:

代码托管从业者 Git 指南

版本 1 的 Pack-Index 现在已经很难见到,原因很简单,不支持 Pack 文件大于 4 GB,版本 2 格式非常有趣,魔数为 '\377','t','O','c',第二个 4 字节就是版本信息,随后是 256 * 4 的扇区表,0~254 分别表示前缀从 0x00~0xFE 的对象数量,而 fanout[255] 则表示所有对象的数量,随后对象 ID 按字典排序到 sha listing,紧接着是相应的 crc checksums,然后是 packfile offsets,packfile offsets 是 4 字节的,这并不能支持 Pack 大于 4 GB。而后续的 large packfile offsets 则支持了 Pack 大于 4 GB。当 4byte offset 最高位是 1 时说明需要从 large packfile offsets 读取长度。

Pack Index 文件很好的解决了 Pack 文件的随机读取的问题,按照其特性,我们在查找 Git 对象时,使用二分法查找,最多 8 次就可以在找到对象在 Pack 中的偏移,进一步读取文件。

但如果 Pack 文件数量特别多时,还是会遇到查找对象性能较多,微软在将 Windows 源码迁移到 Git 后也遇到了这个问题,后来在微软工程师的努力下,multi-pack-index(MIDX)出现了,存在多个 Pack 文件时,MIDX 便可以加快 Git 对象的查找。

既然我们已经对 Git 的存储有了个简单的认识,那么要找到某个文件也不在话下,分支对应了一个提交,提交有一个 ID,我们可以在松散对象或者打包对象中找到该 ID,然后获得提交的内容,找到 tree 后,按照路径一级级往下找,找到路径匹配的 blob,该 blob 解压后的内容就是文件的原始内容,一个简单的流程如下:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpdjwp.html