Git
章節 ▾ 第二版

10.2 Git 內部 - Git 物件

Git 物件

Git 是一個內容可定址的檔案系統。很好。那是什麼意思?這表示 Git 的核心是一個簡單的鍵值資料儲存區。這表示你可以將任何內容插入 Git 儲存庫,Git 會回傳一個獨特的金鑰,你稍後可以使用該金鑰來擷取該內容。

為了示範,讓我們看看底層命令 git hash-object,它會取得一些資料,將其儲存在你的 .git/objects 目錄(物件資料庫)中,並回傳一個獨特的金鑰,現在可以參考該資料物件。

首先,你初始化一個新的 Git 儲存庫,並驗證 objects 目錄中(可以預料)沒有任何內容

$ git init test
Initialized empty Git repository in /tmp/test/.git/
$ cd test
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f

Git 已初始化 objects 目錄並在其中建立了 packinfo 子目錄,但沒有常規檔案。現在,讓我們使用 git hash-object 來建立一個新的資料物件,並手動將其儲存在你的新 Git 資料庫中

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

在其最簡單的形式中,git hash-object 會取得你傳遞給它的內容,並僅傳回可用於將其儲存在 Git 資料庫中的唯一金鑰。-w 選項接著會指示命令不要僅傳回金鑰,而是將該物件寫入資料庫。最後,--stdin 選項會指示 git hash-object 從 stdin 取得要處理的內容;否則,命令會預期在命令結尾處會有一個包含要使用的內容的檔案名稱引數。

上述命令的輸出是一個 40 個字元的校驗和雜湊。這是 SHA-1 雜湊 - 你要儲存的內容加上標頭的校驗和,你將在稍後了解。現在你可以看到 Git 如何儲存你的資料

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

如果你再次檢查你的 objects 目錄,你可以看到它現在包含該新內容的檔案。這就是 Git 最初儲存內容的方式 - 每段內容都以單一檔案的方式儲存,並以內容及其標頭的 SHA-1 校驗和命名。子目錄以 SHA-1 的前 2 個字元命名,而檔案名稱則是剩餘的 38 個字元。

一旦你的物件資料庫中有內容,你可以使用 git cat-file 命令檢查該內容。此命令有點像用於檢查 Git 物件的瑞士刀。將 -p 傳遞給 cat-file 會指示命令首先找出內容類型,然後適當地顯示它

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

現在,您可以將內容新增至 Git 並再次提取出來。您也可以對檔案中的內容執行此操作。例如,您可以在檔案上執行一些簡單的版本控制。首先,建立一個新檔案並將其內容儲存在您的資料庫中

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

然後,將一些新內容寫入檔案,然後再次儲存

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

您的物件資料庫現在包含此新檔案的兩個版本(以及您儲存在那裡的第一個內容)

$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

此時,您可以刪除本地的 test.txt 檔案副本,然後使用 Git 從物件資料庫中檢索您儲存的第一個版本

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1

或第二個版本

$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2

但是記住檔案每個版本的 SHA-1 金鑰並不實際;此外,您並未將檔案名稱儲存在您的系統中 — 僅儲存內容。此物件類型稱為 *blob*。您可以讓 Git 透過其 SHA-1 金鑰,使用 git cat-file -t 告訴您 Git 中任何物件的物件類型

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

樹狀物件

我們要檢視的下一種 Git 物件類型是 *tree*,它解決了儲存檔案名稱的問題,並且還允許您將一組檔案一起儲存。Git 以類似於 UNIX 檔案系統的方式儲存內容,但稍作簡化。所有內容都儲存為樹狀 (tree) 和 blob 物件,其中樹狀物件對應於 UNIX 目錄條目,而 blob 物件或多或少對應於 inodes 或檔案內容。單個樹狀物件包含一個或多個條目,每個條目都是 blob 或子樹的 SHA-1 雜湊值,並帶有其關聯的模式、類型和檔案名稱。例如,假設您有一個專案,其中最新的樹狀結構看起來像這樣

$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859      README
100644 blob 8f94139338f9404f26296befa88755fc2598c289      Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0      lib

master^{tree} 語法指定由您 master 分支上的最後一個提交指向的樹狀物件。請注意,lib 子目錄不是 blob,而是指向另一個樹狀結構的指標

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b      simplegit.rb
注意

根據您使用的 shell,在使用 master^{tree} 語法時可能會遇到錯誤。

在 Windows 的 CMD 中,^ 字元用於跳脫,因此您必須將其加倍以避免這種情況:git cat-file -p master^^{tree}。使用 PowerShell 時,使用 {} 字元的參數必須加上引號,以避免參數被錯誤地解析:git cat-file -p 'master^{tree}'

