2023-01-11
Git
如果你了解过一些 Git 底层原理,就会知道 Git 会在仓库的 .git/objects
目录中存储各种对象(Blob、Tree 和 Commit)。但 Git 是如何存储这些对象的呢?
本文将尝试用 Python 完成 Blob 对象的创建和保存,由此我们就可以知道 Git 是怎么存储 Blob 对象的。
一、先用 Git 创建一个最简单的 Blob
我们先创建一个最简单的仓库,里面存储一个文件,看看 Git 会在 .git/objects
中创建什么东西吧!
创建一个 hello
仓库,里面新建一个 hello.txt
,并将其添加到暂存区:
$ mkdir hello
$ cd hello
$ git init
Initialized empty Git repository in /Users/guoshuai/Documents/repos/hello/.git/
$ find .git/objects -type f
此时还没有存储任何对象
$ echo 'Hello world!' > hello.txt
$ git add hello.txt
$ find .git/objects -type f
.git/objects/cd/0875583aabe89ee197ea133980a9085d08e497
可以看到,此时 Git 在 .git/objects
目录中存储了一个文件,这个就是 hello.txt
文件内容所对应的 Blob 对象的文件形式了。
用 file
命令查看其文件类型,发现是 zlib 压缩后的文件:
$ file .git/objects/cd/0875583aabe89ee197ea133980a9085d08e497
.git/objects/cd/0875583aabe89ee197ea133980a9085d08e497: zlib compressed data
我们计算一下它的 MD5,得到:
$ md5sum .git/objects/cd/0875583aabe89ee197ea133980a9085d08e497
b2ba11b81d81fd634f33befa5b166a6a .git/objects/cd/0875583aabe89ee197ea133980a9085d08e497
记住这个 MD5,因为接下来我们将使用 Python 手动创建一个这样的对象,存储到文件中,看看能不能得到和 Git 一样的结果。
二、使用 Python 手动创建对象
首先,还是创建一个空的仓库:
$ mkdir my-hello
$ cd my-hello
$ git init
Initialized empty Git repository in /Users/guoshuai/Documents/repos/my-hello/.git/
$ find .git/objects -type f
此时对象目录为空
启动一个 Python 交互式命令行,依次执行:
-
创建文件内容
注意:
echo
命令会自动添加一个 newline 字符,所以我们在这里也加上,让两者内容相同:>>> content = 'Hello world!\n'
-
创建文件头,Blob 对象的文件头格式为:
对象类型(blob)+ 1 个空格 + 内容字节数 + 1 个空字节
>>> header = f'blob {len(content)}\0' >>> header 'blob 13\x00'
-
将文件头和文件内容拼接,得到真正要存储的内容:
>>> store = header + content >>> store 'blob 13\x00Hello world!\n'
并对字符串
store
进行编码,得到字节串store_bytes
:>>> store_bytes = store.encode('utf-8') >>> store_bytes b'blob 13\x00Hello world!\n'
-
引入 Python 哈希库,使用 SHA1 计算文件内容字节串的哈希值字符串:
>>> import hashlib >>> sha1 = hashlib.sha1() >>> sha1.update(store_bytes) >>> hash_str = sha1.hexdigest() >>> hash_str 'cd0875583aabe89ee197ea133980a9085d08e497'
这个哈希值字符串将作为对象的 key(对象的存储路径),即对象最终会被存储到
.git/objects/cd/0875583aabe89ee197ea133980a9085d08e497
>>> obj_file_dir = '.git/objects/' + hash_str[:2] >>> obj_file_dir '.git/objects/cd' >>> obj_file_path = obj_file_path = obj_file_dir + '/' + hash_str[2:] >>> obj_file_path '.git/objects/cd/0875583aabe89ee197ea133980a9085d08e497'
-
引入 zlib,对
store_bytes
进行压缩,我们最终要存储的是压缩后的内容>>> import zlib, os >>> os.mkdir(obj_file_dir) >>> store_compressed = zlib.compress(store_bytes, level=1) >>> with open(obj_file_path, 'wb') as f: f.write(store_compressed)
文件保存后,退出 Python 命令行,检查新保存的文件的 MD5:
$ md5sum .git/objects/cd/0875583aabe89ee197ea133980a9085d08e497
b2ba11b81d81fd634f33befa5b166a6a .git/objects/cd/0875583aabe89ee197ea133980a9085d08e497
可以看到,和前面 Git 生成的文件的 MD5 值是一样的,说明我们用 Python 保存的 Blob 对象和 Git 保存的 Blob 对象是完全一致的。
三、总结
至此,我们就很清楚 Git 是如何构造和存储 Blob 对象的了。即:
- 从文件内容(content)创建文件头(header);
- 将文件内容和文件头拼起来,得到要存储的文件内容;
- 将拼接后的文件内容用 SHA1 计算哈希值(字符串),得到此对象的 key,同时也是存储路径;
- 存储文件前用 zlib 对其进行压缩;
- 将压缩后的内容存储到 key 指向的路径。
四、启示
从上面可以看到,Git 将内存存储为 Blob 对象的时候,其存储路径由内容(加文件头)的哈希值决定。因此,相同的内容,其存储路径也相同,即只会存储一份。
例如,我们有两个相同的文件 hello.txt
和 another-hello.txt
,都添加到暂存区:
将现有的 hello.txt(已暂存)拷贝一份,命名为 another-hello.txt
$ cp hello.txt another-hello.txt
将 another-hello.txt 暂存
$ git add another-hello.txt
此时查看 Git 存储了哪些 Blob,发现只有一个 Blob 对象
$ find .git/objects -type f
.git/objects/cd/0875583aabe89ee197ea133980a9085d08e497
所以可以说,Git 不会存储重复的内容。
五、踩坑记录
熟悉的读者可以早已看出,本文是参考了 Pro Git 2 的第 10 章。它使用 Ruby 完成了这个过程,我就想着尝试用 Python 试一试。第一次尝试失败了,原因是使用 zlib 压缩的时候,Python 的 zlib.compress
默认的压缩等级和 Git 采取的压缩等级不同,导致保存下来的文件的 magic number 不一样,最后对文件计算 MD5 就有不同的结果。所以这里明确指定 zlib.compress
的 level
参数为 1
,意思是「速度最快」,也就是 Git 中采取的压缩等级。