== 揭開面紗 ==

我們揭開Git神秘面紗，往裡瞧瞧它是如何創造奇蹟的。我會跳過細節。更深入的描述參
見 http://www.kernel.org/pub/software/scm/git/docs/user-manual.html[ 用戶手
冊]。

=== 大象無形 ===

Git怎麼這麼謙遜寡言呢？除了偶爾提交和合併外，你可以如常工作，就像不知道版本控
制系統存在一樣。那就是，直到你需要它的時候，而且那是你歡欣的時候，Git一直默默
注視着你。

其他版本控制系統強迫你與繁文縟節和官僚主義不斷鬥爭。檔案的權限可能是隻讀的，
除非你顯式地告訴中心伺服器哪些檔案你打算編輯。即使最基本的命令，隨着用戶數目
的增多，也會慢的像爬一樣。中心伺服器可能正跟蹤什麼人，什麼時候check out了什麼
代碼。當網絡連接斷了的時候，你就遭殃了。開發人員不斷地與這些版本控制系統的種
種限製作鬥爭。一旦網絡或中心伺服器癱瘓，工作就嘎然而止。

與之相反，Git簡單地在你工作目錄下的`.git`目錄保存你項目的歷史。這是你自己的歷
史拷貝，因此你可以保持離線，直到你想和他人溝通為止。你擁有你的檔案命運完全的
控制權，因為Git可以輕易在任何時候從`.git`重建一個保存狀態。

=== 數據完整性 ===

很多人把加密和保持信息機密關聯起來，但一個同等重要的目標是保證信息安全。合理
使用哈希加密功能可以防止無意或有意的數據損壞行為。

一個SHA1哈希值可被認為是一個唯一的160位ID數，用它可以唯一標識你一生中遇到的每
個位元組串。 實際上不止如此：每個位元組串可供任何人用好多輩子。

對一個檔案而言，其整體內容的哈希值可以被看作這個檔案的唯一標識ID數。

因為一個SHA1哈希值本身也是一個位元組串，我們可以哈希包括其他哈希值的位元組串。這
個簡單的觀察出奇地有用：查看“哈希鏈”。我們之後會看Git如何利用這一點來高效地
保證數據完整性。

簡言之，Git把你數據保存在`.git/objects`子目錄，那裡看不到正常檔案名，相反你只
看到ID。通過用ID作為檔案名，加上一些檔案鎖和時間戳技巧，Git把任意一個原始的文
件系統轉化為一個高效而穩定的資料庫。

=== 智能 ===

Git是如何知道你重命名了一個檔案，即使你從來沒有明確提及這個事實？當然，你或許
是運行了 *git mv* ，但這個命令和 *git add* 緊接 *git rm* 是完全一樣的。

Git啟發式地找出相連版本之間的重命名和拷貝。實際上，它能檢測檔案之間代碼塊的移
動或拷貝！儘管它不能覆蓋所有的情況，但它已經做的很好了，並且這個功能也總在改
進中。如果它在你那兒不工作的話，可以嘗試打開開銷更高的拷貝檢測選項，並考慮升
級。

=== 索引 ===

為每個加入管理的檔案，Git在一個名為“index”的檔案裡記錄統計信息，諸如大小，
創建時間和最後修改時間。為了確定檔案是否更改，Git比較其當前統計信息與那些在索
引裡的統計信息。如果一致，那Git就跳過重新讀檔案。

因為統計信息的調用比讀檔案內容快的很多，如果你僅僅編輯了少數幾個檔案，Git几乎
不需要什麼時間就能更新他們的統計信息。

我們前面講過索引是一個中轉區。為什麼一堆檔案的統計數據是一個中轉區？因為添加
命令將檔案放到Git的資料庫並更新它們的統計信息，而無參數的提交命令創建一個提交，
只基于這些統計信息和已經在資料庫裡的檔案。

=== Git的源起 ===

這個 http://lkml.org/lkml/2005/4/6/121[ Linux內核郵件列表帖子] 描述了導致Git
的一系列事件。整個討論線索是一個令人着迷的歷史探究過程，對Git史學家而言。