如果您使用 ZSH,^ 字元用於 globbing,因此您必須將整個運算式括在引號中:git cat-file -p "master^{tree}"

從概念上講,Git 正在儲存的資料看起來像這樣

Simple version of the Git data model
圖 173. Git 資料模型的簡單版本

您可以相當容易地建立自己的樹狀結構。Git 通常透過取得您的暫存區域或索引的狀態並從中寫出一系列的樹狀物件來建立樹狀結構。因此,要建立樹狀物件,您首先必須透過暫存一些檔案來設定索引。要建立一個帶有單個條目的索引 — 您的 test.txt 檔案的第一個版本 — 您可以使用 plumbing 命令 git update-index。您可以使用此命令將 test.txt 檔案的早期版本人為地新增至新的暫存區域。您必須傳遞 --add 選項,因為該檔案尚未存在於您的暫存區域中(您甚至還沒有設定暫存區域),並且傳遞 --cacheinfo 選項,因為您要新增的檔案不在您的目錄中,而是在您的資料庫中。然後,您指定模式、SHA-1 和檔案名稱

$ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

在本例中,您指定模式為 100644,表示它是一個普通檔案。其他選項有 100755,表示它是一個可執行檔案;而 120000 則指定一個符號連結。模式取自普通的 UNIX 模式,但靈活性較差 — 這三種模式是 Git 中檔案 (blob) 唯一有效的模式(儘管其他模式用於目錄和子模組)。

現在,您可以使用 git write-tree 將暫存區域寫出到樹狀物件。不需要 -w 選項 — 如果該樹狀結構尚不存在,則呼叫此命令會自動根據索引的狀態建立一個樹狀物件

$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt

您也可以使用之前看到的同一個 git cat-file 命令來驗證這是否是樹狀物件

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

您現在將使用 test.txt 的第二個版本和一個新檔案建立一個新的樹狀結構

$ echo 'new file' > new.txt
$ git update-index --cacheinfo 100644 \
  1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt

您的暫存區域現在包含新版本的 test.txt 以及新檔案 new.txt。寫出該樹狀結構(將暫存區域或索引的狀態記錄到樹狀物件),看看它長什麼樣子

$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

請注意,此樹狀結構同時具有檔案條目,並且 test.txt SHA-1 是來自稍早的「版本 2」SHA-1 (1f7a7a)。為了好玩,您將把第一個樹狀結構作為子目錄新增到此樹狀結構中。您可以透過呼叫 git read-tree 將樹狀結構讀入您的暫存區域。在本例中,您可以使用此命令的 --prefix 選項將現有的樹狀結構作為子樹讀入您的暫存區域

$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579      bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

如果您從您剛寫入的新樹狀結構建立工作目錄,您將在工作目錄的頂層獲得兩個檔案,以及一個名為 bak 的子目錄,其中包含 test.txt 檔案的第一個版本。您可以將 Git 為這些結構包含的資料視為如下所示

The content structure of your current Git data
圖 174. 您目前的 Git 資料的內容結構

提交物件

如果您已完成以上所有操作,您現在將擁有三個代表您要追蹤的專案不同快照的樹狀結構,但之前的問題仍然存在:您必須記住所有三個 SHA-1 值才能回憶起這些快照。您也沒有任何關於誰儲存了快照、何時儲存了快照或為何儲存了快照的資訊。這是提交物件為您儲存的基本資訊。

要建立提交物件,您可以呼叫 commit-tree 並指定一個樹狀 SHA-1,以及直接位於其之前的任何提交物件(如果有的話)。從您寫入的第一個樹狀結構開始

$ echo 'First commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d
注意

由於建立時間和作者資料不同,您將獲得不同的雜湊值。此外,雖然原則上任何提交物件都可以在給定該資料的情況下精確地重現,但本書結構的歷史細節意味著列印的提交雜湊可能與給定的提交不符。在本章的後面,請將提交和標籤雜湊替換為您自己的校驗和。

現在,您可以使用 git cat-file 查看您的新提交物件

$ git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700

First commit

提交物件的格式很簡單:它指定該點專案快照的頂層樹狀結構;父提交(如果有的話)(上述提交物件沒有任何父項);作者/提交者資訊(使用您的 user.nameuser.email 設定和時間戳記);一個空白行,然後是提交訊息。

接下來,您將寫入另外兩個提交物件,每個物件都參考直接在其之前的提交

$ echo 'Second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'Third commit'  | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9

這三個提交物件中的每一個都指向您建立的三個快照樹狀結構之一。奇怪的是,您現在擁有一個真正的 Git 歷史記錄,如果您在最後一個提交 SHA-1 上執行該命令,則可以使用 git log 命令查看該歷史記錄

