## Git 對象
Git 是一個內容尋址文件系統。 看起來很酷, 但這是什么意思呢? 這意味著,Git 的核心部分是一個簡單的鍵值對數據庫(key-value data store)。 你可以向該數據庫插入任意類型的內容,它會返回一個鍵值,通過該鍵值可以在任意時刻再次檢索(retrieve)該內容。 可以通過底層命令?`hash-object`?來演示上述效果——該命令可將任意數據保存于?`.git`?目錄,并返回相應的鍵值。 首先,我們需要初始化一個新的 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`?目錄進行了初始化,并創建了?`pack`?和?`info`?子目錄,但均為空。 接著,往 Git 數據庫存入一些文本:
~~~
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
~~~
`-w`?選項指示?`hash-object`?命令存儲數據對象;若不指定此選項,則該命令僅返回對應的鍵值。`--stdin`?選項則指示該命令從標準輸入讀取內容;若不指定此選項,則須在命令尾部給出待存儲文件的路徑。 該命令輸出一個長度為 40 個字符的校驗和。 這是一個 SHA-1 哈希值——一個將待存儲的數據外加一個頭部信息(header)一起做 SHA-1 校驗運算而得的校驗和。后文會簡要討論該頭部信息。 現在我們可以查看 Git 是如何存儲數據的:
~~~
$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
~~~
可以在?`objects`?目錄下看到一個文件。 這就是開始時 Git 存儲內容的方式——一個文件對應一條內容,以該內容加上特定頭部信息一起的 SHA-1 校驗和為文件命名。 校驗和的前兩個字符用于命名子目錄,余下的 38 個字符則用作文件名。
可以通過?`cat-file`?命令從 Git 那里取回數據。 這個命令簡直就是一把剖析 Git 對象的瑞士軍刀。 為?`cat-file`?指定?`-p`?選項可指示該命令自動判斷內容的類型,并為我們顯示格式友好的內容:
~~~
$ 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
~~~
現在可以把文件內容恢復到第一個版本:
~~~
$ 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 object)。 利用?`cat-file -t`?命令,可以讓 Git 告訴我們其內部存儲的任何對象類型,只要給定該對象的 SHA-1 值:
~~~
$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob
~~~
### [樹對象](http://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-Git-%E5%AF%B9%E8%B1%A1#樹對象)
接下來要探討的對象類型是樹對象(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
~~~
`master^{tree}`?語法表示?`master`?分支上最新的提交所指向的樹對象。 請注意,`lib`?子目錄(所對應的那條樹對象記錄)并不是一個數據對象,而是一個指針,其指向的是另一個樹對象:
~~~
$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b simplegit.rb
~~~
從概念上講,Git 內部存儲的數據有點像這樣:

Figure 10-1.?簡化版的 Git 數據模型。
你可以輕松創建自己的樹對象。 通常,Git 根據某一時刻暫存區(即 index 區域,下同)所表示的狀態創建并記錄一個對應的樹對象,如此重復便可依次記錄(某個時間段內)一系列的樹對象。 因此,為創建一個樹對象,首先需要通過暫存一些文件來創建一個暫存區。 可以通過底層命令`update-index`?為一個單獨文件——我們的 test.txt 文件的首個版本——創建一個暫存區。 利用該命令,可以把 test.txt 文件的首個版本人為地加入一個新的暫存區。 必須為上述命令指定?`--add`?選項,因為此前該文件并不在暫存區中(我們甚至都還沒來得及創建一個暫存區呢);同樣必需的還有?`--cacheinfo`?選項,因為將要添加的文件位于 Git 數據庫中,而不是位于當前目錄下。 同時,需要指定文件模式、SHA-1 與文件名:
~~~
$ git update-index --add --cacheinfo 100644 \
83baae61804e65cc73a7201a7252750c76066a30 test.txt
~~~
本例中,我們指定的文件模式為?`100644`,表明這是一個普通文件。 其他選擇包括:`100755`,表示一個可執行文件;`120000`,表示一個符號鏈接。 這里的文件模式參考了常見的 UNIX 文件模式,但遠沒那么靈活——上述三種模式即是 Git 文件(即數據對象)的所有合法模式(當然,還有其他一些模式,但用于目錄項和子模塊)。
現在,可以通過?`write-tree`?命令將暫存區內容寫入一個樹對象。 此處無需指定?`-w`?選項——如果某個樹對象此前并不存在的話,當調用?`write-tree`?命令時,它會根據當前暫存區狀態自動創建一個新的樹對象:
~~~
$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
~~~
不妨驗證一下它確實是一個樹對象:
~~~
$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree
~~~
接著我們來創建一個新的樹對象,它包括 test.txt 文件的第二個版本,以及一個新的文件:
~~~
$ echo 'new file' > new.txt
$ git update-index 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 值(`1f7a7a`)是先前值的“第二版”。 只是為了好玩:你可以將第一個樹對象加入第二個樹對象,使其成為新的樹對象的一個子目錄。 通過調用?`read-tree`?命令,可以把樹對象讀入暫存區。 本例中,可以通過對?`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 內部存儲著的用于表示上述結構的數據是這樣的:

Figure 10-2.?當前 Git 的數據內容結構。
### [提交對象](http://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-Git-%E5%AF%B9%E8%B1%A1#提交對象)
現在有三個樹對象,分別代表了我們想要跟蹤的不同項目快照。然而問題依舊:若想重用這些快照,你必須記住所有三個 SHA-1 哈希值。 并且,你也完全不知道是誰保存了這些快照,在什么時刻保存的,以及為什么保存這些快照。 而以上這些,正是提交對象(commit object)能為你保存的基本信息。
可以通過調用?`commit-tree`?命令創建一個提交對象,為此需要指定一個樹對象的 SHA-1 值,以及該提交的父提交對象(如果有的話)。 我們從之前創建的第一個樹對象開始:
~~~
$ echo 'first commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d
~~~
現在可以通過?`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.name`?和?`user.email`?配置來設定,外加一個時間戳);留空一行,最后是提交注釋。
接著,我們將創建另兩個提交對象,它們分別引用各自的上一個提交(作為其父提交對象):
~~~
$ echo 'second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'third commit' | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9
~~~
這三個提交對象分別指向之前創建的三個樹對象快照中的一個。 現在,如果對最后一個提交的 SHA-1 值運行?`git log`?命令,會出乎意料的發現,你已有一個貨真價實的、可由?`git log`?查看的 Git 提交歷史了:
~~~
$ 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 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
~~~
如果跟蹤所有的內部指針,將得到一個類似下面的對象關系圖:

Figure 10-3.?你的 Git 目錄下的所有對象。
### [對象存儲](http://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-Git-%E5%AF%B9%E8%B1%A1#對象存儲)
前文曾提及,在存儲內容時,會有個頭部信息一并被保存。 讓我們略花些時間來看看 Git 是如何存儲其對象的。 通過在 Ruby 腳本語言中交互式地演示,你將看到一個數據對象——本例中是字符串“what is up, doc?”——是如何被存儲的。
可以通過?`irb`?命令啟動 Ruby 的交互模式:
~~~
$ irb
>> content = "what is up, doc?"
=> "what is up, doc?"
~~~
Git 以對象類型作為開頭來構造一個頭部信息,本例中是一個“blob”字符串。 接著 Git 會添加一個空格,隨后是數據內容的長度,最后是一個空字節(null byte):
~~~
>> header = "blob #{content.length}\0"
=> "blob 16\u0000"
~~~
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"
~~~
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"
~~~
最后,需要將這條經由 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 數據對象。 所有的 Git 對象均以這種方式存儲,區別僅在于類型標識——另兩種對象類型的頭部信息以字符串“commit”或“tree”開頭,而不是“blob”。 另外,雖然數據對象的內容幾乎可以是任何東西,但提交對象和樹對象的內容卻有各自固定的格式。
- 前言
- Scott Chacon 序
- Ben Straub 序
- 獻辭
- 貢獻者
- 引言
- 1. 起步
- 1.1 關于版本控制
- 1.2 Git 簡史
- 1.3 Git 基礎
- 1.4 命令行
- 1.5 安裝 Git
- 1.6 初次運行 Git 前的配置
- 1.7 獲取幫助
- 1.8 總結
- 2. Git 基礎
- 2.1 獲取 Git 倉庫
- 2.2 記錄每次更新到倉庫
- 2.3 查看提交歷史
- 2.4 撤消操作
- 2.5 遠程倉庫的使用
- 2.6 打標簽
- 2.7 Git 別名
- 2.8 總結
- 3. Git 分支
- 3.1 分支簡介
- 3.2 分支的新建與合并
- 3.3 分支管理
- 3.4 分支開發工作流
- 3.5 遠程分支
- 3.6 變基
- 3.7 總結
- 4. 服務器上的 Git
- 4.1 協議
- 4.2 在服務器上搭建 Git
- 4.3 生成 SSH 公鑰
- 4.4 配置服務器
- 4.5 Git 守護進程
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方托管的選擇
- 4.10 總結
- 5. 分布式 Git
- 5.1 分布式工作流程
- 5.2 向一個項目貢獻
- 5.3 維護項目
- 5.4 總結
- 6. GitHub
- 6.1 賬戶的創建和配置
- 6.2 對項目做出貢獻
- 6.3 維護項目
- 6.4 管理組織
- 6.5 腳本 GitHub
- 6.6 總結
- 7. Git 工具
- 7.1 選擇修訂版本
- 7.2 交互式暫存
- 7.3 儲藏與清理
- 7.4 簽署工作
- 7.5 搜索
- 7.6 重寫歷史
- 7.7 重置揭密
- 7.8 高級合并
- 7.9 Rerere
- 7.10 使用 Git 調試
- 7.11 子模塊
- 7.12 打包
- 7.13 替換
- 7.14 憑證存儲
- 7.15 總結
- 8. 自定義 Git
- 8.1 配置 Git
- 8.2 Git 屬性
- 8.3 Git 鉤子
- 8.4 使用強制策略的一個例子
- 8.5 總結
- 9. Git 與其他系統
- 9.1 作為客戶端的 Git
- 9.2 遷移到 Git
- 9.3 總結
- 10. Git 內部原理
- 10.1 底層命令和高層命令
- 10.2 Git 對象
- 10.3 Git 引用
- 10.4 包文件
- 10.5 引用規格
- 10.6 傳輸協議
- 10.7 維護與數據恢復
- 10.8 環境變量
- 10.9 總結
- A. 其它環境中的 Git
- A1.1 圖形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Eclipse 中的 Git
- A1.4 Bash 中的 Git
- A1.5 Zsh 中的 Git
- A1.6 Powershell 中的 Git
- A1.7 總結
- B. 將 Git 嵌入你的應用
- A2.1 命令行 Git 方式
- A2.2 Libgit2
- A2.3 JGit
- C. Git 命令
- A3.1 設置與配置
- A3.2 獲取與創建項目
- A3.3 快照基礎
- A3.4 分支與合并
- A3.5 項目分享與更新
- A3.6 檢查與比較
- A3.7 調試
- A3.8 補丁
- A3.9 郵件
- A3.10 外部系統
- A3.11 管理
- A3.12 底層命令