=== 對象資料庫 ===

你數據的每個版本都保存在“對象資料庫”裡，其位於子目錄`.git/objects`；其他位
于`.git/`的較少數據：索引，分支名，標籤，配置選項，日誌，頭提交的當前位置等。
對象資料庫樸素而優雅，是Git的力量之源。

`.git/objects`裡的每個檔案是一個對象。有3中對象跟我們有關：“blob”對象，
“tree”對象，和“commit”對象。

=== Blob對象 ===

首先來一個小把戲。去一個檔案名，任意檔案名。在一個空目錄：

 $ echo sweet > YOUR_FILENAME
 $ git init
 $ git add .
 $ find .git/objects -type f

你將看到 +.git/objects/aa/823728ea7d592acc69b36875a482cdf3fd5c8d+ 。

我如何在不知道檔案名的情況下知道這個？這是因為以下內容的SHA1哈希值：

 "blob" SP "6" NUL "sweet" LF

是 aa823728ea7d592acc69b36875a482cdf3fd5c8d，這裡SP是一個空格，NUL是一個0位元組，
LF是一個換行符。你可以驗證這一點，鍵入：

  $ printf "blob 6\000sweet\n" | sha1sum

Git基于“內容定址”：檔案並不按它們的檔案名存儲，而是按它們包含內容的哈希值，
在一個叫“blob對象”的檔案裡。我們可以把檔案內容的哈希值看作一個唯一ID，這樣
在某種意義上我們通過他們內容放置檔案。開始的“blob 6”只是一個包含對象類型與
其長度的頭；它簡化了內部存儲。

這樣我可以輕易語言你所看到的。檔案名是無關的：只有裡面的內容被用作構建blob對象。

你可能想知道對相同的檔案什麼會發生。試圖加一個你檔案的拷貝，什麼檔案名都行。
在 +.git/objects+ 的內容保持不變，不管你加了多少。Git只存儲一次數據。

順便說一句，在 +.git/objects+ 裡的檔案用zlib壓縮，因此你不應該直接查看他們。
可以通過http://www.zlib.net/zpipe.c[zpipe -d] 管道， 或者鍵入：

 $ git cat-file -p aa823728ea7d592acc69b36875a482cdf3fd5c8d

這漂亮地打印出給定的對象。

=== Tree對象 ===

但檔案名在哪？它們必定在某個階段保存在某個地方。Git在提交時得到檔案名：

 $ git commit  # 輸入一些信息。
 $ find .git/objects -type f

你應看到3個對象。這次我不能告訴你這兩個新檔案是什麼，因為它部分依賴你選擇的文
件名。我繼續進行，假設你選了``rose''。如果你沒有，你可以重寫歷史以讓它看起來
像似你做了：

 $ git filter-branch --tree-filter 'mv YOUR_FILENAME rose'
 $ find .git/objects -type f

現在你硬看到檔案 +.git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9+ ，因為這是以下內容的SHA1哈希值：

 "tree" SP "32" NUL "100644 rose" NUL 0xaa823728ea7d592acc69b36875a482cdf3fd5c8d

檢查這個檔案真的包含上面內容通過鍵入：

 $ echo 05b217bb859794d08bb9e4f7f04cbda4b207fbe9 | git cat-file --batch

使用zpipe，驗證哈希值是容易的：

 $ zpipe -d < .git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9 | sha1sum

與查看檔案相比，哈希值驗證更技巧一些，因為其輸出不止包含原始未壓縮檔案。

這個檔案是一個“tree”對象：一組數據包含檔案類型，檔案名和哈希值。在我們的例
子裡，檔案類型是100644，這意味着“rose”是一個一般檔案，並且哈希值指blob對象，
包含“rose”的內容。其他可能檔案類型有可執行，連結或者目錄。在最後一個例子裡，
哈希值指向一個tree對象。

在一些過渡性的分支，你會有一些你不在需要的老的對象，儘管有寬限過期之後，它們
會被自動清除，現在我們還是將其刪除，以使我們比較容易跟上這個玩具例子。

 $ rm -r .git/refs/original
 $ git reflog expire --expire=now --all
 $ git prune

