“晴耕 · 白話”之Git解密——認識Git對象

也許你早已經熟悉了Git的日常使用,但是你可曾想過:爲什麼每次新建Git庫時都要執行git init呢?執行git init後生成的.git目錄裏到底藏了哪些祕密?平常使用Git客戶端,以及命令行執行git命令時,Git在背後到底爲我們默默地做了些什麼呢?閱讀本文以後,一切謎團都將引刃而解!

注:
本文的大部分寫作靈感來自於“Pro Git book”。感謝原作者的精彩分享。
本文采用知識共享署名-相同方式共享 4.0 國際許可協議進行許可。
知識共享許可協議

.git目錄

衆所周知,每次我們在本地新建一個Git庫時,都要執行git init命令。Git會在新建庫的根目錄下爲我們自動創建一個.git目錄,把所有它要用到的信息都保存在這個神奇的目錄裏。我們日常對Git的所有操作,本質上講,就是在對這個目錄進行維護。也正是因爲這個原因,如果我們想備份Git庫的話,實際上只要複製這個.git目錄就可以了。

爲了方便後面做實驗,讓我們先在本地新建一個Git庫,並觀察.git目錄下包含的內容。本文推薦大家利用Hello Git提供的兩個Docker鏡像作爲實驗環境:一個代表遠程Git服務(lab-git-remote),另一個代表本地Git客戶端(lab-git-local)。這兩個鏡像都可以從Docker Hub上找到:

docker pull morningspace/lab-git-remote
docker pull morningspace/lab-git-local

有關這兩個Docker鏡像的具體使用方法,請見Hello Git項目的README。本文後續討論的所有動手環節,都將圍繞這兩個Docker鏡像展開。現在,我們就來創建本地Git庫:inside-git。

$ git init inside-git
Initialized empty Git repository in /root/inside-git/.git/
$ cd inside-git
$ ls -l .git
total 32
-rw-r--r-- 1 root root   23 Apr 30 00:46 HEAD
drwxr-xr-x 2 root root 4096 Apr 30 00:46 branches
-rw-r--r-- 1 root root   92 Apr 30 00:46 config
-rw-r--r-- 1 root root   73 Apr 30 00:46 description
drwxr-xr-x 2 root root 4096 Apr 30 00:46 hooks
drwxr-xr-x 2 root root 4096 Apr 30 00:46 info
drwxr-xr-x 4 root root 4096 Apr 30 00:46 objects
drwxr-xr-x 4 root root 4096 Apr 30 00:46 refs

因爲我們還沒有往Git庫裏添加任何內容,所以.git基本上只是一個空的目錄結構。這其中,我們將重點關注兩個目錄和一個文件,分別是:objects目錄,refs目錄,以及HEAD文件。那麼,它們到底有什麼用途呢?別急,我們接着往下看。

認識Git對象

在Git的世界裏,Git把它所管理的一切內容都當作了一個個的Git對象(Git object),這些對象就被保存在.git目錄下。Git在保存這些對象時,還會爲它們生成一個基於SHA-1算法的全局唯一的hash值,以便後面利用這個hash值對Git對象進行存取和引用。

所以,Git的核心,其實就是一個基於文件系統的,存儲“鍵值對”的數據庫。或者,更加“正式”一點的說法是,Git是一個在內容上可尋址的文件系統(Content-Addressable Filesystem)。

基於這一認識,我們再回過頭來看.git目錄下的這兩個目錄和一個文件:

  • objects: 顧名思義,這個目錄裏存的都是Git對象。它以Git對象的形式保存了該Git庫所包含和管理的全部內容;
  • refs: 保存了指向Git對象的引用。準確地說,這個引用所指向的,是與分支,標籤,遠程庫等相關聯的commit對象。Git把它所管理的對象分成了幾種類型,commit對象就是其中之一。關於commit對象,引用,以及refs目錄,後面我們還會詳細介紹;
  • HEAD: 這個很容易理解,它保存了當前分支的head指針,實際上就是指向當前分支最新提交(也是一個commit對象)的引用。

Blob對象

有了前面的鋪墊,從這一節開始,我們將真正開啓探索Git底層機制的神奇之旅啦!

我們要做的第一件事情,是手工創建一個Git對象。Git對象的創建,在我們日常使用Git時,都是由Git在背後悄悄完成的。通過手工創建,很快我們就會揭開它的神祕面紗,看一看Git到底是如何跟蹤和管理內容的。

說是手工創建,其實也不完全是。這裏,我們依然要藉助於Git的一個特殊命令:git hash-object。利用它,我們可以把內容存入.git/objects目錄,並返回指向該內容的唯一鍵(即hash值)。這個命令,我們在日常使用Git時可能從來都沒有用到過,它屬於Git的底層命令。我們平時執行的那些git命令,背後其實調用的都是像git hash-object那樣更爲底層的命令。在後面的內容裏,我們將大量使用這些底層命令,來理解Git背後隱藏的祕密。

OK,現在就讓我們利用git hash-object,手工把一個新建的文件存入Git庫:

$ echo 'Inside Git' > README
$ git hash-object -w README
968b2bf72e28d8c6756054730880cf9f9ab06062

這裏,參數-w是爲了告訴Git,在返回唯一鍵的同時,把內容存入Git庫。關於存入Git庫中的Git對象的格式,後面我們還會詳細解釋,這裏先略過不表。git hash-object的返回結果,即Git對象的唯一鍵,是一個長度爲40個字符的基於SHA-1算法的hash值。