$ git log --stat 1a410e
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:15:24 2009 -0700

	Third commit

 bak/test.txt | 1 +
 1 file changed, 1 insertion(+)

commit cac0cab538b970a37ea1e769cbbde608743bc96d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:14:29 2009 -0700

	Second commit

 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:09:34 2009 -0700

    First commit

 test.txt | 1 +
 1 file changed, 1 insertion(+)

太棒了。您剛才完成了低階操作以建立 Git 歷史記錄,而沒有使用任何前端命令。這基本上是當您執行 git addgit commit 命令時 Git 所做的事情 — 它為已變更的檔案儲存 blob、更新索引、寫出樹狀結構,並寫出參考頂層樹狀結構和緊接著在其之前的提交的提交物件。這三個主要的 Git 物件 — blob、樹狀結構和提交 — 最初以單獨的檔案形式儲存在您的 .git/objects 目錄中。以下是範例目錄中的所有物件,並附有它們儲存的內容

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1

如果您遵循所有內部指標,您將獲得類似於這樣的物件圖

All the reachable objects in your Git directory
圖 175. 您 Git 目錄中所有可存取的物件

物件儲存

我們稍早提到,每個您提交到 Git 物件資料庫的物件都會儲存一個標頭。讓我們花一點時間來看看 Git 如何儲存其物件。您將看到如何使用 Ruby 指令碼語言以互動方式儲存 blob 物件 — 在本例中,字串「what is up, doc?」。

您可以使用 irb 命令啟動互動式 Ruby 模式

$ irb
>> content = "what is up, doc?"
=> "what is up, doc?"

Git 首先建構一個標頭,該標頭首先識別物件的類型 — 在本例中,是一個 blob。在標頭的第一個部分,Git 會加入一個空格,然後是內容的大小(以位元組為單位),並加入一個最後的 null 位元組

>> header = "blob #{content.bytesize}\0"
=> "blob 16\u0000"

Git 將標頭和原始內容串連在一起,然後計算新內容的 SHA-1 校驗和。您可以透過包含 SHA1 摘要程式庫並使用 require 命令,然後使用字串呼叫 Digest::SHA1.hexdigest() 來計算 Ruby 中字串的 SHA-1 值

>> store = header + content
=> "blob 16\u0000what is up, doc?"
>> require 'digest/sha1'
=> true
>> sha1 = Digest::SHA1.hexdigest(store)
=> "bd9dbf5aae1a3862dd1526723246b20206e5fc37"

讓我們將其與 git hash-object 的輸出進行比較。在這裡,我們使用 echo -n 以防止在輸入中新增換行符號。

$ echo -n "what is up, doc?" | git hash-object --stdin
bd9dbf5aae1a3862dd1526723246b20206e5fc37

Git 使用 zlib 壓縮新內容,您可以使用 zlib 程式庫在 Ruby 中執行此操作。首先,您需要載入該程式庫,然後在內容上執行 Zlib::Deflate.deflate()

>> require 'zlib'
=> true
>> zlib_content = Zlib::Deflate.deflate(store)
=> "x\x9CK\xCA\xC9OR04c(\xCFH,Q\xC8,V(-\xD0QH\xC9O\xB6\a\x00_\x1C\a\x9D"

最後,您將把 zlib 壓縮的內容寫入磁碟上的物件。您將確定要寫出的物件的路徑(SHA-1 值的前兩個字元是子目錄名稱,最後 38 個字元是該目錄中的檔案名稱)。在 Ruby 中,您可以使用 FileUtils.mkdir_p() 函數來建立子目錄(如果它不存在)。然後,使用 File.open() 開啟該檔案,並使用產生的檔案控制代碼上的 write() 呼叫將先前 zlib 壓縮的內容寫出到該檔案

>> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
=> ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37"
>> require 'fileutils'
=> true
>> FileUtils.mkdir_p(File.dirname(path))
=> ".git/objects/bd"
>> File.open(path, 'w') { |f| f.write zlib_content }
=> 32

讓我們使用 git cat-file 檢查物件的內容

---
$ git cat-file -p bd9dbf5aae1a3862dd1526723246b20206e5fc37
what is up, doc?
---

就是這樣 — 您已建立一個有效的 Git blob 物件。

所有 Git 物件都以相同的方式儲存,只是類型不同 — 標頭將以 commit 或 tree 開頭,而不是字串 blob。此外,雖然 blob 內容幾乎可以是任何內容,但 commit 和 tree 內容的格式非常具體。

scroll-to-top