阿里妹导读:经过不断地迭代,如今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 –reversecommit e83c5163316f89bfbde7d9ab23ca2e25604af290Author: Linus Torvalds <t[email protected]>Date: Thu Apr 7 15:13:13 2005 -0700
Initial revision of "git", the information manager from hell
# 调换为第一个提交,指定commit-id$ git checkout 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-dbdefaulting to private storage area
# 检察初始化后的名目结构$ tree . -a.└── .dircache # git工作名目 └── objects # objects文献 ├── 00 ├── 01 ├── 02 ├── …… # 省略 ├── fe └── ff258 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/index00000000 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 bb122552ab7b4020b5f612cc3bbdd5b43d1fd3a8temp_git_file_61uTTP: blob$ cat ./temp_git_file_RwpU8bhello 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 82f8604c3652fa5762899b5ff73eb37bef2da795temp_git_file_tBTXFM: blob
# 检察 temp_git_file_tBTXFM 文献实质$ cat ./temp_git_file_tBTXFM hello git!
show-diff:检察diff实质
检察工作区和暂存区中的文献差异。命令使用$ show-diff运转流程读取并解析索引文献:.dircache/index。循环遍历调换文献信息,比较工作区中的文献信息和索引文献中记录的文献信息差异。无差异,显示 <file-name>: ok。有差异,调用 diff 命令输出差异实质。运转示例# 创建文献并提交到暂存区$ echo "hello git!" > README.md$ update-cache README.md
# 当前无差异$ show-diffREADME.md: ok
# 更改README.md$ echo "hello world!" > README.md
# 检察diff$ show-diffREADME.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-treec771b3ab2fe3b7e43099290d3e99a3e8c414ec72
# 检察objects实质$ cat-file c771b3ab2fe3b7e43099290d3e99a3e8c414ec72temp_git_file_r90ft5: tree$ cat ./temp_git_file_r90ft5100664 README.md��`L6R�Wb��_�>�{�-��
read-tree:读取tree
read-tree 读取并解析指定sha1值的tree东西,输出调换文献的信息。命令使用$ read-tree <sha1>运转步骤解析sha1值。读取对应sha1值的object东西。输出调换文献的属性、路径、sha1值。运转示例# 提交$ write-treec771b3ab2fe3b7e43099290d3e99a3e8c414ec72
# 读取tree东西$ read-tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72100664 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 < changelogCommitting initial tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec727ea820bd363e24f5daa5de8028d77d88260503d9
# 检察commit东西实质$ cat-file 7ea820bd363e24f5daa5de8028d77d88260503d9temp_git_file_CIfJsg: commit$ cat temp_git_file_CIfJsgtree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72author Xiaowen Xia <chenan.xxw@aos-hw09> Tue Sep 1 10:56:16 2020committer 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 东西的指针和所有提交信息。三种东西相互之间的关系如下:图源:https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell另外,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文献的格式由 <type> + <size> + <要保存的实质> 组成,其中 <type> 可以是"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 东西由<type> + <size>+ <file-content> 拼装并压缩:使用 cat-file 检察 blob 东西实质:# 检察 blob 东西实质$ cat-file 82f8604c3652fa5762899b5ff73eb37bef2da795temp_git_file_tBTXFM: blob
$ cat ./temp_git_file_tBTXFM hello git!
tree 东西
运转 write-tree 会生成 tree 东西。tree 东西用于保存多个提交文献的信息。tree 东西由 <type> + <size> + 文献模式 + 文献名称 + 文献sha1值 拼装并压缩:文献sha1值 使用binary格式保存,占用20字节。使用 cat-file 检察 tree 东西实质:# 检察 tree 东西实质$ cat-file c771b3ab2fe3b7e43099290d3e99a3e8c414ec72temp_git_file_r90ft5: tree
$ cat ./temp_git_file_r90ft5100664 README.md��`L6R�Wb��_�>�{�-��文献sha1值 使用binary格式保存,所以打印的时候会有乱码。
commit 东西
运转 commit-tree 会生成 commit 东西。commit 东西保存一次提交的信息,包括所在的tree信息,parent信息以及提交的作者等信息。commit 东西由<type> + <size> + <tree, sha1> + <parent, sha1>* + <author-info> + <committer-info> + <comment> 拼装并压缩:tree sha1值 和 parent sha1值 使用hex字符串格式保存,占用40字节。使用 cat-file 检察 commit 东西实质:# 检察 commit 东西实质$ cat-file 7ea820bd363e24f5daa5de8028d77d88260503d9temp_git_file_CIfJsg: commit
$ cat temp_git_file_CIfJsgtree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72author Xiaowen Xia <chenan.xxw@aos-hw09> Tue Sep 1 10:56:16 2020committer 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?欢迎投递简历到邮箱:[email protected]
参考资料
Git官方网站:https://git-scm.comGit官方文档中心:https://git-scm.com/docGit官网的Git底层原理介绍:Git Internals – Git Objectszlib 官方网站: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)