源碼解析:Git的第一個(gè)提交是什么樣的?
前言
Git 是目前世界上被最廣泛使用的現(xiàn)代軟件版本管理系統(tǒng)(Version Control System)。Git 本身亦是一個(gè)成熟并處于活躍開發(fā)狀態(tài)的開源項(xiàng)目,今天驚人數(shù)量的軟件項(xiàng)目依賴 Git 進(jìn)行版本管理,這些項(xiàng)目包括開源以及各種商業(yè)軟件。Git 在職業(yè)軟件開發(fā)者中擁有良好的聲譽(yù),Git 目前支持絕大多數(shù)的操作系統(tǒng)以及 IDE(Integrated Development Environments)。
Git 最初是由 Linux 操作系統(tǒng)內(nèi)核的創(chuàng)造者 Linus Torvalds 在 2005 年創(chuàng)造,Git 第一個(gè)可用版本是 Linus 花了兩周時(shí)間用C寫出來的。Git 第一個(gè)版本就實(shí)現(xiàn)了 Git 源碼自托管,一個(gè)月之內(nèi),Linux系統(tǒng)的源碼也已經(jīng)由 Git 管理了!
Git 的第一個(gè)提交源碼僅有約1000行,但是已經(jīng)實(shí)現(xiàn)了Git的基本設(shè)計(jì)原理,比如初始化倉庫、提交代碼、查看代碼diff、讀取提交信息等,Git 定義了三個(gè)區(qū):工作區(qū)(workspace)、暫存區(qū)(index)、版本庫(commit history),也實(shí)現(xiàn)了三類重要的 Git 對(duì)象:blob、tree、commit。本文將從源碼上分析 Git 的第一個(gè)提交并挖掘背后優(yōu)秀的設(shè)計(jì)原理。
編譯
獲取源碼
在Github上可以找到Git的倉庫鏡像:https://github.com/git/git.git
- # 獲取 git 源碼
- $ git clone https://github.com/git/git.git
- # 查看第一個(gè)提交
- $ 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
- # 變更為第一個(gè)提交,指定commit-id
- $ git reset --hard e83c5163316f89bfbde7d9ab23ca2e25604af290
文件結(jié)構(gòu)
- $ 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 # 讀取當(dāng)前索引文件內(nèi)容
- ├── [8.2K] README
- ├── [ 986] read-tree.c # 讀取tree
- ├── [2.0K] show-diff.c # 查看diff內(nèi)容
- ├── [5.3K] update-cache.c # 添加文件或目錄
- └── [1.4K] write-tree.c # 寫入到tree
- # 統(tǒng)計(jì)代碼行數(shù),總共1089行
- $ find . "(" -name "*.c" -or -name "*.h" -or -name "Makefile" ")" -print | xargs wc -l
- ...
- 1089 total
編譯
編譯第一個(gè)提交的Git會(huì)有編譯問題,需要更改Makefile添加相關(guān)的依賴庫:
- $ git diff ./Makefile
- ...
- -LIBS= -lssl
- +LIBS= -lssl -lz -lcrypto
- ...
編譯:
- # 編譯
- $ make
只支持在 linux 平臺(tái)上編譯運(yùn)行。
源碼分析
Write programs that do one thing and do it well. ——Unix philosophy
查看編譯生成的可執(zhí)行文件,總共有7個(gè):

命令使用過程:

init-db:初始化倉庫
命令說明
- $ init-db
運(yùn)行流程
創(chuàng)建目錄:.dircache。
創(chuàng)建目錄:.dircache/objects。
在 .dircache/objects 中創(chuàng)建了從 00 ~ ff 共256個(gè)目錄。
.dircache/ 是Git的工作目錄,最新版本的Git工作目錄為 .git/ 。
運(yùn)行示例
- # 運(yùn)行init-db初始化倉庫
- $ init-db
- defaulting to private storage area
- # 查看初始化后的目錄結(jié)構(gòu)
- $ 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 主要是把工作區(qū)的修改文件提交到暫存區(qū)。工作區(qū)、暫存區(qū)等說明見下文【設(shè)計(jì)原理】 。
命令使用
- $ update-cache ...
運(yùn)行流程
- 讀取并解析索引文件 :.dircache/index。
- 遍歷多個(gè)文件,讀取并生成變更文件信息(文件名稱、文件內(nèi)容sha1值、日期、大小等),寫入到索引文件中。
- 遍歷多個(gè)文件,讀取并壓縮變更文件,存儲(chǔ)到objects文件中,該文件為blob對(duì)象。
如果是剛初始化的倉庫,會(huì)自動(dòng)創(chuàng)建索引文件。索引文件說明見下文【設(shè)計(jì)原理 - 索引文件】。blob對(duì)象的文件格式及說明見下文【設(shè)計(jì)原理 - blob對(duì)象】。sha1值說明見下文【設(shè)計(jì)原理 - 哈希算法】。
運(yùn)行示例
- # 新增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內(nèi)容,sha1值從索引文件中獲取
- $ cat-file bb122552ab7b4020b5f612cc3bbdd5b43d1fd3a8
- temp_git_file_61uTTP: blob
- $ cat ./temp_git_file_RwpU8b
- hello git
cat-file:查看objects文件內(nèi)容
cat-file 根據(jù)sha1值查看暫存區(qū)中的objects文件內(nèi)容。cat-file 是一個(gè)輔助工具,在正常的開發(fā)工作流中一般不會(huì)使用到。
命令使用
- $ cat-file <sha1>
運(yùn)行流程
- 根據(jù)入?yún)ha1值定位objects文件,比如.dircache/objects/46/4b392e2c8c7d2d13d90e6916e6d41defe8bb6a
- 讀取該objects文件內(nèi)容,解壓得到真實(shí)數(shù)據(jù)。
- 寫入到臨時(shí)文件 temp_git_file_XXXXXX(隨機(jī)不重復(fù)文件)。
objects內(nèi)容為壓縮格式,基于zlib壓縮算法,objects說明見【設(shè)計(jì)原理 - objects 文件】。
運(yùn)行示例
- # cat-file 會(huì)把內(nèi)容讀取到temp_git_file_rLcGKX
- $ cat-file 82f8604c3652fa5762899b5ff73eb37bef2da795
- temp_git_file_tBTXFM: blob
- # 查看 temp_git_file_tBTXFM 文件內(nèi)容
- $ cat ./temp_git_file_tBTXFM
- hello git!
show-diff:查看diff內(nèi)容
查看工作區(qū)和暫存區(qū)中的文件差異。
命令使用
- $ show-diff
運(yùn)行流程
讀取并解析索引文件:.dircache/index。
循環(huán)遍歷變更文件信息,比較工作區(qū)中的文件信息和索引文件中記錄的文件信息差異。
無差異,顯示
有差異,調(diào)用 diff 命令輸出差異內(nèi)容。
運(yùn)行示例
- # 創(chuàng)建文件并提交到暫存區(qū)
- $ echo "hello git!" > README.md
- $ update-cache README.md
- # 當(dāng)前無差異
- $ 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 作用將保存在索引文件中的多個(gè)objects對(duì)象歸并到一個(gè)類型為tree的objects文件中,該文件即Git中重要的對(duì)象:tree。
命令使用
- $ write-tree
運(yùn)行流程
- 讀取并解析索引文件:.dircache/index。
- 循環(huán)遍歷變更文件信息,按照指定格式編排變更文件信息及內(nèi)容。
- 壓縮并存儲(chǔ)到objects文件中,該object文件為tree對(duì)象。
tree對(duì)象的文件格式及相關(guān)說明見下文【設(shè)計(jì)原理 - tree對(duì)象】。
運(yùn)行示例
- # 提交
- $ write-tree
- c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
- # 查看objects內(nèi)容
- $ 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對(duì)象,輸出變更文件的信息。
命令使用
- $ read-tree <sha1>
運(yùn)行步驟
- 解析sha1值。
- 讀取對(duì)應(yīng)sha1值的object對(duì)象。
- 輸出變更文件的屬性、路徑、sha1值。
運(yùn)行示例
- # 提交
- $ write-tree
- c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
- # 讀取tree對(duì)象
- $ read-tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
- 100664 README.md (82f8604c3652fa5762899b5ff73eb37bef2da795)
commit-tree:提交tree
commit-tree 把本地變更提交到版本庫里,具體是基于一個(gè)tree對(duì)象的sha1值創(chuàng)建一個(gè)commit對(duì)象。
命令使用
- $ commit-tree <sha1> [-p <sha1>]* < changelog
運(yùn)行流程
- 參數(shù)解析。
- 獲取用戶名稱、用戶郵件、提交日期。
- 寫入tree信息。
- 寫入parent信息。
- 寫入author、commiter信息。
- 寫入comments(注釋)。
- 壓縮并存儲(chǔ)到objects文件中,該object文件為commit對(duì)象。
commit對(duì)象的文件格式及說明見下文【設(shè)計(jì)原理 - commit對(duì)象】。
運(yùn)行示例
- # 寫入到tree
- $ write-tree
- c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
- # 提交tree
- $ echo "first commit" > changelog
- $ commit-tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72 < changelog
- Committing initial tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
- 7ea820bd363e24f5daa5de8028d77d88260503d9
- # 查看commit對(duì)象內(nèi)容
- $ 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
設(shè)計(jì)原理
Write programs to work together.
——Unix philosophy
與傳統(tǒng)的集中式版本控制系統(tǒng)(CVCS)相反,Git 從一開始就設(shè)計(jì)成了去中心化的分布式系統(tǒng),每個(gè)開發(fā)者本地工作區(qū)都是一個(gè)完整的版本庫,擁有本地的代碼倉庫。另外,Git 的設(shè)計(jì)初衷是為了讓更多的開發(fā)者一起開發(fā)軟件。
該版本 Git 定義了三種對(duì)象:
- blob 對(duì)象:保存著文件快照。
- tree 對(duì)象:記錄著目錄結(jié)構(gòu)和 blob 對(duì)象索引。
- commit 對(duì)象:包含著指向前述 tree 對(duì)象的指針和所有提交信息。
三種對(duì)象相互之間的關(guān)系如下:
圖源:https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell
另外,Git 也定義了三個(gè)區(qū),工作區(qū)(workspace),暫存區(qū)(index)和版本庫(commit history):
- 工作區(qū)(workspace):我們直接修改代碼的地方。
- 暫存區(qū)(index):數(shù)據(jù)暫時(shí)存放的區(qū)域,用于在工作區(qū)和版本庫之間進(jìn)行數(shù)據(jù)交流。
- 版本庫(commit history):存放已經(jīng)提交的數(shù)據(jù)。
每個(gè)可執(zhí)行文件的具體分工是:init-db 用來創(chuàng)建一個(gè)初始化倉庫,update-cache 會(huì)將 工作區(qū) 的變更寫到 索引文件 (index)中,write-tree 會(huì)將之前的所有變更整理成 tree 對(duì)象,commit-tree 會(huì)將 指定的 tree 對(duì)象寫到本地版本庫中。另外,show-diff 用來查看 工作區(qū) 和 暫存區(qū) 中的文件差異,read-tree 用來讀取 tree對(duì)象 的信息。
由此可以繪制一個(gè)簡(jiǎn)單的Git開發(fā)工作流:
objects 文件
objects文件是載體,用來存儲(chǔ)Git中的3個(gè)重要對(duì)象:blob、tree、commit。
- $ tree .git/objects
- .git/objects
- ├── 02
- │ └── 77ec89d7ba8c46a16d86f219b21cfe09a611e1
- ├── ...... # 省略
- ├── be
- │ ├── adb5bac00c74c97da7f471905Ab0da8b50229c
- │ └── ee7b5e8ab6ae1c0c1f3cfa2c4643aacdb30b9b
- ├── ...... # 省略
- ├── c9
- │ └── f6098f3ba06cf96e1248e9f39270883ba0e82e
- ├── ...... # 省略
- ├── cf
- │ ├── 631abbf3c4cec0911cb60cc307f3dce4f7a000
- │ └── 9e478ab3fc98680684cc7090e84644363a4054
- ├── ...... # 省略
- └── ff
為了節(jié)約存儲(chǔ),同時(shí)也能存儲(chǔ)多個(gè)信息,objects文件內(nèi)容都是經(jīng)過 zlib 壓縮過的。objects文件的格式由
使用 cat-file 可以查看object文件是什么類型的對(duì)象。
.dircache/objects 目錄結(jié)構(gòu)如下:
$ tree .git/objects.git/objects├── 02│ └── 77ec89d7ba8c46a16d86f219b21cfe09a611e1├── ...... # 省略├── be│ ├── adb5bac00c74c97da7f471905Ab0da8b50229c│ └── ee7b5e8ab6ae1c0c1f3cfa2c4643aacdb30b9b├── ...... # 省略├── c9│ └── f6098f3ba06cf96e1248e9f39270883ba0e82e├── ...... # 省略├── cf│ ├── 631abbf3c4cec0911cb60cc307f3dce4f7a000│ └── 9e478ab3fc98680684cc7090e84644363a4054├── ...... # 省略└── ff
問:為什么 .dircache/objects/ 目錄下面要以sha1值前一個(gè)字節(jié)的hex值作為子目錄?
blob 對(duì)象
運(yùn)行 update-cache 會(huì)生成 blob 對(duì)象。
blob 對(duì)象用于存儲(chǔ)變更文件內(nèi)容,其實(shí)就代表一個(gè)變更文件快照。blob 對(duì)象由
使用 cat-file 查看 blob 對(duì)象內(nèi)容:
- # 查看 blob 對(duì)象內(nèi)容
- $ cat-file 82f8604c3652fa5762899b5ff73eb37bef2da795temp_git_file_tBTXFM: blob
- $ cat ./temp_git_file_tBTXFM
- hello git!
tree 對(duì)象
運(yùn)行 write-tree 會(huì)生成 tree 對(duì)象。
tree 對(duì)象用于存儲(chǔ)多個(gè)提交文件的信息。tree 對(duì)象由
文件sha1值 使用binary格式存儲(chǔ),占用20字節(jié)。
使用 cat-file 查看 tree 對(duì)象內(nèi)容:
- # 查看 tree 對(duì)象內(nèi)容
- $ cat-file c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
- temp_git_file_r90ft5: tree
- $ cat ./temp_git_file_r90ft5
- 100664 README.md��`L6R�Wb��_�>�{�-��
文件sha1值 使用binary格式存儲(chǔ),所以打印的時(shí)候會(huì)有亂碼。
commit 對(duì)象
運(yùn)行 commit-tree 會(huì)生成 commit 對(duì)象。
commit 對(duì)象存儲(chǔ)一次提交的信息,包括所在的tree信息,parent信息以及提交的作者等信息。commit 對(duì)象由
tree sha1值 和 parent sha1值 使用hex字符串格式存儲(chǔ),占用40字節(jié)。
使用 cat-file 查看 commit 對(duì)象內(nèi)容:
- # 查看 commit 對(duì)象內(nèi)容
- $ 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
索引文件
索引文件默認(rèn)路徑為:.dircache/index。索引文件用來存儲(chǔ)變更文件的相關(guān)信息,當(dāng)運(yùn)行 update-cache 時(shí)會(huì)添加變更文件的信息到索引文件中。
同時(shí)也有一個(gè)叫 .dircache/index.lock 的文件,該文件存在時(shí)表示當(dāng)前工作區(qū)被鎖定,無法進(jìn)行提交操作。
使用 hexdump 命令可以查看到索引文件內(nèi)容:
- $ 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 索引文件使用二進(jìn)制存儲(chǔ)相關(guān)內(nèi)容,該文件由 文件頭 + 變更文件信息 組成:

文件頭大小為32字節(jié),一個(gè)變更文件信息大小至少是63字節(jié)。其中:文件頭中的sha1值由整個(gè)索引文件內(nèi)容(文件頭 + 變更文件信息)計(jì)算得到的。變更文件信息的sha1值由變更文件內(nèi)容(壓縮后)計(jì)算得到的。
哈希算法
該 Git 版本中使用的哈希算法為 sha1算法 ,代碼中使用的是 OpenSSL 庫中提供的sha1算法。
目前 Git 已經(jīng)有了新的選擇: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];
- /* 省略 */
- /* 計(jì)算索引文件頭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;
- }
總結(jié)與思考
Use software leverage to your advantage. ——Unix philosophy
好的代碼不是寫出來的,是改出來的
Git 的第一個(gè)提交中,雖然實(shí)現(xiàn)了 Git 的分布式核心思想,以及三種對(duì)象,三個(gè)區(qū)等核心概念,但是 Git 的靈魂功能比如分支策略、遠(yuǎn)程倉庫、日志系統(tǒng)、git hooks 等功能都是后面逐步迭代出來的。
關(guān)于細(xì)節(jié)
問:為什么 .dircache/objects/ 目錄下面要以 sha1 值前一個(gè)字節(jié)的 hex 值作為子目錄?
答:ext3 文件系統(tǒng)下,一個(gè)目錄下只能有 32000 個(gè)一級(jí)子文件,如果都把 objects 文件存儲(chǔ)到一個(gè) .git/objects/ 目錄里,很大概率會(huì)達(dá)到上限。同時(shí)要是一個(gè)目錄下面子文件太多,那文件查找效率會(huì)降低很多。
關(guān)于代碼質(zhì)量
Git 的第一次提交源碼,從代碼質(zhì)量、數(shù)據(jù)結(jié)構(gòu)上看其實(shí)并沒有多少參考價(jià)值,反而我還發(fā)現(xiàn)了很多可以優(yōu)化的地方,比如:
- 異常處理不完善,經(jīng)常出現(xiàn)段錯(cuò)誤(SegmentFault)。
- 存在幾處內(nèi)存泄漏的地方,比如 write-tree.c > main函數(shù) > buffer內(nèi)存塊 。
- 從索引文件中讀取到的變更文件信息使用數(shù)組存儲(chǔ),涉及到了比較多的申請(qǐng)釋放操作,性能上是有損失的,可以優(yōu)化成鏈表存儲(chǔ)。
不過這些都不重要,重要的是 Git 的設(shè)計(jì)原理和思想。
【本文為51CTO專欄作者“阿里巴巴官方技術(shù)”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者】