源码解析:Git的第一个提交是什么样的?

开发 开发工具
Git 本身亦是一个成熟并处于活跃开发状态的开源项目,今天惊人数量的软件项目依赖 Git 进行版本管理,这些项目包括开源以及各种商业软件。

[[342934]]

 前言

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

  1. # 获取 git 源码 
  2. $ git clone https://github.com/git/git.git 
  3.  
  4. # 查看第一个提交 
  5. $ git log --date-order --reverse 
  6. commit e83c5163316f89bfbde7d9ab23ca2e25604af290 
  7. Author: Linus Torvalds <torvalds@ppc970.osdl.org> 
  8. Date:   Thu Apr 7 15:13:13 2005 -0700 
  9.  
  10.     Initial revision of "git", the information manager from hell 
  11.  
  12. # 变更为第一个提交,指定commit-id 
  13. $ git reset --hard e83c5163316f89bfbde7d9ab23ca2e25604af290 

文件结构

  1. $ tree -h 
  2. ├── [2.4K]  cache.h 
  3. ├── [ 503]  cat-file.c                  # 查看objects文件 
  4. ├── [4.0K]  commit-tree.c               # 提交tree 
  5. ├── [1.2K]  init-db.c                   # 初始化仓库 
  6. ├── [ 970]  Makefile 
  7. ├── [5.5K]  read-cache.c                # 读取当前索引文件内容 
  8. ├── [8.2K]  README 
  9. ├── [ 986]  read-tree.c                 # 读取tree 
  10. ├── [2.0K]  show-diff.c                 # 查看diff内容 
  11. ├── [5.3K]  update-cache.c              # 添加文件或目录 
  12. └── [1.4K]  write-tree.c                # 写入到tree 
  13.  
  14. # 统计代码行数,总共1089行 
  15. $ find . "(" -name "*.c" -or -name "*.h" -or -name "Makefile" ")" -print | xargs wc -l 
  16.  ... 
  17.  1089 total 

编译

编译第一个提交的Git会有编译问题,需要更改Makefile添加相关的依赖库:

  1. $ git diff ./Makefile 
  2. ...  
  3. -LIBS= -lssl 
  4. +LIBS= -lssl -lz -lcrypto 
  5. ... 

编译:

  1. # 编译 
  2. $ make 

只支持在 linux 平台上编译运行。

源码分析

Write programs that do one thing and do it well. ——Unix philosophy

查看编译生成的可执行文件,总共有7个:


命令使用过程:


init-db:初始化仓库

命令说明

  1. $ init-db 

运行流程

创建目录:.dircache。

创建目录:.dircache/objects。

在 .dircache/objects 中创建了从 00 ~ ff 共256个目录。

.dircache/ 是Git的工作目录,最新版本的Git工作目录为 .git/ 。

运行示例

  1. # 运行init-db初始化仓库 
  2. $ init-db 
  3. defaulting to private storage area 
  4.  
  5. # 查看初始化后的目录结构 
  6. $ tree . -a 
  7. └── .dircache                   # git工作目录 
  8.     └── objects                 # objects文件 
  9.         ├── 00 
  10.         ├── 01 
  11.         ├── 02 
  12.         ├── ......              # 省略 
  13.         ├── fe 
  14.         └── ff 
  15. 258 directories, 0 files 

