共计 10033 个字符,预计需要花费 26 分钟才能阅读完成。
简介: 通过一直地迭代,现在 Git 的性能越来越欠缺和弱小。然而 Git 的第一个提交源码仅约 1000 行,过后的 Git 实现了哪些性能?本文将从源码开始,剖析其核心思想,开掘背地优良的设计原理。
前言
Git 是目前世界上被最宽泛应用的古代软件版本管理系统(Version Control System)。Git 自身亦是一个成熟并处于沉闷开发状态的开源我的项目,明天惊人数量的软件我的项目依赖 Git 进行版本治理,这些我的项目包含开源以及各种商业软件。Git 在职业软件开发者中领有良好的名誉,Git 目前反对绝大多数的操作系统以及 IDE(Integrated Development Environments)。
Git 最后是由 Linux 操作系统内核的创造者 Linus Torvalds 在 2005 年发明,Git 第一个可用版本是 Linus 花了两周工夫用 C 写进去的。Git 第一个版本就实现了 Git 源码自托管,一个月之内,Linux 零碎的源码也曾经由 Git 治理了!
Git 的第一个提交源码仅有约 1000 行,然而曾经实现了 Git 的根本设计原理,比方初始化仓库、提交代码、查看代码 diff、读取提交信息等,Git 定义了三个区:工作区(workspace)、暂存区(index)、版本库(commit history),也实现了三类重要的 Git 对象:blob、tree、commit。本文将从源码上剖析 Git 的第一个提交并开掘背地优良的设计原理。
编译
获取源码
在 Github 上能够找到 Git 的仓库镜像:
https://github.com/git/git.git
# 获取 git 源码
$ git clone https://github.com/git/git.git
# 查看第一个提交
$ git log --date-order --reverse
commit e83c5163316f89bfbde7d9ab23ca2e25604af290
Author: Linus Torvalds <torvalds@ppc970.osdl.org>
Date: Thu Apr 7 15:13:13 2005 -0700
Initial revision of "git", the information manager from hell
# 变更为第一个提交,指定 commit-id
$ git reset --hard e83c5163316f89bfbde7d9ab23ca2e25604af290
文件构造
$ tree -h
.
├── [2.4K] cache.h
├── [503] cat-file.c # 查看 objects 文件
├── [4.0K] commit-tree.c # 提交 tree
├── [1.2K] init-db.c # 初始化仓库
├── [970] Makefile
├── [5.5K] read-cache.c # 读取以后索引文件内容
├── [8.2K] README
├── [986] read-tree.c # 读取 tree
├── [2.0K] show-diff.c # 查看 diff 内容
├── [5.3K] update-cache.c # 增加文件或目录
└── [1.4K] write-tree.c # 写入到 tree
# 统计代码行数,总共 1089 行
$ find . "(" -name "*.c" -or -name "*.h" -or -name "Makefile" ")" -print | xargs wc -l
...
1089 total
编译
编译第一个提交的 Git 会有编译问题,须要更改 Makefile 增加相干的依赖库:
$ git diff ./Makefile
...
-LIBS= -lssl
+LIBS= -lssl -lz -lcrypto
...
编译:
# 编译
$ make
只反对在 linux 平台上编译运行。
源码剖析
Write programs that do one thing and do it well.
——Unix philosophy
查看编译生成的可执行文件,总共有 7 个:
命令应用过程:
init-db:初始化仓库
命令阐明
$ init-db
运行流程
创立目录:.dircache。创立目录:.dircache/objects。在 .dircache/objects 中创立了从 00 ~ ff 共 256 个目录。.dircache/ 是 Git 的工作目录,最新版本的 Git 工作目录为 .git/。运行示例
# 运行 init-db 初始化仓库
$ init-db
defaulting to private storage area
# 查看初始化后的目录构造
$ tree . -a
.
└── .dircache # git 工作目录
└── objects # objects 文件
├── 00
├── 01
├── 02
├── ...... # 省略
├── fe
└── ff
258 directories, 0 files
最新版本 Git 应用 git init . 初始化仓库,而且初始化工作目录为 .git/,初始化后,.git/ 目录中的文件和性能也十分丰盛,包含 .git/HEAD、.git/refs/、.git/info/ 等,以及很多的 hooks 示例:.git/hooks/**.sample。
update-cache:增加文件或目录
update-cache 次要是把工作区的批改文件提交到暂存区。工作区、暂存区等阐明见下文【设计原理】。
命令应用
$ update-cache <file> ...
运行流程
读取并解析索引文件:.dircache/index。
遍历多个文件,读取并生成变更文件信息(文件名称、文件内容 sha1 值、日期、大小等),写入到索引文件中。
遍历多个文件,读取并压缩变更文件,存储到 objects 文件中,该文件为 blob 对象。
如果是刚初始化的仓库,会主动创立索引文件。索引文件阐明见下文【设计原理 – 索引文件】。blob 对象的文件格式及阐明见下文【设计原理 – blob 对象】。sha1 值阐明见下文【设计原理 – 哈希算法】。
运行示例
# 新增 README.md 文件
$ echo "hello git" > README.md
# 提交
$ update-cache README.md
# 查看索引文件
$ hexdump -C .dircache/index
00000000 43 52 49 44 01 00 00 00 01 00 00 00 af a4 fc 8e |CRID............|
00000010 5e 34 9d dd 31 8b 4c 8e 15 ca 32 05 5a e9 a4 c8 |^4..1.L...2.Z...|
00000020 af bd 4c 5f bf fb 41 37 af bd 4c 5f bf fb 41 37 |..L_..A7..L_..A7|
00000030 00 03 01 00 91 16 d2 04 b4 81 00 00 ee 03 00 00 |................|
00000040 ee 03 00 00 0a 00 00 00 bb 12 25 52 ab 7b 40 20 |..........%R.{@ |
00000050 b5 f6 12 cc 3b bd d5 b4 3d 1f d3 a8 09 00 52 45 |....;...=.....RE|
00000060 41 44 4d 45 2e 6d 64 00 |ADME.md.|
00000068
# 查看 objects 内容,sha1 值从索引文件中获取
$ cat-file bb122552ab7b4020b5f612cc3bbdd5b43d1fd3a8
temp_git_file_61uTTP: blob
$ cat ./temp_git_file_RwpU8b
hello git
cat-file:查看 objects 文件内容
cat-file 依据 sha1 值查看暂存区中的 objects 文件内容。cat-file 是一个辅助工具,在失常的开发工作流中个别不会应用到。
命令应用
$ cat-file <sha1>
运行流程
依据入参 sha1 值定位 objects 文件,比方 .dircache/objects/46/4b392e2c8c7d2d13d90e6916e6d41defe8bb6a
读取该 objects 文件内容,解压失去实在数据。
写入到临时文件 temp_git_file_XXXXXX(随机不反复文件)。
objects 内容为压缩格局,基于 zlib 压缩算法,objects 阐明见【设计原理 – objects 文件】。
运行示例
# cat-file 会把内容读取到 temp_git_file_rLcGKX
$ cat-file 82f8604c3652fa5762899b5ff73eb37bef2da795
temp_git_file_tBTXFM: blob
# 查看 temp_git_file_tBTXFM 文件内容
$ cat ./temp_git_file_tBTXFM
hello git!
show-diff:查看 diff 内容
查看工作区和暂存区中的文件差别。
命令应用
$ show-diff
运行流程
读取并解析索引文件:.dircache/index。
循环遍历变更文件信息,比拟工作区中的文件信息和索引文件中记录的文件信息差别。
无差别,显示 : ok。
有差别,调用 diff 命令输入差别内容。
运行示例
# 创立文件并提交到暂存区
$ echo "hello git!" > README.md
$ update-cache README.md
# 以后无差别
$ show-diff
README.md: ok
# 更改 README.md
$ echo "hello world!" > README.md
# 查看 diff
$ show-diff
README.md: 82f8604c3652fa5762899b5ff73eb37bef2da795
--- - 2020-08-31 17:33:50.047881667 +0800
+++ README.md 2020-08-31 17:33:47.827740680 +0800
@@ -1 +1 @@
-hello git!
+hello world!
write-tree:写入到 tree
write-tree 作用将保留在索引文件中的多个 objects 对象归并到一个类型为 tree 的 objects 文件中,该文件即 Git 中重要的对象:tree。
命令应用
$ write-tree
运行流程
读取并解析索引文件:.dircache/index。
循环遍历变更文件信息,依照指定格局编排变更文件信息及内容。
压缩并存储到 objects 文件中,该 object 文件为 tree 对象。
tree 对象的文件格式及相干阐明见下文【设计原理 – tree 对象】。
运行示例
# 提交
$ write-tree
c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
# 查看 objects 内容
$ cat-file c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
temp_git_file_r90ft5: tree
$ cat ./temp_git_file_r90ft5
100664 README.md��`L6R�Wb��_�>�{�-��
read-tree:读取 tree
read-tree 读取并解析指定 sha1 值的 tree 对象,输入变更文件的信息。
命令应用
$ read-tree <sha1>
运行步骤
解析 sha1 值。
读取对应 sha1 值的 object 对象。
输入变更文件的属性、门路、sha1 值。
运行示例
# 提交
$ write-tree
c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
# 读取 tree 对象
$ read-tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
100664 README.md (82f8604c3652fa5762899b5ff73eb37bef2da795)
commit-tree:提交 tree
commit-tree 把本地变更提交到版本库里,具体是基于一个 tree 对象的 sha1 值创立一个 commit 对象。
命令应用
$ commit-tree <sha1> [-p <sha1>]* < changelog
运行流程
参数解析。
获取用户名称、用户邮件、提交日期。
写入 tree 信息。
写入 parent 信息。
写入 author、commiter 信息。
写入 comments(正文)。
压缩并存储到 objects 文件中,该 object 文件为 commit 对象。
commit 对象的文件格式及阐明见下文【设计原理 – commit 对象】。
运行示例
# 写入到 tree
$ write-tree
c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
# 提交 tree
$ echo "first commit" > changelog
$ commit-tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72 < changelog
Committing initial tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
7ea820bd363e24f5daa5de8028d77d88260503d9
# 查看 commit 对象内容
$ cat-file 7ea820bd363e24f5daa5de8028d77d88260503d9
temp_git_file_CIfJsg: commit
$ cat temp_git_file_CIfJsg
tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
author Xiaowen Xia <chenan.xxw@aos-hw09> Tue Sep 1 10:56:16 2020
committer Xiaowen Xia <chenan.xxw@aos-hw09> Tue Sep 1 10:56:16 2020
first commit
设计原理
Write programs to work together.
——Unix philosophy
与传统的集中式版本控制系统(CVCS)相同,Git 从一开始就设计成了去中心化的分布式系统,每个开发者本地工作区都是一个残缺的版本库,领有本地的代码仓库。另外,Git 的设计初衷是为了让更多的开发者一起开发软件。
该版本 Git 定义了三种对象:
blob 对象:保留着文件快照。
tree 对象:记录着目录构造和 blob 对象索引。
commit 对象:蕴含着指向前述 tree 对象的指针和所有提交信息。
三种对象相互之间的关系如下:
另外,Git 也定义了三个区,工作区(workspace),暂存区(index)和版本库(commit history):
- 工作区(workspace):咱们间接批改代码的中央。
- 暂存区(index):数据临时寄存的区域,用于在工作区和版本库之间进行数据交换。
- 版本库(commit history):寄存曾经提交的数据。
每个可执行文件的具体分工是:init-db 用来创立一个初始化仓库,update-cache 会将 工作区 的变更写到 索引文件(index)中,write-tree 会将之前的所有变更整顿成 tree 对象,commit-tree 会将 指定的 tree 对象写到本地版本库中。另外,show-diff 用来查看 工作区 和 暂存区 中的文件差别,read-tree 用来读取 tree 对象 的信息。
由此能够绘制一个简略的 Git 开发工作流:
objects 文件
objects 文件是载体,用来存储 Git 中的 3 个重要对象:blob、tree、commit。
objects 文件的存储目录默认为.dircache/objects,也能够通过环境变量:SHA1_FILE_DIRECTORY 指定。文件门路和名称依据 sha1 值决定,取 sha1 值的第一个字节的 hex 值为目录,其余字节的 hex 值为名称,比方 sha1 值为:
0277ec89d7ba8c46a16d86f219b21cfe09a611e1
的对象文件存储门路为:
.dircache/objects/02/77ec89d7ba8c46a16d86f219b21cfe09a611e1
为了节约存储,同时也能存储多个信息,objects 文件内容都是通过 zlib 压缩过的。objects 文件的格局由 + + < 要存储的内容 > 组成,其中 能够是 ”blob”(blob 对象)、”tree”(tree 对象)、”commit”(commit 对象)。
应用 cat-file 能够查看 object 文件是什么类型的对象。
.dircache/objects 目录构造如下:
$ tree .git/objects
.git/objects
├── 02
│ └── 77ec89d7ba8c46a16d86f219b21cfe09a611e1
├── ...... # 省略
├── be
│ ├── adb5bac00c74c97da7f471905ab0da8b50229c
│ └── ee7b5e8ab6ae1c0c1f3cfa2c4643aacdb30b9b
├── ...... # 省略
├── c9
│ └── f6098f3ba06cf96e1248e9f39270883ba0e82e
├── ...... # 省略
├── cf
│ ├── 631abbf3c4cec0911cb60cc307f3dce4f7a000
│ └── 9e478ab3fc98680684cc7090e84644363a4054
├── ...... # 省略
└── ff
问:为什么 .dircache/objects/ 目录上面要以 sha1 值前一个字节的 hex 值作为子目录?
blob 对象
运行 update-cache 会生成 blob 对象。
blob 对象用于存储变更文件内容,其实就代表一个变更文件快照。blob 对象由 + + 拼装并压缩:
应用 cat-file 查看 blob 对象内容:
# 查看 blob 对象内容
$ cat-file 82f8604c3652fa5762899b5ff73eb37bef2da795temp_git_file_tBTXFM: blob
$ cat ./temp_git_file_tBTXFM
hello git!
tree 对象
运行 write-tree 会生成 tree 对象。
tree 对象用于存储多个提交文件的信息。tree 对象由 + + 文件模式 + 文件名称 + 文件 sha1 值 拼装并压缩:
文件 sha1 值 应用 binary 格局存储,占用 20 字节。
应用 cat-file 查看 tree 对象内容:
# 查看 tree 对象内容
$ cat-file c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
temp_git_file_r90ft5: tree
$ cat ./temp_git_file_r90ft5
100664 README.md��`L6R�Wb��_�>�{�-��
文件 sha1 值 应用 binary 格局存储,所以打印的时候会有乱码。
commit 对象
运行 commit-tree 会生成 commit 对象。
commit 对象存储一次提交的信息,包含所在的 tree 信息,parent 信息以及提交的作者等信息。commit 对象由 + + + * + + + 拼装并压缩:
tree sha1 值 和 parent sha1 值 应用 hex 字符串格局存储,占用 40 字节。
应用 cat-file 查看 commit 对象内容:
# 查看 commit 对象内容
$ cat-file 7ea820bd363e24f5daa5de8028d77d88260503d9
temp_git_file_CIfJsg: commit
$ cat temp_git_file_CIfJsg
tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
author Xiaowen Xia <chenan.xxw@aos-hw09> Tue Sep 1 10:56:16 2020
committer Xiaowen Xia <chenan.xxw@aos-hw09> Tue Sep 1 10:56:16 2020
first commit
索引文件
索引文件默认门路为:.dircache/index。索引文件用来存储变更文件的相干信息,当运行 update-cache 时会增加变更文件的信息到索引文件中。
同时也有一个叫 .dircache/index.lock 的文件,该文件存在时示意当前工作区被锁定,无奈进行提交操作。
应用 hexdump 命令能够查看到索引文件内容:
$ hexdump -C .dircache/index
00000000 43 52 49 44 01 00 00 00 01 00 00 00 ae 73 c4 f2 |CRID.........s..|
00000010 ce 32 c9 6f 13 20 0d 56 9c e8 cf 0d d3 75 10 c8 |.2.o. .V.....u..|
00000020 94 ad 4c 5f f4 5c 42 06 94 ad 4c 5f f4 5c 42 06 |..L_.B...L_.B.|
00000030 00 03 01 00 91 16 d2 04 b4 81 00 00 ee 03 00 00 |................|
00000040 ee 03 00 00 0b 00 00 00 a3 f4 a0 66 c5 46 39 78 |...........f.F9x|
00000050 1e 30 19 a3 20 42 e3 82 84 ee 31 54 09 00 52 45 |.0.. B....1T..RE|
00000060 41 44 4d 45 2e 6d 64 00 |ADME.md.|
.dircache/index 索引文件应用二进制存储相干内容,该文件由 文件头 + 变更文件信息 组成:
文件头大小为 32 字节,一个变更文件信息大小至多是 63 字节。其中:文件头中的 sha1 值由整个索引文件内容(文件头 + 变更文件信息)计算失去的。变更文件信息的 sha1 值由变更文件内容(压缩后)计算失去的。
哈希算法
该 Git 版本中应用的哈希算法为 sha1 算法,代码中应用的是 OpenSSL 库中提供的 sha1 算法。
目前 Git 曾经有了新的抉择:sha256 算法,且目前正在做 sha1 到 sha256 的迁徙。
#include <openssl/sha.h>
static int verify_hdr(struct cache_header *hdr, unsigned long size)
{
SHA_CTX c;
unsigned char sha1[20];
/* 省略 */
/* 计算索引文件头 sha1 值 */
SHA1_Init(&c);
SHA1_Update(&c, hdr, offsetof(struct cache_header, sha1));
SHA1_Update(&c, hdr+1, size - sizeof(*hdr));
SHA1_Final(sha1, &c);
/* 省略 */
return 0;
}
总结与思考
Use software leverage to your advantage.
——Unix philosophy
好的代码不是写进去的,是改进去的
Git 的第一个提交中,尽管实现了 Git 的分布式核心思想,以及三种对象,三个区等外围概念,然而 Git 的灵魂性能比方分支策略、近程仓库、日志零碎、git hooks 等性能都是前面逐渐迭代进去的。
对于细节
问:为什么 .dircache/objects/ 目录上面要以 sha1 值前一个字节的 hex 值作为子目录?
答:ext3 文件系统下,一个目录下只能有 32000 个一级子文件,如果都把 objects 文件存储到一个 .git/objects/ 目录里,很大概率会达到下限。同时要是一个目录下体面文件太多,那文件查找效率会升高很多。
对于代码品质
Git 的第一次提交源码,从代码品质、数据结构上看其实并没有多少参考价值,反而我还发现了很多能够优化的中央,比方:
- 异样解决不欠缺,经常出现段谬误(SegmentFault)。
- 存在几处内存透露的中央,比方 write-tree.c > main 函数 > buffer 内存块。
- 从索引文件中读取到的变更文件信息应用数组存储,波及到了比拟多的申请开释操作,性能上是有损失的,能够优化成链表存储。
不过这些都不重要,重要的是 Git 的设计原理和思维。
招聘
如果你是一个懂代码,爱 Git,有技术幻想的工程师,并想要和咱们一起打造世界 NO.1 的代码服务和产品,请分割我吧!C/C++/Golang/Java 咱们都要 (=´∀`)人(´∀`=)
If not now, when? If not me, who?
欢送投递简历到邮箱:chenan.xxw@alibaba-inc.com
参考资料
Git 官方网站:https://git-scm.com
Git 官网文档核心:https://git-scm.com/doc
Git 官网的 Git 底层原理介绍:Git Internals – Git Objects
zlib 官方网站:http://zlib.net
浅析 Git 存储—对象、打包文件及打包文件索引
(https://www.jianshu.com/p/923bf0485995))
深刻了解 Git – 所有皆 commit
(https://www.cnblogs.com/jasongrass/p/10582449.html))
深刻了解 Git – Git 底层对象(https://www.cnblogs.com/jasongrass/p/10582465.html))