阿里妹導讀:經過不斷地迭代,如今Git的功能越來越完善和強大。然而Git的第一個提交原始碼僅約1000行,當時的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 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 diff ./Makefile
...
-LIBS= -lssl
+LIBS= -lssl -lz -lcrypto
...
# 編譯
$ make
原始碼分析
Write programs that do one thing and do it well. ——Unix philosophy
init-db:初始化倉庫
$ init-db
建立目錄:.dircache。 建立目錄:.dircache/objects。 在 .dircache/objects 中建立了從 00 ~ ff 共256個目錄。
# 執行init-db初始化倉庫
$ init-db
defaulting to private storage area
# 檢視初始化後的目錄結構
$ tree . -a
.
└── .dircache # git工作目錄
└── objects # objects檔案
├── 00
├── 01
├── 02
├── ...... # 省略
├── fe
└── ff
258 directories, 0 files
update-cache:新增檔案或目錄
$ update-cache <file> ...
讀取並解析索引檔案 :.dircache/index。 遍歷多個檔案,讀取並生成變更檔案資訊(檔名稱、檔案內容sha1值、日期、大小等),寫入到索引檔案中。 遍歷多個檔案,讀取並壓縮變更檔案,儲存到objects檔案中,該檔案為blob物件。
# 新增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>
根據入參sha1值定位objects檔案,比如.dircache/objects/46/4b392e2c8c7d2d13d90e6916e6d41defe8bb6a 讀取該objects檔案內容,解壓得到真實資料。 寫入到臨時檔案 temp_git_file_XXXXXX(隨機不重複檔案)。
# 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。 迴圈遍歷變更檔案資訊,比較工作區中的檔案資訊和索引檔案中記錄的檔案資訊差異。 無差異,顯示 <file-name>: 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
讀取並解析索引檔案:.dircache/index。 迴圈遍歷變更檔案資訊,按照指定格式編排變更檔案資訊及內容。 壓縮並儲存到objects檔案中,該object檔案為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>
解析sha1值。 讀取對應sha1值的object物件。 輸出變更檔案的屬性、路徑、sha1值。
# 提交
$ write-tree
c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
# 讀取tree物件
$ read-tree c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
100664 README.md (82f8604c3652fa5762899b5ff73eb37bef2da795)
commit-tree:提交tree
$ commit-tree <sha1> [-p <sha1>]* < changelog
引數解析。 獲取使用者名稱稱、使用者郵件、提交日期。 寫入tree資訊。 寫入parent資訊。 寫入author、commiter資訊。 寫入comments(註釋)。 壓縮並儲存到objects檔案中,該object檔案為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
blob 物件:儲存著檔案快照。 tree 物件:記錄著目錄結構和 blob 物件索引。 commit 物件:包含著指向前述 tree 物件的指標和所有提交資訊。
工作區(workspace):我們直接修改程式碼的地方。 暫存區(index):資料暫時存放的區域,用於在工作區和版本庫之間進行資料交流。 版本庫(commit history):存放已經提交的資料。
objects 檔案
$ tree .git/objects
.git/objects
├── 02
│ └── 77ec89d7ba8c46a16d86f219b21cfe09a611e1
├── ...... # 省略
├── be
│ ├── adb5bac00c74c97da7f471905ab0da8b50229c
│ └── ee7b5e8ab6ae1c0c1f3cfa2c4643aacdb30b9b
├── ...... # 省略
├── c9
│ └── f6098f3ba06cf96e1248e9f39270883ba0e82e
├── ...... # 省略
├── cf
│ ├── 631abbf3c4cec0911cb60cc307f3dce4f7a000
│ └── 9e478ab3fc98680684cc7090e84644363a4054
├── ...... # 省略
└── ff
blob 物件
# 檢視 blob 物件內容
$ cat-file 82f8604c3652fa5762899b5ff73eb37bef2da795temp_git_file_tBTXFM: blob
$ cat ./temp_git_file_tBTXFM
hello git!
tree 物件
# 檢視 tree 物件內容
$ cat-file c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
temp_git_file_r90ft5: tree
$ cat ./temp_git_file_r90ft5
100664 README.md��`L6R�Wb��_�>�{�-��
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 時會新增變更檔案的資訊到索引檔案中。
$ 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.|
雜湊演算法
#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
好的程式碼不是寫出來的,是改出來的
關於細節
關於程式碼質量
異常處理不完善,經常出現段錯誤(SegmentFault)。
存在幾處記憶體洩漏的地方,比如 write-tree.c > main函式 > buffer記憶體塊 。
從索引檔案中讀取到的變更檔案資訊使用陣列儲存,涉及到了比較多的申請釋放操作,效能上是有損失的,可以最佳化成連結串列儲存。
招聘
參考資料
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)