最新版本Git使用 git init . 初始化仓库,而且初始化工作目录为 .git/,初始化后,.git/ 目录中的文件和功能也非常丰富,包括 .git/HEAD、.git/refs/ 、.git/info/ 等,以及很多的 hooks 示例:.git/hooks/**.sample。

update-cache:添加文件或目录

update-cache 主要是把工作区的修改文件提交到暂存区。工作区、暂存区等说明见下文【设计原理】 。

命令使用

  1. update-cache ... 

运行流程

  1. 读取并解析索引文件 :.dircache/index。
  2. 遍历多个文件,读取并生成变更文件信息(文件名称、文件内容sha1值、日期、大小等),写入到索引文件中。
  3. 遍历多个文件,读取并压缩变更文件,存储到objects文件中,该文件为blob对象。

如果是刚初始化的仓库,会自动创建索引文件。索引文件说明见下文【设计原理 - 索引文件】。blob对象的文件格式及说明见下文【设计原理 - blob对象】。sha1值说明见下文【设计原理 - 哈希算法】。

运行示例

  1. # 新增README.md文件 
  2. $ echo "hello git" > README.md 
  3.  
  4. # 提交 
  5. update-cache README.md 
  6.  
  7. # 查看索引文件 
  8. $ hexdump -C .dircache/index 
  9. 00000000  43 52 49 44 01 00 00 00  01 00 00 00 af a4 fc 8e  |CRID............| 
  10. 00000010  5e 34 9d dd 31 8b 4c 8e  15 ca 32 05 5A e9 a4 c8  |^4..1.L...2.Z...| 
  11. 00000020  af bd 4c 5f bf fb 41 37  af bd 4c 5f bf fb 41 37  |..L_..A7..L_..A7| 
  12. 00000030  00 03 01 00 91 16 d2 04  b4 81 00 00 ee 03 00 00  |................| 
  13. 00000040  ee 03 00 00 0a 00 00 00  bb 12 25 52 ab 7b 40 20  |..........%R.{@ | 
  14. 00000050  b5 f6 12 cc 3b bd d5 b4  3d 1f d3 a8 09 00 52 45  |....;...=.....RE| 
  15. 00000060  41 44 4d 45 2e 6d 64 00                           |ADME.md.| 
  16. 00000068 
  17.  
  18. # 查看objects内容,sha1值从索引文件中获取 
  19. $ cat-file bb122552ab7b4020b5f612cc3bbdd5b43d1fd3a8 
  20. temp_git_file_61uTTP: blob 
  21. $ cat ./temp_git_file_RwpU8b 
  22. hello git 

cat-file:查看objects文件内容

cat-file 根据sha1值查看暂存区中的objects文件内容。cat-file 是一个辅助工具,在正常的开发工作流中一般不会使用到。

命令使用

 

  1. $ cat-file <sha1> 

运行流程

  1. 根据入参sha1值定位objects文件,比如.dircache/objects/46/4b392e2c8c7d2d13d90e6916e6d41defe8bb6a
  2. 读取该objects文件内容,解压得到真实数据。
  3. 写入到临时文件 temp_git_file_XXXXXX(随机不重复文件)。

objects内容为压缩格式,基于zlib压缩算法,objects说明见【设计原理 - objects 文件】。

运行示例

  1. # cat-file 会把内容读取到temp_git_file_rLcGKX 
  2. $ cat-file 82f8604c3652fa5762899b5ff73eb37bef2da795 
  3. temp_git_file_tBTXFM: blob 
  4.  
  5. # 查看 temp_git_file_tBTXFM 文件内容 
  6. $ cat ./temp_git_file_tBTXFM  
  7. hello git! 

show-diff:查看diff内容

查看工作区和暂存区中的文件差异。

命令使用

  1. $ show-diff 

运行流程

读取并解析索引文件:.dircache/index。

循环遍历变更文件信息,比较工作区中的文件信息和索引文件中记录的文件信息差异。

无差异,显示 : ok。

有差异,调用 diff 命令输出差异内容。

运行示例

  1. # 创建文件并提交到暂存区 
  2. $ echo "hello git!" > README.md 
  3. update-cache README.md 
  4.  
  5. # 当前无差异 
  6. $ show-diff 
  7. README.md: ok 
  8.  
  9. # 更改README.md 
  10. $ echo "hello world!" > README.md 
  11.  
  12. # 查看diff 
  13. $ show-diff 
  14. README.md:  82f8604c3652fa5762899b5ff73eb37bef2da795 
  15. --- -   2020-08-31 17:33:50.047881667 +0800 
  16. +++ README.md   2020-08-31 17:33:47.827740680 +0800 
  17. @@ -1 +1 @@ 
  18. -hello git! 
  19. +hello world! 

write-tree:写入到tree

write-tree 作用将保存在索引文件中的多个objects对象归并到一个类型为tree的objects文件中,该文件即Git中重要的对象:tree。

命令使用

  1. $ write-tree 

运行流程

  1. 读取并解析索引文件:.dircache/index。
  2. 循环遍历变更文件信息,按照指定格式编排变更文件信息及内容。
  3. 压缩并存储到objects文件中,该object文件为tree对象。

tree对象的文件格式及相关说明见下文【设计原理 - tree对象】。

运行示例

  1. # 提交 
  2. $ write-tree 
  3. c771b3ab2fe3b7e43099290d3e99a3e8c414ec72 
  4.  
  5. # 查看objects内容 
  6. $ cat-file  c771b3ab2fe3b7e43099290d3e99a3e8c414ec72 
  7. temp_git_file_r90ft5: tree 
  8. $ cat ./temp_git_file_r90ft5 
  9. 100664 README.md��`L6R�Wb��_�>�{�-�� 

read-tree:读取tree

read-tree 读取并解析指定sha1值的tree对象,输出变更文件的信息。

命令使用

 

  1. read-tree <sha1> 

运行步骤

  1. 解析sha1值。
  2. 读取对应sha1值的object对象。
  3. 输出变更文件的属性、路径、sha1值。

运行示例

  1. # 提交 
  2. $ write-tree 
  3. c771b3ab2fe3b7e43099290d3e99a3e8c414ec72 
  4.  
  5. # 读取tree对象 
  6. read-tree  c771b3ab2fe3b7e43099290d3e99a3e8c414ec72 
  7. 100664 README.md (82f8604c3652fa5762899b5ff73eb37bef2da795) 

commit-tree:提交tree

commit-tree 把本地变更提交到版本库里,具体是基于一个tree对象的sha1值创建一个commit对象。

命令使用

  1. commit-tree <sha1> [-p <sha1>]* < changelog 

运行流程

  1. 参数解析。
  2. 获取用户名称、用户邮件、提交日期。
  3. 写入tree信息。
  4. 写入parent信息。
  5. 写入author、commiter信息。
  6. 写入comments(注释)。
  7. 压缩并存储到objects文件中,该object文件为commit对象。

commit对象的文件格式及说明见下文【设计原理 - commit对象】。

运行示例

  1. # 写入到tree 
  2. $ write-tree  
  3. c771b3ab2fe3b7e43099290d3e99a3e8c414ec72 
  4.  
  5. # 提交tree 
  6. $ echo "first commit" > changelog 
  7. commit-tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72 < changelog 
  8. Committing initial tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72 
  9. 7ea820bd363e24f5daa5de8028d77d88260503d9 
  10.  
  11. # 查看commit对象内容 
  12. $ cat-file 7ea820bd363e24f5daa5de8028d77d88260503d9 
  13. temp_git_file_CIfJsg: commit 
  14. $ cat temp_git_file_CIfJsg 
  15. tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72 
  16. author Xiaowen Xia <chenan.xxw@aos-hw09> Tue Sep  1 10:56:16 2020 
  17. committer Xiaowen Xia <chenan.xxw@aos-hw09> Tue Sep  1 10:56:16 2020 
  18.  
  19. 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。

  1. $ tree .git/objects 
  2. .git/objects 
  3. ├── 02 
  4. │   └── 77ec89d7ba8c46a16d86f219b21cfe09a611e1 
  5. ├── ......                                          # 省略 
  6. ├── be 
  7. │   ├── adb5bac00c74c97da7f471905Ab0da8b50229c 
  8. │   └── ee7b5e8ab6ae1c0c1f3cfa2c4643aacdb30b9b 
  9. ├── ......                                          # 省略 
  10. ├── c9 
  11. │   └── f6098f3ba06cf96e1248e9f39270883ba0e82e 
  12. ├── ......                                          # 省略 
  13. ├── cf 
  14. │   ├── 631abbf3c4cec0911cb60cc307f3dce4f7a000 
  15. │   └── 9e478ab3fc98680684cc7090e84644363a4054 
  16. ├── ......                                          # 省略 
  17. └── ff 

为了节约存储,同时也能存储多个信息,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 对象内容:

  1. # 查看 blob 对象内容 
  2. $ cat-file 82f8604c3652fa5762899b5ff73eb37bef2da795temp_git_file_tBTXFM: blob 
  3.  
  4. $ cat ./temp_git_file_tBTXFM  
  5. hello git! 

tree 对象

运行 write-tree 会生成 tree 对象。

tree 对象用于存储多个提交文件的信息。tree 对象由 + + 文件模式 + 文件名称 + 文件sha1值 拼装并压缩:

文件sha1值 使用binary格式存储,占用20字节。

使用 cat-file 查看 tree 对象内容:

  1. # 查看 tree 对象内容 
  2. $ cat-file  c771b3ab2fe3b7e43099290d3e99a3e8c414ec72 
  3. temp_git_file_r90ft5: tree 
  4.  
  5. $ cat ./temp_git_file_r90ft5 
  6. 100664 README.md��`L6R�Wb��_�>�{�-�� 

文件sha1值 使用binary格式存储,所以打印的时候会有乱码。

commit 对象

运行 commit-tree 会生成 commit 对象。

commit 对象存储一次提交的信息,包括所在的tree信息,parent信息以及提交的作者等信息。commit 对象由 + +

tree sha1值 和 parent sha1值 使用hex字符串格式存储,占用40字节。

使用 cat-file 查看 commit 对象内容:

  1. # 查看 commit 对象内容 
  2. $ cat-file 7ea820bd363e24f5daa5de8028d77d88260503d9 
  3. temp_git_file_CIfJsg: commit 
  4.  
  5. $ cat temp_git_file_CIfJsg 
  6. tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72 
  7. author Xiaowen Xia <chenan.xxw@aos-hw09> Tue Sep  1 10:56:16 2020 
  8. committer Xiaowen Xia <chenan.xxw@aos-hw09> Tue Sep  1 10:56:16 2020 
  9.  
  10. first commit 

索引文件

索引文件默认路径为:.dircache/index。索引文件用来存储变更文件的相关信息,当运行 update-cache 时会添加变更文件的信息到索引文件中。

同时也有一个叫 .dircache/index.lock 的文件,该文件存在时表示当前工作区被锁定,无法进行提交操作。

使用 hexdump 命令可以查看到索引文件内容:

  1. $ hexdump -C .dircache/index  
  2. 00000000  43 52 49 44 01 00 00 00  01 00 00 00 ae 73 c4 f2  |CRID.........s..| 
  3. 00000010  ce 32 c9 6f 13 20 0d 56  9c e8 cf 0d d3 75 10 c8  |.2.o. .V.....u..| 
  4. 00000020  94 ad 4c 5f f4 5c 42 06  94 ad 4c 5f f4 5c 42 06  |..L_.\B...L_.\B.| 
  5. 00000030  00 03 01 00 91 16 d2 04  b4 81 00 00 ee 03 00 00  |................| 
  6. 00000040  ee 03 00 00 0b 00 00 00  a3 f4 a0 66 c5 46 39 78  |...........f.F9x| 
  7. 00000050  1e 30 19 a3 20 42 e3 82  84 ee 31 54 09 00 52 45  |.0.. B....1T..RE| 
  8. 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 的迁移。

  1. #include <openssl/sha.h> 
  2.  
  3. static int verify_hdr(struct cache_header *hdr, unsigned long size
  4.   SHA_CTX c; 
  5.   unsigned char sha1[20]; 
  6.         /* 省略 */ 
  7.   /* 计算索引文件头sha1值 */ 
  8.   SHA1_Init(&c); 
  9.   SHA1_Update(&c, hdr, offsetof(struct cache_header, sha1)); 
  10.   SHA1_Update(&c, hdr+1, size - sizeof(*hdr)); 
  11.   SHA1_Final(sha1, &c); 
  12.   /* 省略 */ 
  13.   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 的设计原理和思想。