此時,我們會發現在.git目錄的objects子目錄下多了一個新文件。文件名和上級目錄名稱的組合剛好構成了git hash-object返回的唯一鍵:

$ find .git/objects -type f
.git/objects/96/8b2bf72e28d8c6756054730880cf9f9ab06062

利用另一個Git的底層命令git cat-file,我們還可以還原剛纔所保存的內容正文:

$ git cat-file -p 968b2bf72e28d8c6756054730880cf9f9ab06062
Inside Git

接下來,我們再對README的內容做些修改,然後存成一個新的Git對象:

$ echo 'Welcome to Inside Git' > README
$ git hash-object -w README
4f4fc3399cef946fc77e12211808d0590715793d

現在再來看一下我們的.git目錄:

$ find .git/objects -type f
.git/objects/96/8b2bf72e28d8c6756054730880cf9f9ab06062
.git/objects/4f/4fc3399cef946fc77e12211808d0590715793d

這個時候.git目錄下已經有兩個Git對象了,它們分別對應於README文件的兩個不同版本。如果在執行git cat-file命令時使用-t參數,我們還能看到Git對象的所屬類型:

$ git cat-file -t 8b2bf72e28d8c6756054730880cf9f9ab06062
blob
$ git cat-file -t 4f4fc3399cef946fc77e12211808d0590715793d
blob

可以看到,與文件相對應的Git對象都是blob類型的。很快我們就會看到,除了blob對象,Git還支持很多其他類型的對象。

Git對象的存儲

前面提到過,存入Git庫中的Git對象是有着特定格式的。具體來說,它包含了一個header字段,後跟一個’\0’字符,然後纔是內容正文。整個Git對象在存入Git庫時,還會經過一次zlib的deflate壓縮處理:

<header字段>\0<內容正文>

其中,header字段的格式爲:

<對象類型> <內容正文長度>

這裏的對象類型指的就是Git對象的類型,比如:blob,tree,commit,tag等。事實上,對header字段採用SHA-1算法計算得到的結果,就是Git對象的唯一鍵。它是一個長度爲40個字符的字符串。

爲了驗證這一點,下面我們將再次手工創建一個Git對象。和之前不同的是,這裏我們不再使用git提供的現成命令git hash-object了,而是採用第三方命令行工具:sha1sum和pigz。

注意:如果你使用的是Hello Git提供的Docker鏡像,那麼這些命令行工具都已經預裝了。否則需要自行安裝,比如在Ubuntu上安裝pigz,可以執行如下命令:

$ apt-get update
$ apt-get install pigz

假設我們的內容正文是“What is inside Git?”,字符串長度爲19,對應的Git對象類型爲blob。按照上面的格式把header字段和內容正文用’\0’字符隔開,然後利用sha1sum計算SHA-1的hash值如下:

$ echo -ne 'blob 19\0What is inside Git?' | sha1sum | awk '{print $1}'
5fe76ad3e039c1fdc74201715f8c150bed50e351

我們在執行echo時,加上了參數-n-e。前者是爲了避免echo在輸出字符串時自動添加換行符;後者是爲了讓echo能夠識別反斜槓轉義符(即字符串中的’\0’)。

然後,我們再用git hash-object來驗證一下,這個用sha1sum生成的hash值是否是正確的:

$ echo -n 'What is inside Git?' | git hash-object --stdin
5fe76ad3e039c1fdc74201715f8c150bed50e351

返回的結果表明,用sha1sum生成的結果和git hash-object是完全一致的。這裏,使用參數--stdin是爲了告訴git,從標準輸入中讀取內容。

接下來,我們再來看一下Git對象的存儲。按照規則,我們把前面生成的唯一鍵拆成兩個部分:前兩位作爲目錄名,在.git/objects下新建一個子目錄;後38位則作爲文件名,在該子目錄下新建一個文件。同時,在把Git對象寫入該文件時調用pigz,對包含header值在內的完整內容進行壓縮處理:

$ mkdir .git/objects/5f
$ echo -ne 'blob 19\0What is inside Git?' | pigz -cz > .git/objects/5f/e76ad3e039c1fdc74201715f8c150bed50e351

我們爲pigz指定了兩個參數:參數-c表示將所有信息輸出到標準輸出設備。當然,因爲我們使用了重定向符,所以標準輸出設備的輸出最終被存成了文件。另一個參數-z,則指示pigz在進行壓縮處理時採用zlib的deflate算法。默認情況下pigz使用的是gzip算法,兩者在格式上有所不同。

如果此時我們執行git cat-file查看該Git對象的內容:

$ git cat-file -p 5fe76ad3e039c1fdc74201715f8c150bed50e351
What is inside Git?

就會發現,返回的結果和我們用pigz進行壓縮處理之前的內容正文是一摸一樣的。這和我們用pigz的-d參數對Git對象進行解壓得到的結果也完全一致:

$ pigz -d < .git/objects/5f/e76ad3e039c1fdc74201715f8c150bed50e351
blob 19What is inside Git

這裏的返回結果包含了header字段和內容正文,中間的’\0’由於是不可見字符,所以沒有顯示出來。

好了,今天就先到這裏啦。在下篇文章裏,我們將討論Git中的另外兩種重要的對象,它們分別是:Tree和Commit!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章