Git內部原理深入解析Git對象

      網友投稿 795 2022-05-29

      一、底層命令與上層命令

      Git 有很多的子命令,例如 checkout、branch、remote 等,然而由于 Git 最初是一套面向版本控制系統的工具集,而不是一個完整的、用戶友好的版本控制系統,所以它還包含了一部分用于完成底層工作的子命令,這些命令被設計成能以 UNIX 命令行的風格連接在一起,抑或藉由腳本調用,來完成工作,這部分命令一般被稱作“底層(plumbing)”命令,而那些更友好的命令則被稱作“上層(porcelain)”命令。

      底層命令得以窺探 Git 內部的工作機制,也有助于說明 Git 是如何完成工作的,以及它為何如此運作,多數底層命令并不面向最終用戶:它們更適合作為新工具的組件和自定義腳本的組成部分。

      當在一個新目錄或已有目錄執行 git init 時,Git 會創建一個 .git 目錄,這個目錄包含了幾乎所有 Git 存儲和操作的東西。如若想備份或復制一個版本庫,只需把這個目錄拷貝至另一處即可。新初始化的 .git 目錄的典型結構如下:

      $ ls -F1 config description HEAD hooks/ info/ objects/ refs/

      1

      2

      3

      4

      5

      6

      7

      8

      隨著 Git 版本的不同,該目錄下可能還會包含其他內容,不過對于一個全新的 git init 版本庫,這將是看到的默認結構,description 文件僅供 GitWeb 程序使用,無需關心,config 文件包含項目特有的配置選項,info 目錄包含一個全局性排除(global exclude)文件,用以放置那些不希望被記錄在 .gitignore 文件中的忽略模式(ignored patterns),hooks 目錄包含客戶端或服務端的鉤子腳本(hook scripts)。

      剩下的四個條目很重要:HEAD 文件、(尚待創建的)index 文件,和 objects 目錄、refs 目錄,它們都是 Git 的核心組成部分:objects 目錄存儲所有數據內容;refs 目錄存儲指向數據(分支、遠程倉庫和標簽等)的提交對象的指針;HEAD 文件指向目前被檢出的分支;index 文件保存暫存區信息。

      二、Git 的核心部分

      Git 是一個內容尋址文件系統,聽起來很酷,但這是什么意思呢? 這意味著,Git 的核心部分是一個簡單的鍵值對數據庫(key-value data store),可以向 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

      1

      2

      3

      4

      5

      6

      7

      8

      可以看到 Git 對 objects 目錄進行了初始化,并創建了 pack 和 info 子目錄,但均為空。

      接著,用 git hash-object 創建一個新的數據對象并將它手動存入新 Git 數據庫中:

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

      1

      2

      在這種最簡單的形式中,git hash-object 會接受我們傳給它的東西,而它只會返回可以存儲在 Git 倉庫中的唯一鍵。-w 選項會指示該命令不要只返回鍵,還要將該對象寫入數據庫中。最后,–stdin 選項則指示該命令從標準輸入讀取內容;若不指定此選項,則須在命令尾部給出待存儲文件的路徑。

      此命令輸出一個長度為 40 個字符的校驗和,這是一個 SHA-1 哈希值,一個將待存儲的數據外加一個頭部信息(header)一起做 SHA-1 校驗運算而得的校驗和。現在可以查看 Git 是如何存儲數據的:

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

      1

      2

      如果再次查看 objects 目錄,那么可以在其中找到一個與新內容對應的文件,這就是開始時 Git 存儲內容的方式,一個文件對應一條內容,以該內容加上特定頭部信息一起的 SHA-1 校驗和為文件命名,校驗和的前兩個字符用于命名子目錄,余下的 38 個字符則用作文件名。

      一旦將內容存儲在了對象數據庫中,那么可以通過 cat-file 命令從 Git 那里取回數據,這個命令簡直就是一把剖析 Git 對象的瑞士軍刀,為 cat-file 指定 -p 選項可指示該命令自動判斷內容的類型,并為我們顯示大致的內容:

      $ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4 test content

      1

      2

      至此,已經掌握了如何向 Git 中存入內容,以及如何將它們取出。同樣,可以將這些操作應用于文件中的內容,例如,可以對一個文件進行簡單的版本控制。首先,創建一個新文件并將其內容存入數據庫:

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

      1

      2

      3

      接著,向文件里寫入新內容,并再次將其存入數據庫:

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

      1

      2

      3

      對象數據庫記錄下了該文件的兩個不同版本,當然之前我們存入的第一條內容也還在:

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

      1

      2

      3

      4

      現在可以在刪掉 test.txt 的本地副本,然后用 Git 從對象數據庫中取回它的第一個版本:

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

      1

      2

      3

      或者第二個版本:

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

      1

      2

      3

      然而,記住文件的每一個版本所對應的 SHA-1 值并不現實;另一個問題是,在這個(簡單的版本控制)系統中,文件名并沒有被保存,僅保存了文件的內容,上述類型的對象我們稱之為數據對象(blob object)。利用 git cat-file -t 命令,可以讓 Git 告訴我們其內部存儲的任何對象類型,只要給定該對象的 SHA-1 值:

      $ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob

      1

      2

      三、樹對象

      接下來探討的 Git 對象類型是樹對象(tree object),它能解決文件名保存的問題,也允許我們將多個文件組織到一起。Git 以一種類似于 UNIX 文件系統的方式存儲內容,但作了些許簡化,所有內容均以樹對象和數據對象的形式存儲,其中樹對象對應了 UNIX 中的目錄項,數據對象則大致上對應了 inodes 或文件內容。一個樹對象包含了一條或多條樹對象記錄(tree entry),每條記錄含有一個指向數據對象或者子樹對象的 SHA-1 指針,以及相應的模式、類型、文件名信息。例如,某項目當前對應的最新樹對象可能是這樣的:

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

      1

      2

      3

      4

      master^{tree} 語法表示 master 分支上最新的提交所指向的樹對象,不過要注意,lib 子目錄(所對應的那條樹對象記錄)并不是一個數據對象,而是一個指針,其指向的是另一個樹對象:

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

      1

      2

      可能會在某些 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 內部存儲的數據有點像這樣:

      可以輕松創建自己的樹對象,通常,Git 根據某一時刻暫存區(即 index 區域,下同)所表示的狀態創建并記錄一個對應的樹對象,如此重復便可依次記錄(某個時間段內)一系列的樹對象。因此,為創建一個樹對象,首先需要通過暫存一些文件來創建一個暫存區。可以通過底層命令 git update-index 為一個單獨文件,我們的 test.txt 文件的首個版本,創建一個暫存區。利用該命令,可以把 test.txt 文件的首個版本人為地加入一個新的暫存區,必須為上述命令指定 --add 選項,因為此前該文件并不在暫存區中(甚至都還沒來得及創建一個暫存區呢);同樣必需的還有 --cacheinfo 選項,因為將要添加的文件位于 Git 數據庫中,而不是位于當前目錄下。同時,需要指定文件模式、SHA-1 與文件名:

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

      1

      2

      本例中,指定的文件模式為 100644,表明這是一個普通文件,其他選擇包括:100755,表示一個可執行文件;120000,表示一個符號鏈接。這里的文件模式參考了常見的 UNIX 文件模式,但遠沒那么靈活,上述三種模式即是 Git 文件(即數據對象)的所有合法模式(當然,還有其他一些模式,但用于目錄項和子模塊)。

      現在,可以通過 git write-tree 命令將暫存區內容寫入一個樹對象,此處無需指定 -w 選項,如果某個樹對象此前并不存在的話,當調用此命令時,它會根據當前暫存區狀態自動創建一個新的樹對象:

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

      1

      2

      3

      4

      不妨用之前見過的 git cat-file 命令驗證一下它確實是一個樹對象:

      $ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree

      1

      2

      接著來創建一個新的樹對象,它包括 test.txt 文件的第二個版本,以及一個新的文件:

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

      1

      2

      3

      4

      暫存區現在包含了 test.txt 文件的新版本,和一個新文件:new.txt,記錄下這個目錄樹(將當前暫存區的狀態記錄為一個樹對象),然后觀察它的結構:

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

      1

      2

      3

      4

      5

      可以注意到,新的樹對象包含兩條文件記錄,同時 test.txt 的 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

      1

      2

      3

      4

      5

      6

      7

      如果基于這個新的樹對象創建一個工作目錄,可以發現工作目錄的根目錄包含兩個文件以及一個名為 bak 的子目錄,該子目錄包含 test.txt 文件的第一個版本。可以認為 Git 內部存儲著的用于表示上述結構的數據是這樣的:

      四、提交對象

      如果做完了以上所有操作,那么現在就有了三個樹對象,分別代表我們想要跟蹤的不同項目快照 然而問題依舊:若想重用這些快照,必須記住所有三個 SHA-1 哈希值。并且也完全不知道是誰保存了這些快照,在什么時刻保存的,以及為什么保存這些快照,而以上這些,正是提交對象(commit object)能保存的基本信息。

      可以通過調用 commit-tree 命令創建一個提交對象,為此需要指定一個樹對象的 SHA-1 值,以及該提交的父提交對象(如果有的話),從之前創建的第一個樹對象開始:

      $ echo 'first commit' | git commit-tree d8329f fdf4fc3344e67ab068f836878b6c4951e3b15f3d

      1

      2

      $ git cat-file -p fdf4fc3 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 author Scott Chacon 1243040974 -0700 committer Scott Chacon 1243040974 -0700 first commit

      1

      2

      3

      4

      5

      接著,將創建另兩個提交對象,它們分別引用各自的上一個提交(作為其父提交對象):

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

      1

      2

      3

      4

      這三個提交對象分別指向之前創建的三個樹對象快照中的一個。現在,如果對最后一個提交的 SHA-1 值運行 git log 命令,會出乎意料的發現,已有一個貨真價實的、可由 git log 查看的 Git 提交歷史了:

      $ git log --stat 1a410e commit 1a410efbd13591db07496601ebc7a059dd55cfe9 Author: Scott Chacon 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 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 Date: Fri May 22 18:09:34 2009 -0700 first commit test.txt | 1 + 1 file changed, 1 insertion(+)

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      太神奇了: 就在剛才,沒有借助任何上層命令,僅憑幾個底層操作便完成了一個 Git 提交歷史的創建,這就是每次運行 git add 和 git commit 命令時,Git 所做的工作實質就是將被改寫的文件保存為數據對象,更新暫存區,記錄樹對象,最后創建一個指明了頂層樹對象和父提交的提交對象。這三種主要的 Git 對象——數據對象、樹對象、提交對象,最初均以單獨文件的形式保存在 .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

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      如果跟蹤所有的內部指針,將得到一個類似下面的對象關系圖:

      五、對象存儲

      上文曾提及,向 Git 倉庫提交的所有對象都會有個頭部信息一并被保存,讓我們略花些時間來看看 Git 是如何存儲其對象的,通過在 Ruby 腳本語言中交互式地演示,將看到一個數據對象。本例中是字符串“what is up, doc?”,是如何被存儲的呢?

      可以通過 irb 命令啟動 Ruby 的交互模式:

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

      1

      2

      3

      Git 首先會以識別出的對象的類型作為開頭來構造一個頭部信息,本例中是一個“blob”字符串,接著 Git 會在頭部的第一部分添加一個空格,隨后是數據內容的字節數,最后是一個空字節(null byte):

      >> header = "blob #{content.length}

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

      " => "blob 16\u0000"

      1

      2

      Git 會將上述頭部信息和原始數據拼接起來,并計算出這條新內容的 SHA-1 校驗和,在 Ruby 中可以這樣計算 SHA-1 值,先通過 require 命令導入 SHA-1 digest 庫,然后對目標字符串調用 Digest::SHA1.hexdigest():

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

      1

      2

      3

      4

      5

      6

      來比較一下 git hash-object 的輸出,這里使用了 echo -n 以避免在輸出中添加換行。

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

      1

      2

      Git 會通過 zlib 壓縮這條新內容,在 Ruby 中可以借助 zlib 庫做到這一點,先導入相應的庫,然后對目標內容調用 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"

      1

      2

      3

      4

      最后,需要將這條經由 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

      1

      2

      3

      4

      5

      6

      7

      8

      用 git cat-file 查看一下該對象的內容:

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

      1

      Git內部原理之深入解析Git對象

      2

      3

      4

      就是這樣,已創建了一個有效的 Git 數據對象。

      所有的 Git 對象均以這種方式存儲,區別僅在于類型標識,另兩種對象類型的頭部信息以字符串“commit”或“tree”開頭,而不是“blob”。另外,雖然數據對象的內容幾乎可以是任何東西,但提交對象和樹對象的內容卻有各自固定的格式。

      Git 數據庫

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:UNIX 環境高級編程|進程控制(下)
      下一篇:數據結構溢彩加強版——(二)算法篇
      相關文章
      亚洲成a人片77777kkkk| 4338×亚洲全国最大色成网站| 国产成人亚洲影院在线观看| 激情婷婷成人亚洲综合| 亚洲人成自拍网站在线观看| 亚洲一级毛片免观看| 亚洲乱码卡三乱码新区| 亚洲视频欧洲视频| 亚洲视频中文字幕| 久久精品国产精品亚洲毛片| 亚洲av无码成人黄网站在线观看| 亚洲女久久久噜噜噜熟女| 亚洲精品美女久久777777| 亚洲国产综合无码一区| 国产成人A人亚洲精品无码| 国产∨亚洲V天堂无码久久久| 国产精品国产亚洲精品看不卡| 亚洲国产精品成人久久| 亚洲国产精品久久久天堂| 亚洲av日韩av不卡在线观看 | 亚洲AV永久纯肉无码精品动漫| 亚洲国产精华液网站w| 亚洲AV无码久久精品成人| 亚洲av日韩av无码| 中文字幕亚洲免费无线观看日本| 亚洲欧洲日产国码二区首页| 亚洲人成人77777在线播放| 亚洲AV成人一区二区三区在线看| 亚洲色在线无码国产精品不卡| 亚洲爆乳少妇无码激情| 九九精品国产亚洲AV日韩| 亚洲AV无码成人精品区大在线 | 亚洲色偷偷偷网站色偷一区| 亚洲国产成人久久精品app| 2020亚洲男人天堂精品| 亚洲精品成a人在线观看夫| 爱情岛亚洲论坛在线观看 | 国产精品亚洲色图| 久久久久亚洲?V成人无码| 亚洲成a人片在线观看日本| 亚洲美女视频网站|