前言
git 是一種程式設計師幾乎每天都會用到的工具,給我們程式碼管理帶去了極大的方便。以往的 git 介紹,多是介紹git 的高階命令,如git rebse
、git cherry-picker
、git bisect
等,少有看到剖析git 內部原理的。原因也很簡單,即使對 git 的原理不甚瞭解,也並不會影響我們熟練使用 git。但是很多事我們不光要知其然,更要知其所以然,方能舉一反三。
概念
在介紹 git 的原理之前,先介紹幾個基本概念和會用到的命令,以便大家在閱讀文章時能更加輕鬆地理解。
- BLOB (binary large object):二進位制大物件,是一個可以儲存二進位制檔案的容器。
- three git status:
- Untracked:檔案還未被
git add
,對應工作區(Working Directory)。 - Staged:
git add
後所處的狀態, 對應暫存區(Stage)。 - Committed:
git commite
後所處的狀態,對應版本庫(Commit History)。
git cat-file
:檢視 git 物件的瑞士軍刀,能轉譯二進位制檔案為可讀檔案。git hash-object -w filename
:檢視到 git 已儲存的資料hexdump -C filename
:檢視二進位制檔案的十六進位制編碼
.git 內幕
當你在一個新目錄或已有目錄內執行 git init
時,git 會建立一個 .git 目錄,幾乎所有 git 儲存和操作的內容都位於該目錄下。記得在剛接觸 git 時,因為對 stage
、branch
等概念不夠了解,還做出過備份整個專案的蠢事。其實如果真要做備份,備份當前的 .git 檔案即可。
下圖為剛初始化的 .git 的目錄結構(不同版本的 git 會有所不同),幾乎所有的 git 操作和儲存都位於該目錄下。本次真要介紹四個核心檔案或目錄:HEAD 及 index 檔案,objects 及 refs 目錄。
簡而言之,git 從核心上來看不過是簡單地儲存鍵值對(key-value)。它允許插入任意型別的內容,並會返回一個鍵值,通過該鍵值可以在任何時候再取出該內容。
git add
先丟擲一個問題:一個空的資料夾是否能新增到 git 專案中?很多人可能都知道答案,但是為什麼吶?通過對 git add
背後原理的解析,相信你可以得出一個確切的答案。
- 初始化
$ mkdir alpha && cd alpha
$ git init
$ mkdir data
複製程式碼
通過執行上面的命令,我們建立了一個名為 alpha
的資料夾,並且將其初始化為一個 git 專案,再新建一個 data
空資料夾。通過執行 git add data
命令希望把 data
空資料夾新增到暫存區中,發現執行後並沒有發生任何變化,執行 git status
檢視倉庫的狀態,丟擲這樣一段提示:
$ git status
On branch master
No commits yet
nothing to commit (create/copy files and use "git add" to track)
複製程式碼
如果在 data
資料夾內再新建一個空資料夾又會如何吶,發現還是沒有任何變化。這樣就得出了開頭那個問題的答案,空的資料夾是無法新增到 git 專案中的。再檢視一下 .git
目錄,也沒有發生任何變化。
- 新增資料夾到暫存區
$ echo 'a' > data/letter.txt
$ git status
Untracked files:
(use "git add <file>..." to include in what will be committed)
data/
nothing added to commit but untracked files present (use "git add" to track)
複製程式碼
在 data
目錄下新建一個名為 letter.text
的檔案,寫入 a
。發現倉庫的狀態發生了變化,但是 .git
目錄並沒有發生變化,說明在工作區的操作並不會產生 git 歷史記錄。
執行 git add data
命令,發現 .git
目錄終於發生變化了。 分別多了 index
檔案和 objects/78/...
檔案。
之前說過,git 的儲存其實是以鍵值對的形式存在的。index
是一個列表,每一行維護一個檔名到 BLOB 雜湊值的對映。objects
目錄下存放的就是 value,這個BLOB檔案包含了data/letter.txt
檔案壓縮後的內容,並以內容的雜湊值作為檔名。雜湊是一段演算法,它將給定內容轉換為更小的,且能唯一確定原內容的值。
我曾經嘗試直接開啟這些檔案,但是開啟都是一串亂碼。可以通過十六進位制編碼的方式開啟這些檔案,但是畢竟不是機器,想要看明白還是有一些難度的?。
幸好 git 提供了一把瑞士軍刀(git cat-file
)給我們使用,可以將資料內容取回。
列印出 data/letter.txt
中的內容為 a
。
$ git cat-file -p 78981922613b2afb6025042ff6bd878ac1994e85
a
複製程式碼
至於 git/index
中的內容,可以通過 git ls-file
檢視。
$ git ls-file --stage
100644 78981922613b2afb6025042ff6bd878ac1994e85 0 data/letter.txt
複製程式碼
正如之前所說的,index
檔案存放的是一個列表,記錄了雜湊值對應的檔案。7898
這個 BLOB 檔案儲存的是 data/letter.txt
中的內容。objects
目錄下的結點是不可變的。這意味著它的內容可以編輯,但不能刪除。新增的檔案內容和建立的提交都儲存在 objects
目錄下。(注:git prune
刪除所有不能被ref訪問到的物件,執行此命令可能會丟失資料。)
git commit
$ git commit -m 'a1'
[master (root-commit) b83b660] a1
1 file changed, 1 insertion(+)
create mode 100644 data/letter.txt
$ git status
On branch master
nothing to commit, working tree clean
複製程式碼
執行 git commit
命令後,檔案被儲存到了版本庫中,倉庫處於 clean
的狀態。再看看 .git
目錄,發現多了 logs
目錄和 objects
下的三個檔案。
提交命令其實包含了三個步驟:
- 建立提交版本對應檔案的 tree 物件
什麼是 tree 物件,tree 物件可以儲存檔名,同時也允許儲存一組檔案。一個單獨的 tree 物件包含一條或多條 tree 記錄,每一條記錄含有一個指向 BLOB 或子 tree 物件的雜湊值。
建立新提交後,對應data目錄的tree物件如下:記錄了
data/letter.txt
檔案的所有資訊,我們可以使用這些資訊來恢復data/letter.txt
檔案。空格分隔的第一部分表示該檔案的許可權,表明是一個普通檔案;第二部分表示該記錄對應的是一個 BLOB 物件,第三部分是該BLOB 的雜湊值,第四部分記錄了檔名。
$ git cat-file -p e908cfc6e086c91a073c55a6882adebfc9c4520c
100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 letter.txt
複製程式碼
用圖示表式即為:data
目錄對應的 tree 物件指向對應data/letter.txt
那麼問題來了,data 並非根目錄,檔案的指向肯定是從根目錄開始。
$ git cat-file -p master^{tree}
040000 tree e908cfc6e086c91a073c55a6882adebfc9c4520c data
$ git cat-file -p 9745002f161a1be75bf65f869ab16743da2a6fda
040000 tree e908cfc6e086c91a073c55a6882adebfc9c4520c data
複製程式碼
master^{tree}
表示 master (當前)分支上最新提交指向的 tree 物件。從給出的結果中可以看到根目錄(root)指向了 data
目錄,圖示如下:
- 建立一個提交物件
我們在提交時輸入了
commit message
等資訊,這些資訊自然也存放在objects
目錄下:
$ git cat-file -p b83b66096fb5b5225ec0c5741f45d334ceb94046
tree 9745002f161a1be75bf65f869ab16743da2a6fda
author Bill Qiu <fanglaiq@gmail.com> 1536220281 +0800
committer Bill Qiu <fanglaiq@gmail.com> 1536220281 +0800
a1
複製程式碼
第一行指向一個tree物件。通過這裡的雜湊值,我們可以找到對應工作區根目錄(即 alpha 目錄)的 tree 物件,最後一行是提交資訊。
- 將當前分支指向新提交
到了這裡還存在最後一個問題,分支。 git 怎麼知道現在的提交應該指向哪個分支?答案在
HEAD
檔案中:ref: refs/heads/master
。HEAD 指向 master,master 就是我們當前分支。因為是第一個提交,代表master引用的檔案還不存在。git 會自動建立.git/refs/heads/master
,並寫入提交物件的雜湊值:b83b66096fb5b5225ec0c5741f45d334ceb94046
。
最後,所有 BLOB 檔案所儲存的資訊都通過 tree 物件串聯了起來,就好比 key-value
的形式。
再來回答下開頭提出的問題吧,一個空的資料夾是否能新增到 git 專案中?
答:不可以。因為 git 使用的索引機制,是以檔案為最小單位儲存內容,跟蹤變化的。
那麼怎麼做才可以使這個資料夾存在吶?通常的做法是在裡面新建一個名為 .gitkeep
的檔案。