Git 內幕(一)

_墨白發表於2018-09-07

前言

git 是一種程式設計師幾乎每天都會用到的工具,給我們程式碼管理帶去了極大的方便。以往的 git 介紹,多是介紹git 的高階命令,如git rebsegit cherry-pickergit bisect等,少有看到剖析git 內部原理的。原因也很簡單,即使對 git 的原理不甚瞭解,也並不會影響我們熟練使用 git。但是很多事我們不光要知其然,更要知其所以然,方能舉一反三。

概念

在介紹 git 的原理之前,先介紹幾個基本概念和會用到的命令,以便大家在閱讀文章時能更加輕鬆地理解。

  • BLOB (binary large object):二進位制大物件,是一個可以儲存二進位制檔案的容器。
  • three git status:
  1. Untracked:檔案還未被git add,對應工作區(Working Directory)。
  2. Staged:git add 後所處的狀態, 對應暫存區(Stage)。
  3. 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 時,因為對 stagebranch 等概念不夠了解,還做出過備份整個專案的蠢事。其實如果真要做備份,備份當前的 .git 檔案即可。

下圖為剛初始化的 .git 的目錄結構(不同版本的 git 會有所不同),幾乎所有的 git 操作和儲存都位於該目錄下。本次真要介紹四個核心檔案或目錄:HEAD 及 index 檔案,objects 及 refs 目錄。

Git 內幕(一)

簡而言之,git 從核心上來看不過是簡單地儲存鍵值對(key-value)。它允許插入任意型別的內容,並會返回一個鍵值,通過該鍵值可以在任何時候再取出該內容。

git add

先丟擲一個問題:一個空的資料夾是否能新增到 git 專案中?很多人可能都知道答案,但是為什麼吶?通過對 git add 背後原理的解析,相信你可以得出一個確切的答案。

  1. 初始化
$ 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 目錄,也沒有發生任何變化。

  1. 新增資料夾到暫存區
$ 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 內幕(一)

之前說過,git 的儲存其實是以鍵值對的形式存在的。index是一個列表,每一行維護一個檔名到 BLOB 雜湊值的對映。objects 目錄下存放的就是 value,這個BLOB檔案包含了data/letter.txt 檔案壓縮後的內容,並以內容的雜湊值作為檔名。雜湊是一段演算法,它將給定內容轉換為更小的,且能唯一確定原內容的值。 我曾經嘗試直接開啟這些檔案,但是開啟都是一串亂碼。可以通過十六進位制編碼的方式開啟這些檔案,但是畢竟不是機器,想要看明白還是有一些難度的?。 Git 內幕(一)

Git 內幕(一)

幸好 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 下的三個檔案。

Git 內幕(一)

提交命令其實包含了三個步驟:

  1. 建立提交版本對應檔案的 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

Git 內幕(一)

那麼問題來了,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 目錄,圖示如下:

Git 內幕(一)
  1. 建立一個提交物件 我們在提交時輸入了 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 內幕(一)
  1. 將當前分支指向新提交 到了這裡還存在最後一個問題,分支。 git 怎麼知道現在的提交應該指向哪個分支?答案在 HEAD 檔案中:ref: refs/heads/master。HEAD 指向 master,master 就是我們當前分支。因為是第一個提交,代表master引用的檔案還不存在。git 會自動建立 .git/refs/heads/master ,並寫入提交物件的雜湊值:b83b66096fb5b5225ec0c5741f45d334ceb94046
Git 內幕(一)

最後,所有 BLOB 檔案所儲存的資訊都通過 tree 物件串聯了起來,就好比 key-value 的形式。

再來回答下開頭提出的問題吧,一個空的資料夾是否能新增到 git 專案中? 答:不可以。因為 git 使用的索引機制,是以檔案為最小單位儲存內容,跟蹤變化的。 那麼怎麼做才可以使這個資料夾存在吶?通常的做法是在裡面新建一個名為 .gitkeep 的檔案。

參考連結

相關文章