Git 對(duì)象

2018-07-10 17:33 更新

Git 對(duì)象

Git 是一套內(nèi)容尋址文件系統(tǒng)。很不錯(cuò)。不過(guò)這是什么意思呢? 這種說(shuō)法的意思是,Git 從核心上來(lái)看不過(guò)是簡(jiǎn)單地存儲(chǔ)鍵值對(duì)(key-value)。它允許插入任意類(lèi)型的內(nèi)容,并會(huì)返回一個(gè)鍵值,通過(guò)該鍵值可以在任何時(shí)候再取出該內(nèi)容。可以通過(guò)底層命令 hash-object 來(lái)示范這點(diǎn),傳一些數(shù)據(jù)給該命令,它會(huì)將數(shù)據(jù)保存在 .git 目錄并返回表示這些數(shù)據(jù)的鍵值。首先初使化一個(gè) Git 倉(cāng)庫(kù)并確認(rèn) objects 目錄是空的:

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

Git 初始化了 objects 目錄,同時(shí)在該目錄下創(chuàng)建了 pack 和 info 子目錄,但是該目錄下沒(méi)有其他常規(guī)文件。我們往這個(gè) Git 數(shù)據(jù)庫(kù)里存儲(chǔ)一些文本:

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

參數(shù) -w 指示 hash-object 命令存儲(chǔ) (數(shù)據(jù)) 對(duì)象,若不指定這個(gè)參數(shù)該命令僅僅返回鍵值。--stdin 指定從標(biāo)準(zhǔn)輸入設(shè)備 (stdin) 來(lái)讀取內(nèi)容,若不指定這個(gè)參數(shù)則需指定一個(gè)要存儲(chǔ)的文件的路徑。該命令輸出長(zhǎng)度為 40 個(gè)字符的校驗(yàn)和。這是個(gè) SHA-1 哈希值──其值為要存儲(chǔ)的數(shù)據(jù)加上你馬上會(huì)了解到的一種頭信息的校驗(yàn)和?,F(xiàn)在可以查看到 Git 已經(jīng)存儲(chǔ)了數(shù)據(jù):

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

可以在 objects 目錄下看到一個(gè)文件。這便是 Git 存儲(chǔ)數(shù)據(jù)內(nèi)容的方式──為每份內(nèi)容生成一個(gè)文件,取得該內(nèi)容與頭信息的 SHA-1 校驗(yàn)和,創(chuàng)建以該校驗(yàn)和前兩個(gè)字符為名稱(chēng)的子目錄,并以 (校驗(yàn)和) 剩下 38 個(gè)字符為文件命名 (保存至子目錄下)。

通過(guò) cat-file 命令可以將數(shù)據(jù)內(nèi)容取回。該命令是查看 Git 對(duì)象的瑞士軍刀。傳入 -p 參數(shù)可以讓該命令輸出數(shù)據(jù)內(nèi)容的類(lèi)型:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

可以往 Git 中添加更多內(nèi)容并取回了。也可以直接添加文件。比方說(shuō)可以對(duì)一個(gè)文件進(jìn)行簡(jiǎn)單的版本控制。首先,創(chuàng)建一個(gè)新文件,并把文件內(nèi)容存儲(chǔ)到數(shù)據(jù)庫(kù)中:

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

接著往該文件中寫(xiě)入一些新內(nèi)容并再次保存:

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

數(shù)據(jù)庫(kù)中已經(jīng)將文件的兩個(gè)新版本連同一開(kāi)始的內(nèi)容保存下來(lái)了:

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

再將文件恢復(fù)到第一個(gè)版本:

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

或恢復(fù)到第二個(gè)版本:

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

需要記住的是幾個(gè)版本的文件 SHA-1 值可能與實(shí)際的值不同,其次,存儲(chǔ)的并不是文件名而僅僅是文件內(nèi)容。這種對(duì)象類(lèi)型稱(chēng)為 blob 。通過(guò)傳遞 SHA-1 值給 cat-file -t 命令可以讓 Git 返回任何對(duì)象的類(lèi)型:

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

tree (樹(shù)) 對(duì)象

接下去來(lái)看 tree 對(duì)象,tree 對(duì)象可以存儲(chǔ)文件名,同時(shí)也允許存儲(chǔ)一組文件。Git 以一種類(lèi)似 UNIX 文件系統(tǒng)但更簡(jiǎn)單的方式來(lái)存儲(chǔ)內(nèi)容。所有內(nèi)容以 tree 或 blob 對(duì)象存儲(chǔ),其中 tree 對(duì)象對(duì)應(yīng)于 UNIX 中的目錄,blob 對(duì)象則大致對(duì)應(yīng)于 inodes 或文件內(nèi)容。一個(gè)單獨(dú)的 tree 對(duì)象包含一條或多條 tree 記錄,每一條記錄含有一個(gè)指向 blob 或子 tree 對(duì)象的 SHA-1 指針,并附有該對(duì)象的權(quán)限模式 (mode)、類(lèi)型和文件名信息。以 simplegit 項(xiàng)目為例,最新的 tree 可能是這個(gè)樣子:

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