在真實項目裡你通常應該避免像這樣的命令，因為你在破換備份。如果你期望一個乾淨
的倉庫，通常最好做一個新的克隆。還有，直接操作 +.git+ 時一定要小心：如果
Git命令同時也在運行會怎樣，或者突然停電？一般，引用應由 *git update-ref -d*
刪除，儘管通常手工刪除 +refs/original+ 也是安全的。

=== Commit對象 ===

我們已經解釋了三個對象中的兩個。第三個是“commit”對象。其內容依賴于提交信息
以及其創建的日期和時間。為滿足這裡我們所有的，我們不得不調整一下：

 $ git commit --amend -m Shakespeare  # 改提交信息
 $ git filter-branch --env-filter 'export
     GIT_AUTHOR_DATE="Fri 13 Feb 2009 15:31:30 -0800"
     GIT_AUTHOR_NAME="Alice"
     GIT_AUTHOR_EMAIL="alice@example.com"
     GIT_COMMITTER_DATE="Fri, 13 Feb 2009 15:31:30 -0800"
     GIT_COMMITTER_NAME="Bob"
     GIT_COMMITTER_EMAIL="bob@example.com"'  # Rig timestamps and authors.
 $ find .git/objects -type f

你現在應看到 +.git/objects/49/993fe130c4b3bf24857a15d7969c396b7bc187+ 是下列
內容的SHA1哈希值：

 "commit 158" NUL
 "tree 05b217bb859794d08bb9e4f7f04cbda4b207fbe9" LF
 "author Alice <alice@example.com> 1234567890 -0800" LF
 "committer Bob <bob@example.com> 1234567890 -0800" LF
 LF
 "Shakespeare" LF

和前面一樣，你可以運行zpipe或者cat-file來自己看。

這是第一個提交，因此沒有父提交，但之後的提交將總有至少一行，指定一個父提交。

=== 沒那麼神 ===

Git的秘密似乎太簡單。看起來似乎你可以整合幾個shell腳本，加幾行C代碼來弄起來，
也就幾個小時的事：一個基本檔案操作和SHA1哈希化的混雜，用鎖檔案裝飾一下，檔案
同步保證健壯性。實際上，這準確描述了Git的最早期版本。儘管如此，除了巧妙地打包
以節省空間，巧妙地索引以省時間，我們現在知道Git如何靈巧地改造檔案系統成為一個
對版本控制完美的資料庫。

例如，如果對象資料庫裡的任何一個檔案由於硬碟錯誤損毀，那麼其哈希值將不再匹配，
這個錯誤會報告給我們。通過哈希化其他對象的哈希值，我們在所有層面維護數據完整
性。Commit對象是原子的，也就是說，一個提交永遠不會部分地記錄變更：在我們已經
存儲所有相關tree對象，blob對象和父commit對象之後，我們才可以計算提交的的哈希
值並將其存儲在資料庫，對象資料庫不受諸如停電之類的意外中斷影響。

我們打敗即使是最狡猾的對手。假設有誰試圖悄悄修改一個項目裡一個遠古版本檔案的
內容。為使對象據庫看起來健康，他們也必須修改相應blob對象的哈希值，既然它現在
是一個不同的位元組串。這意味着他們講不得不引用這個檔案的tree對象的哈希值，並反
過來改變所有與這個tree相關的commit對象的哈希值，還要加上這些提交所有後裔的哈
希值。這暗示官方head的哈希值與這個壞倉庫不同。通過跟蹤不匹配哈希值線索，我
們可以查明殘缺檔案，以及第一個被破壞的提交。

總之，只要20個位元組代表最後一次提交的是安全的，不可能篡改一個Git倉庫。

那麼Git的著名功能怎樣呢？分支？合併？標籤？單純的細節。當前head保存在檔案
+.git /HEAD+ ，其中包含了一個commit對象的哈希值。該哈希值在運行提交以及其他命
令是更新。分支几乎一樣：它們是保存在 +.git/refs/heads+ 的檔案。標籤也是：它們
住在住在 +.git/refs/tags+ ，但它們由一套不同的命令更新。