【本文为51CTO专栏作者“阿里巴巴官方技术”原创稿件,转载请联系原作者】

戳这里,看该作者更多好文

 

责任编辑:武晓燕 来源: 51CTO专栏
相关推荐

2020-04-24 10:02:44

组件Vue组件库

2016-08-05 12:58:44

GitLinux开源

2018-10-15 10:10:41

Linux内核补丁

2022-02-15 10:45:50

软件汽车开发

2022-07-21 07:09:21

React前端项目

2018-01-05 08:54:09

加固型数据中心业务

2010-07-30 14:58:06

Flex应用

2014-02-17 17:18:00

程序员

2013-01-31 11:51:37

开源KVM

2013-02-27 10:53:16

开源KVM

2022-10-30 15:03:25

人工智能仓库管理机器人

2017-12-17 16:53:27

云计算亚马逊云端

2017-07-27 16:25:54

云管理公共云合并

2016-12-07 18:10:08

边缘计算

2012-08-24 10:46:12

程序员

2015-04-08 10:40:09

2020-07-14 14:50:44

Vue代码前端

2013-08-20 10:31:16

云计算2.0云计算

2020-12-03 06:34:34

分支策略SIT

2015-08-06 16:13:18

Facebook社交
点赞
收藏

51CTO技术栈公众号