master^{tree} 表示 branch 分支上最新提交指向的 tree 對(duì)象。請(qǐng)注意 lib 子目錄并非一個(gè) blob 對(duì)象,而是一個(gè)指向另一個(gè) tree 對(duì)象的指針:

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

從概念上來(lái)講,Git 保存的數(shù)據(jù)如圖 9-1 所示。

圖 9-1. Git 對(duì)象模型的簡(jiǎn)化版

你可以自己創(chuàng)建 tree 。通常 Git 根據(jù)你的暫存區(qū)域或 index 來(lái)創(chuàng)建并寫(xiě)入一個(gè) tree 。因此要?jiǎng)?chuàng)建一個(gè) tree 對(duì)象的話首先要通過(guò)將一些文件暫存從而創(chuàng)建一個(gè) index ??梢允褂?plumbing 命令 update-index 為一個(gè)單獨(dú)文件 ── test.txt 文件的第一個(gè)版本 ── 創(chuàng)建一個(gè) index 。通過(guò)該命令人為的將 test.txt 文件的首個(gè)版本加入到了一個(gè)新的暫存區(qū)域中。由于該文件原先并不在暫存區(qū)域中 (甚至就連暫存區(qū)域也還沒(méi)被創(chuàng)建出來(lái)呢) ,必須傳入 --add 參數(shù);由于要添加的文件并不在當(dāng)前目錄下而是在數(shù)據(jù)庫(kù)中,必須傳入 --cacheinfo 參數(shù)。同時(shí)指定了文件模式,SHA-1 值和文件名:

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

在本例中,指定了文件模式為 100644,表明這是一個(gè)普通文件。其他可用的模式有:100755 表示可執(zhí)行文件,120000 表示符號(hào)鏈接。文件模式是從常規(guī)的 UNIX 文件模式中參考來(lái)的,但是沒(méi)有那么靈活 ── 上述三種模式僅對(duì) Git 中的文件 (blobs) 有效 (雖然也有其他模式用于目錄和子模塊)。

現(xiàn)在可以用 write-tree 命令將暫存區(qū)域的內(nèi)容寫(xiě)到一個(gè) tree 對(duì)象了。無(wú)需 -w 參數(shù) ── 如果目標(biāo) tree 不存在,調(diào)用 write-tree 會(huì)自動(dòng)根據(jù) index 狀態(tài)創(chuàng)建一個(gè) tree 對(duì)象。

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

可以這樣驗(yàn)證這確實(shí)是一個(gè) tree 對(duì)象:

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

再根據(jù) test.txt 的第二個(gè)版本以及一個(gè)新文件創(chuàng)建一個(gè)新 tree 對(duì)象:

$ echo 'new file' > new.txt
$ git update-index test.txt
$ git update-index --add new.txt

這時(shí)暫存區(qū)域中包含了 test.txt 的新版本及一個(gè)新文件 new.txt 。創(chuàng)建 (寫(xiě)) 該 tree 對(duì)象 (將暫存區(qū)域或 index 狀態(tài)寫(xiě)入到一個(gè) tree 對(duì)象),然后瞧瞧它的樣子:

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

請(qǐng)注意該 tree 對(duì)象包含了兩個(gè)文件記錄,且 test.txt 的 SHA 值是早先值的 "第二版" (1f7a7a)。來(lái)點(diǎn)更有趣的,你將把第一個(gè) tree 對(duì)象作為一個(gè)子目錄加進(jìn)該 tree 中。可以用 read-tree 命令將 tree 對(duì)象讀到暫存區(qū)域中去。在這時(shí),通過(guò)傳一個(gè) --prefix 參數(shù)給 read-tree,將一個(gè)已有的 tree 對(duì)象作為一個(gè)子 tree 讀到暫存區(qū)域中:

$ 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

如果從剛寫(xiě)入的新 tree 對(duì)象創(chuàng)建一個(gè)工作目錄,將得到位于工作目錄頂級(jí)的兩個(gè)文件和一個(gè)名為 bak 的子目錄,該子目錄包含了 test.txt 文件的第一個(gè)版本。可以將 Git 用來(lái)包含這些內(nèi)容的數(shù)據(jù)想象成如圖 9-2 所示的樣子。

圖 9-2. 當(dāng)前 Git 數(shù)據(jù)的內(nèi)容結(jié)構(gòu)

commit (提交) 對(duì)象

你現(xiàn)在有三個(gè) tree 對(duì)象,它們指向了你要跟蹤的項(xiàng)目的不同快照,可是先前的問(wèn)題依然存在:必須記往三個(gè) SHA-1 值以獲得這些快照。你也沒(méi)有關(guān)于誰(shuí)、何時(shí)以及為何保存了這些快照的信息。commit 對(duì)象為你保存了這些基本信息。

要?jiǎng)?chuàng)建一個(gè) commit 對(duì)象,使用 commit-tree 命令,指定一個(gè) tree 的 SHA-1,如果有任何前繼提交對(duì)象,也可以指定。從你寫(xiě)的第一個(gè) tree 開(kāi)始:

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

