原始碼解析:Git的第一個提交是什麼樣的?

阿里技術發表於2020-09-22

原始碼解析:Git的第一個提交是什麼樣的?

阿里妹導讀:經過不斷地迭代,如今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 <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會有編譯問題,需要更改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個:

原始碼解析:Git的第一個提交是什麼樣的?

命令使用過程:
原始碼解析:Git的第一個提交是什麼樣的?

init-db:初始化倉庫

命令說明
$ init-db

執行流程
  1. 建立目錄:.dircache。
  2. 建立目錄:.dircache/objects。
  3. 在 .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> ...

執行流程
  1. 讀取並解析索引檔案 :.dircache/index。
  2. 遍歷多個檔案,讀取並生成變更檔案資訊(檔名稱、檔案內容sha1值、日期、大小等),寫入到索引檔案中。
  3. 遍歷多個檔案,讀取並壓縮變更檔案,儲存到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>

執行流程

  1. 根據入參sha1值定位objects檔案,比如.dircache/objects/46/4b392e2c8c7d2d13d90e6916e6d41defe8bb6a
  2. 讀取該objects檔案內容,解壓得到真實資料。
  3. 寫入到臨時檔案 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
執行流程
  1. 讀取並解析索引檔案:.dircache/index。
  2. 迴圈遍歷變更檔案資訊,比較工作區中的檔案資訊和索引檔案中記錄的檔案資訊差異。

    • 無差異,顯示 <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

執行流程

  1. 讀取並解析索引檔案:.dircache/index。
  2. 迴圈遍歷變更檔案資訊,按照指定格式編排變更檔案資訊及內容。
  3. 壓縮並儲存到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>

執行步驟

  1. 解析sha1值。
  2. 讀取對應sha1值的object物件。
  3. 輸出變更檔案的屬性、路徑、sha1值。

執行示例
# 提交$ write-treec771b3ab2fe3b7e43099290d3e99a3e8c414ec72
# 讀取tree物件$ read-tree  c771b3ab2fe3b7e43099290d3e99a3e8c414ec72100664 README.md (82f8604c3652fa5762899b5ff73eb37bef2da795)

commit-tree:提交tree

commit-tree 把本地變更提交到版本庫裡,具體是基於一個tree物件的sha1值建立一個commit物件。

命令使用
$ commit-tree <sha1> [-p <sha1>]* < changelog

執行流程

  1. 引數解析。
  2. 獲取使用者名稱稱、使用者郵件、提交日期。
  3. 寫入tree資訊。
  4. 寫入parent資訊。
  5. 寫入author、commiter資訊。
  6. 寫入comments(註釋)。
  7. 壓縮並儲存到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 物件的指標和所有提交資訊。

三種物件相互之間的關係如下:
原始碼解析:Git的第一個提交是什麼樣的?
圖源: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開發工作流:
原始碼解析: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> 拼裝並壓縮:

原始碼解析:Git的第一個提交是什麼樣的?

使用 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值 拼裝並壓縮:
原始碼解析:Git的第一個提交是什麼樣的?

檔案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> 拼裝並壓縮:

原始碼解析:Git的第一個提交是什麼樣的?

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 索引檔案使用二進位制儲存相關內容,該檔案由 檔案頭 + 變更檔案資訊 組成:
原始碼解析:Git的第一個提交是什麼樣的?
檔案頭大小為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)

相關文章