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

簡介: 通過不斷地迭代,現在Git的功能愈來愈完善和強大。然而Git的第一個提交源碼僅約1000行,當時的Git實現了哪些功能?本文將從源碼開始,分析其核心思想,挖掘背後優秀的設計原理。html

image.png

前言

Git 是目前世界上被最普遍使用的現代軟件版本管理系統(Version Control System)。Git 自己亦是一個成熟並處於活躍開發狀態的開源項目,今天驚人數量的軟件項目依賴 Git 進行版本管理,這些項目包括開源以及各類商業軟件。Git 在職業軟件開發者中擁有良好的聲譽,Git 目前支持絕大多數的操做系統以及 IDE(Integrated Development Environments)。linux

Git 最初是由 Linux 操做系統內核的創造者 Linus Torvalds 在 2005 年創造,Git 第一個可用版本是 Linus 花了兩週時間用C寫出來的。Git 第一個版本就實現了 Git 源碼自託管,一個月以內,Linux系統的源碼也已經由 Git 管理了!git

Git 的第一個提交源碼僅有約1000行,可是已經實現了Git的基本設計原理,好比初始化倉庫、提交代碼、查看代碼diff、讀取提交信息等,Git 定義了三個區:工做區(workspace)、暫存區(index)、版本庫(commit history),也實現了三類重要的 Git 對象:blob、tree、commit。本文將從源碼上分析 Git 的第一個提交併挖掘背後優秀的設計原理。github

編譯

獲取源碼

在Github上能夠找到Git的倉庫鏡像:
https://github.com/git/git.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 reset --hard 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個:工具

image.png

命令使用過程:

image.png

init-db:初始化倉庫

命令說明

$ init-db

運行流程

建立目錄:.dircache。
建立目錄:.dircache/objects。
在 .dircache/objects 中建立了從 00 ~ ff 共256個目錄。

.dircache/ 是Git的工做目錄,最新版本的Git工做目錄爲 .git/ 。

運行示例

# 運行init-db初始化倉庫
$ init-db
defaulting to private storage area 
# 查看初始化後的目錄結構
$ 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 主要是把工做區的修改文件提交到暫存區。工做區、暫存區等說明見下文【設計原理】 。

命令使用

$ 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/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值查看暫存區中的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 82f8604c3652fa5762899b5ff73eb37bef2da795
temp_git_file_tBTXFM: blob

# 查看 temp_git_file_tBTXFM 文件內容
$ cat ./temp_git_file_tBTXFM 
hello git!

show-diff:查看diff內容

查看工做區和暫存區中的文件差別。

命令使用

$ show-diff

運行流程

讀取並解析索引文件:.dircache/index。
循環遍歷變動文件信息,比較工做區中的文件信息和索引文件中記錄的文件信息差別。
無差別,顯示 : 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 做用將保存在索引文件中的多個objects對象歸併到一個類型爲tree的objects文件中,該文件即Git中重要的對象:tree。

命令使用

$ write-tree

運行流程

讀取並解析索引文件:.dircache/index。
循環遍歷變動文件信息,按照指定格式編排變動文件信息及內容。
壓縮並存儲到objects文件中,該object文件爲tree對象。

tree對象的文件格式及相關說明見下文【設計原理 - 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值的tree對象,輸出變動文件的信息。

命令使用

$ read-tree <sha1>

運行步驟

解析sha1值。
讀取對應sha1值的object對象。
輸出變動文件的屬性、路徑、sha1值。

運行示例

# 提交
$ write-tree
c771b3ab2fe3b7e43099290d3e99a3e8c414ec72

# 讀取tree對象
$ read-tree  c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
100664 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 < 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

與傳統的集中式版本控制系統(CVCS)相反,Git 從一開始就設計成了去中心化的分佈式系統,每一個開發者本地工做區都是一個完整的版本庫,擁有本地的代碼倉庫。另外,Git 的設計初衷是爲了讓更多的開發者一塊兒開發軟件。

該版本 Git 定義了三種對象:

blob 對象:保存着文件快照。
tree 對象:記錄着目錄結構和 blob 對象索引。
commit 對象:包含着指向前述 tree 對象的指針和全部提交信息。

三種對象相互之間的關係以下:

image.png

另外,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開發工做流:

image.png

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文件的格式由 + + <要存儲的內容> 組成,其中 能夠是"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 對象由 + + 拼裝並壓縮:

image.png

使用 cat-file 查看 blob 對象內容:

# 查看 blob 對象內容
$ cat-file 82f8604c3652fa5762899b5ff73eb37bef2da795temp_git_file_tBTXFM: blob

$ cat ./temp_git_file_tBTXFM 
hello git!

tree 對象

運行 write-tree 會生成 tree 對象。

tree 對象用於存儲多個提交文件的信息。tree 對象由 + + 文件模式 + 文件名稱 + 文件sha1值 拼裝並壓縮:

image.png

文件sha1值 使用binary格式存儲,佔用20字節。

使用 cat-file 查看 tree 對象內容:

# 查看 tree 對象內容
$ cat-file  c771b3ab2fe3b7e43099290d3e99a3e8c414ec72
temp_git_file_r90ft5: tree

$ cat ./temp_git_file_r90ft5
100664 README.md��`L6R�Wb��_�>�{�-��

文件sha1值 使用binary格式存儲,因此打印的時候會有亂碼。

commit 對象

運行 commit-tree 會生成 commit 對象。

commit 對象存儲一次提交的信息,包括所在的tree信息,parent信息以及提交的做者等信息。commit 對象由 + + + * + + + 拼裝並壓縮:

image.png

tree sha1值 和 parent sha1值 使用hex字符串格式存儲,佔用40字節。

使用 cat-file 查看 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 時會添加變動文件的信息到索引文件中。

同時也有一個叫 .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 索引文件使用二進制存儲相關內容,該文件由 文件頭 + 變動文件信息 組成:

image.png

文件頭大小爲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))

相關文章
相關標籤/搜索