通過(guò) cat-file 查看這個(gè)新 commit 對(duì)象:

$ 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

commit 對(duì)象有格式很簡(jiǎn)單:指明了該時(shí)間點(diǎn)項(xiàng)目快照的頂層樹(shù)對(duì)象、作者/提交者信息(從 Git 設(shè)置的 user.name 和 user.email中獲得)以及當(dāng)前時(shí)間戳、一個(gè)空行,以及提交注釋信息。

接著再寫(xiě)入另外兩個(gè) commit 對(duì)象,每一個(gè)都指定其之前的那個(gè) commit 對(duì)象:

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

每一個(gè) commit 對(duì)象都指向了你創(chuàng)建的樹(shù)對(duì)象快照。出乎意料的是,現(xiàn)在已經(jīng)有了真實(shí)的 Git 歷史了,所以如果運(yùn)行 git log 命令并指定最后那個(gè) commit 對(duì)象的 SHA-1 便可以查看歷史:

$ 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 files changed, 1 insertions(+), 0 deletions(-)

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 deletions(-)

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

    first commit

 test.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

真棒。你剛剛通過(guò)使用低級(jí)操作而不是那些普通命令創(chuàng)建了一個(gè) Git 歷史。這基本上就是運(yùn)行 git add 和 git commit 命令時(shí) Git 進(jìn)行的工作 ──保存修改了的文件的 blob,更新索引,創(chuàng)建 tree 對(duì)象,最后創(chuàng)建 commit 對(duì)象,這些 commit 對(duì)象指向了頂層 tree 對(duì)象以及先前的 commit 對(duì)象。這三類(lèi) Git 對(duì)象 ── blob,tree 以及 commit ── 都各自以文件的方式保存在 .git/objects 目錄下。以下所列是目前為止樣例中的所有對(duì)象,每個(gè)對(duì)象后面的注釋里標(biāo)明了它們保存的內(nèi)容:

$ 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

如果你按照以上描述進(jìn)行了操作,可以得到如圖 9-3 所示的對(duì)象圖。

圖 9-3. Git 目錄下的所有對(duì)象

對(duì)象存儲(chǔ)

之前我提到當(dāng)存儲(chǔ)數(shù)據(jù)內(nèi)容時(shí),同時(shí)會(huì)有一個(gè)文件頭被存儲(chǔ)起來(lái)。我們花些時(shí)間來(lái)看看 Git 是如何存儲(chǔ)對(duì)象的。你將看來(lái)如何通過(guò) Ruby 腳本語(yǔ)言存儲(chǔ)一個(gè) blob 對(duì)象 (這里以字符串 "what is up, doc?" 為例) 。使用 irb 命令進(jìn)入 Ruby 交互式模式:

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

Git 以對(duì)象類(lèi)型為起始內(nèi)容構(gòu)造一個(gè)文件頭,本例中是一個(gè) blob。然后添加一個(gè)空格,接著是數(shù)據(jù)內(nèi)容的長(zhǎng)度,最后是一個(gè)空字節(jié) (null byte):

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

Git 將文件頭與原始數(shù)據(jù)內(nèi)容拼接起來(lái),并計(jì)算拼接后的新內(nèi)容的 SHA-1 校驗(yàn)和??梢栽?Ruby 中使用 require 語(yǔ)句導(dǎo)入 SHA1 digest 庫(kù),然后調(diào)用 Digest::SHA1.hexdigest() 方法計(jì)算字符串的 SHA-1 值:

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

Git 用 zlib 對(duì)數(shù)據(jù)內(nèi)容進(jìn)行壓縮,在 Ruby 中可以用 zlib 庫(kù)來(lái)實(shí)現(xiàn)。首先需要導(dǎo)入該庫(kù),然后用 Zlib::Deflate.deflate() 對(duì)數(shù)據(jù)進(jìn)行壓縮:

>> require 'zlib'
=> true
>> zlib_content = Zlib::Deflate.deflate(store)
=> "x\234K\312\311OR04c(\317H,Q\310,V(-\320QH\311O\266\a\000_\034\a\235"

最后將用 zlib 壓縮后的內(nèi)容寫(xiě)入磁盤(pán)。需要指定保存對(duì)象的路徑 (SHA-1 值的頭兩個(gè)字符作為子目錄名稱(chēng),剩余 38 個(gè)字符作為文件名保存至該子目錄中)。在 Ruby 中,如果子目錄不存在可以用 FileUtils.mkdir_p() 函數(shù)創(chuàng)建它。接著用 File.open 方法打開(kāi)文件,并用 write() 方法將之前壓縮的內(nèi)容寫(xiě)入該文件:

>> 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

這就行了 ── 你已經(jīng)創(chuàng)建了一個(gè)正確的 blob 對(duì)象。所有的 Git 對(duì)象都以這種方式存儲(chǔ),惟一的區(qū)別是類(lèi)型不同 ── 除了字符串 blob,文件頭起始內(nèi)容還可以是 commit 或 tree 。不過(guò)雖然 blob 幾乎可以是任意內(nèi)容,commit 和 tree 的數(shù)據(jù)卻是有固定格式的。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)