Git 是如何存储文件的
01、存储方式
Git 从核心上来看不过是简单地存储键值对(key-value)。它允许插入任意类型的内容,并会返回一个键值,通过该键值可以在任何时候再取出该内容。
Git 存储数据内容的方式,为每份内容生成一个文件,取得该内容与头信息的 SHA-1 校验和,创建以该校验和前两个字符为名称的子目录,并以 (校验和) 剩下 38 个字符为文件命名 (保存至子目录下)。
写入对象
$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
取回对象
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
返回对象的类型
$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
02、Git 对象
数据对象 blob object
树对象 tree object
解决文件名的保存问题,允许我们将多个文件组织到一起,Git 以一种类似于 UNIX 文件系统的方式存储内容,但作了些许简化。
所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容。 一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。
$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README
100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib (这是一个文件夹)
你可以自己创建 tree 。通常 Git 根据你的暂存区域或 index 来创建并写入一个 tree 。因此要创建一个 tree 对象的话首先要通过将一些文件暂存从而创建一个 index 。
$ git write-tree
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
commit 对象
commit 对象有格式很简单:指明了该时间点项目快照的顶层树对象、作者/提交者信息(从 Git 设置的 user.name 和 user.email中获得)以及当前时间戳、一个空行,以及提交注释信息。
每一个 commit 对象都指向了你创建的树对象快照。
当你执行git add, git commit, Git其实就是保存修改了的文件的 blob,更新索引,创建 tree 对象,最后创建 commit 对象,这些 commit 对象指向了顶层 tree 对象以及先前的 commit 对象。
这三类 Git 对象 ── blob,tree 以及 commit ── 都各自以文件的方式保存在 .git/objects 目录下。
03、对象存储
Git是如何存储对象的?
构造文件头
对象类型+空格+数据内容的长度+空字节(null byte)
>> header = "blob #{content.length}\0"
=> "blob 16\000"
计算SHA-1 校验和
Git 将文件头与原始数据内容拼接起来,并计算拼接后的新内容的 SHA-1 校验和:
>> store = header + content
=> "blob 16\000what is up, doc?"
>> require 'digest/sha1'
=> true
>> sha1 = Digest::SHA1.hexdigest(store)
=> "bd9dbf5aae1a3862dd1526723246b20206e5fc37"
用 zlib 对数据内容进行压缩
在 Ruby 中可以用 zlib 库来实现。首先需要导入该库,然后用 Zlib::Deflate.deflate() 对数据进行压缩:
>> require 'zlib'
=> true
>> zlib_content = Zlib::Deflate.deflate(store)
=> "x\234K\312\311OR04c(\317H,Q\310,V(-\320QH\311O\266\a\000_\034\a\235"
将用 zlib 压缩后的内容写入磁盘
需要指定保存对象的路径 (SHA-1 值的头两个字符作为子目录名称,剩余 38 个字符作为文件名保存至该子目录中)。
>> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
=> ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37"
>> require 'fileutils'
=> true
>> FileUtils.mkdir_p(File.dirname(path))
=> ".git/objects/bd"
>> File.open(path, 'w') { |f| f.write zlib_content }
=> 32
所有的 Git 对象都以这种方式存储,惟一的区别是类型不同 ── 除了字符串 blob,文件头起始内容还可以是 commit 或 tree 。不过虽然 blob 几乎可以是任意内容,commit 和 tree 的数据却是有固定格式的。