git plumbing 更加底層命令解析-深入理解GIT

世有因果知因求果發表於2015-05-25

原文: http://rypress.com/tutorials/git/plumbing

本文詳細介紹GIT Plumbing--更加底層的git命令,你將會對git在內部是如何管理和呈現一個專案repo有一個深入的理解。

除非你想通讀Git原始碼,你可能永遠沒有必要使用下面的命令。但是通過手工的操作一個repo將會讓你對於GIT如何儲存資料的概念細節有個深入理解,你也將對於git是如何工作的有更好的理解。

我們首先來檢閱Git的object database,然後我們使用git的低階命令手工建立和commit一個snapshot

Examine Commit Details

首先我們通過git cat-file plumbing command來檢視最近的一個commit:

git cat-file commit HEAD

commit引數告訴git我們需要檢視一個commit物件。正如我們所知,HEAD指向最近的commit.這將輸出下面的資訊:

tree 552acd444696ccb1c3afe68a55ae8b20ece2b0e6
parent 6a1d380780a83ef5f49523777c5e8d801b7b9ba2
author Ryan <ryan.example@rypress.com> 1326496982 -0600
committer Ryan <ryan.example@rypress.com> 1326496982 -0600

Add .gitignore file

這是代表一個commit的完整資訊:一個tree,一個parent,使用者資料和一個commit message.使用者資訊和commit訊息是非常容易理解的,但我們從來沒有見過tree或者parent.

一個tree object是git對於一個"snapshot"的代表。他們儲存了一個目錄在特定時刻的狀態,該object沒有任何關於時間或者作者的資訊。為了將tree和專案一貫的歷史資訊關聯起來,GIT將每一個tree物件包裝在一個commit物件中,並且指定一個parent,而這個parent實際上就是另外一個commit.通過遍歷每一個commit的parent,你就可以遍歷完專案的整個歷史。

注意每一個commit refers to one and only one tree object,也就是說commit和tree是一一對應的。從git cat-file輸出的內容看,我們可以使用SHA checksum來代表那個tree.這個SHA CHECKSUM對於GIT內部每一個變數都是適用的。

Examine a Tree

下面我們使用git cat-file命令來檢閱一個TREE物件。注意將相關的id更新為你的tree的id:

git cat-file tree 552acd4

不幸的是上述命令輸出的是binary資料,無法閱讀。你可以通過使用下面的git ls-tree命令來輸出可閱讀的內容。

git ls-tree 552acd4

該命令將輸出目錄的列表:

100644 blob 99ed0d431c5a19f147da3c4cb8421b5566600449    .gitignore
040000 tree ab4947cb27ef8731f7a54660655afaedaf45444d    about
100644 blob cefb5a651557e135666af4c07c7f2ab4b8124bd7    blue.html
100644 blob cb01ae23932fd9704fdc5e077bc3c1184e1af6b9    green.html
100644 blob e993e5fa85a436b2bb05b6a8018e81f8e8864a24    index.html
100644 blob 2a6deedee35cc59a83b1d978b0b8b7963e8298e9    news-1.html
100644 blob 0171687fc1b23aa56c24c54168cdebaefecf7d71    news-2.html
...

通過檢查上面命令的輸出內容,我們可以假定"blobs"代表了我們repo裡面的檔案,而trees代表了repo裡面的folders。繼續通過git ls-tree檢閱about tree,我們就可以看到是不是我們的假定是正確的了。

所以,blob物件實際上就是git儲存我們檔案內容的物件,可以簡單理解為檔案,tree物件組合了blob和其他的tree物件形成了目錄列表,也就是說tree可以簡單理解為目錄。這些物件就是git最終形成我們在git中常用命令所操作的唯一物件。commit,tree,blob之間的關係可以用下面的圖形象展示:

Examine a Blob

我們來看看和blue.html檔案相對應的blob:

git cat-file blob cefb5a6

這會展示blue.html檔案的整個內容,這也印證了blob本身就是存資料檔案,blob本身是純粹的content,它本身甚至沒有任何關於檔名稱的資訊。也就是說檔名blue.html是儲存在包含blob的tree物件中,而不是在blob物件中。

你可能知道SHA-1 checksum確保一個物件的內容永遠不會在Git不知情的情況下被篡改。checksum的原理是通過使用物件內容來計算一個唯一的字元序列。這不僅作為一個id,而且它保證一個物件永遠不會悄無聲息地被修改(而git竟然不知情).當我們來談到blob物件時,這又有另外一個好處。既然兩個具有相同內容的blob永遠具有相同的checksum id,那麼git可以跨tree來共享一個blob物件。比如我們的blue.html檔案自從被建立後就沒有被修改,那麼我們的repo將只有一個相關聯的blob,而所有後續的tree物件都將引用它。通過不去建立重複的blob,Git可以大大降低repo的尺寸。有這個理念作為基礎,我們可以修正git的物件圖如下:

然而,只要你更改檔案的任何一行,Git都必需建立一個新的blob物件來反映這個修改,因為內容變更,SHA-1 checksum就將變更。當然,GIT也有一些增量修改的機制來保證這個尺寸增加不是很大的問題。

Examine a Tag

第四個也是最後一個git object是tag物件.我們可以使用git cat-file命令來顯示tag的detail儲存資訊:

git cat-file tag v2.0

上面的命令將輸出和v2.0這個tag相關的commitID,以及tag的名稱,作者,建立時間和附加資訊。下面更新最後版本的git物件圖:

Inspect Git’s Branch Representation

我們現在有了能夠完全瀏覽git branch representation的所有工具,使用-t選項,我們可以得知git對branch使用哪種物件來表示:

git cat-file -t master
將輸出commit
git cat-file commit master
將輸出和git cat-file commit HEAD完全一樣的資訊

一個branch就是一個對一個commit物件的引用,這意味著我們可以通過git cat-file commit master來檢視master branch的詳細資訊。master branch和HEAD都是對一個commit物件的簡單引用。

我們開啟.git/refs/heads/master檔案,你可以看到該檔案內容實際上就是最近的一個commit的checksumID(你可以通過git log -n 1來檢查這個commitid)。這個檔案就是git為了維護master分支所需的一切,所有其他的資訊都是通過這個commit object根據上面討論的關係圖來外推得出的!(branch本身是否就等於branch tip呢??)

另一方面,HEAD reference本身記錄在.git/HEAD檔案中,不像branch tips總是指向一個branch的頂端commit,

HEAD並不是任何一個commit的直接連結。反而,HEAD總是引用一個branch,GIT使用這個branch來指出當前checkout出來的是哪一個commit。記住一個detached HEAD state是在當HEAD不再和branch的tip相一致時發生的!從機理上說,這意味著.git/HEAD並沒有一個Local branch。試著checkout一個老的commit:

git checkout HEAD~1

現在,.git/HEAD就包含一個commitID,而不再是一個branch。這告訴git我們進入了一個dtached HEAD state。無論你在什麼狀態,git checkout命令將總是將checkedout commit的引用記錄在.git/HEAD中。我們繼續git checkout master,返回master branch,來接著做一些其他的實驗:

Explore the Object Database

當我們有一個git物件操作的基本理解,我們可以看看git將這些物件放在哪裡了。在我們的my-git-repo庫,開啟資料夾.git/objects,那就是git的資料庫!

每一個物件,無論物件的型別是什麼,都被儲存為一個檔案,使用他的SHA-1 checksum作為檔名。但是,和傳統將所有objects存放在一個資料夾的做法不同的是:他們通過將他們的ID前兩個字元剔出來作為一個目錄名,而id剩下的字元作為檔名。具體可以看看下面的objects輸出目錄格式:

00  10  28  33  3e  51  5c  6e  77  85  95  f7
01  11  29  34  3f  52  5e  6f  79  86  96  f8
02  16  2a  35  41  53  63  70  7a  87  98  f9
03  1c  2b  36  42  54  64  71  7c  88  99  fa
0c  26  30  3c  4e  5a  6a  75  83  91  a0  info
0e  27  31  3d  50  5b  6b  76  84  93  a2  pack

 

比如,一個具有如下ID的object,

7a52bb857229f89bffa74134ee3de48e5e146105

將會被儲存在7a 檔案架中,而剩下的(52bb8...)作為檔名,也就是說7a目錄+52bb8...檔案的組合來唯一標識該object

這樣做的好處是檢索迅速。知道了這一層,那麼我們就可以重新組合出來我們的objectID,以便方便地查閱它的內容,git cat-file -t 7a52bb8 ,

git cat-file blob 7a52bb8

:該命令就是重新組合我們的object,並且如果發現它是blob物件,我們就將得到其內容,如果是tree物件,則用lstree命令

Collect the Garbage

隨著repo的增長,GIT可能自動將你的object檔案轉換成一種被成為"pack"的壓縮檔案。你可以使用garbage collection的命令來強制執行該壓縮過程。但是要清楚:這個命令是不可回退的。如果你希望繼續explore .git/objects資料夾的內容,你就應該在執行下面的命令之前進行。

git gc

上面這條命令將會壓縮各個object files形成更快更小的pack file並且刪除一些不再引用的commit(比如frrom a deleted, unmerged branch)。

當然,所有相同objectID的都將可用git cat-file來訪問。該git gc命令只是更改git的儲存機制--而不是更改repo的內容。

Add Files to the Index

到現在,我們已經討論過git的low-level representation of commited snapshots.這篇文章中,下面我們將各個零件轉動運轉起來,使用更多的"plumbing"命令來手工準備和提交一個新的snapshot。這將徹底解密GIT是如何管理working directory和staging area的。

在my-git-repo中建立一個新的檔案,命名為news-4.html,並且增加一些html程式碼;然後修改Index.html檔案的news section以便生成一個指向news-4.html的連結。

這時,正常情況下,我們將git add, git commit提交我們的變更。現在我們來試圖使用更加低階的命令來手工完成這個操作。index是git的術語,代表了staged snapshot。

git status
git update-index index.html
git update-index news-4.html

後面的命令將會丟擲一個錯誤,因為git在你不明顯告訴他news-4.html是一個新檔案時,他是不允許加入他不知道的檔案到stage area的。相反地,我們少做修改,增加--add引數:

git update-index --add news-4.html
git status

我們通過上面的update-index命令將我們的working directory搬到了Index區,這意味著我們已經有了一個準備好的snapshot了,剩下來要做的事情就是commit了。

Store the Index in the Database

記住:所有的commits都引用到一個tree object,而這個tree object就代表了那個commit的snapshot。所以,在建立一個commit物件之前,我們需要將我們的Index(staged tree)放到git的object database中。我們可以通過下面的命令來達到目的:

git write-tree

這個命令從index建立一個tree object並且儲存在.git/objects目錄中。它將輸出這個命令結果的tree的checksumid:

5f44809ed995e5b861acf309022ab814ceaaafd6

你可以通過git ls-tree來檢查你的新創的snapshot。記住這個commit中唯一新創的blob是index.html和news-4.html,這個tree的剩餘內容引用了已經存在的blobs

git ls-tree 5f44809

所以,我們已經有了我們的tree object了,但是我們必需將他放到我們專案的歷史中去。

Create a Commit Object

為了commit the new tree object,我們可以手工地找到parent commit的ID:

git log --oneline -n 1

這條命令將輸出下面的內容,我們將使用這個commitID來指定新的commit物件的parent

3329762 Add .gitignore file

git commit-tree命令建立一個commit object根據傳入的tree和parentID引數,但是author資訊卻是從git的一個環境變數來讀取的。

git commit-tree 5f44809 -p 3329762

這條命令將需要更多的輸入: commit message.就像我們在做commit時要求輸入commit message一樣操作就可以了。

該命令最終輸出

c51dc1b3515f9f8e80536aa7acb3d17d0400b0b5

現在你就可以在.git/objects目錄下檢視到這個commit了,但是無論是HEAD或者是branches都沒有被自動更新而包含這個commit. 現在這就是一個dangling commit。好訊息是,我們知道git在哪裡儲存branch資訊的:

Update HEAD

既然我們並不是在一個detached HEAD state, HEAD就是一個對一個branch的引用。所以,我們要更新HEAD需要做的就是移動master branch,向前指向到我們的最新的commit object.這個工作可以通過直接在文字編輯器中修改.git/refs/heads/master為我們commit-tree命令的輸出(commit objectid)來完成。

如果這個檔案根本不存在,也不用煩惱,這僅僅意味著git gc命令packed up all of our branch references into single file.在這種情況下,我們無法來更新。git/refs/heads/master,但是我們應該開啟.git/packed-refs,找到獨一refs/heads/master的引用的那行,修改即可。

既然我們的master branch指向了新的ecommit,我們應該可以在專案歷史中看到news-4.html檔案了。

git log -n 2

上面的章節我們解釋了當我們執行git commit -a -m "some message"時所有傳送在背後的真實事情。你是否覺得還是不用超低階命令的好呢?

 commited, modified, staged

注意:在Git中,你的檔案將有三個可能的狀態存在:commited, modified, staged. Commited意味著資料已經安全地儲存到了你的local database. Modified意味著你已經修改了這個檔案但是還沒有放到資料庫中。staged意味著你已經標示了modified files,以便作為下一個commit的snapshot。這也導致了一個Git專案具有三個不同的section: Git directory, working directory, staging area.

Git directory是Git用於儲存其metadata和objects的地方所在。這個目錄是git最重要的部分,這個也是當你clone一個repo時copy的部分。working directory是你的專案的一個版本的checkout.這些檔案是從git directory的compress database中抽取出來的,並且將這些內容放到磁碟中供你來修改和使用。

staging area就是一個檔案,通常就放在.git目錄中,這個檔案儲存了關於下一個commit的所有資訊。有時我們又稱之為index,但是更多的情況下,人們稱之為staging area. 基本的GIT workflow像下面這個樣子:

1.你在working directory中修改檔案;

2.你stage這些修改,adding snapshots of them to your staging area;

3.你做一個commit,這個動作將把stage區中的snapshot永遠儲存於git directory的repo資料庫中。

如果一個檔案在Git directory中存在了,那麼就被認為被commited了。如果檔案modified,並且已經放到staging area了,那麼成為it is staged.如果自從該檔案被checkout出來後做了修改,但是卻還沒有staged,那麼他就是modified狀態。

.git/index這個檔案實際上就是staging area,它會實時記錄你準備放到下一個commit的snapshot,我們可以用ls-files來檢查他的內容(他是binary檔案)

git ls-files --stage

git ls-files -stage //記住:該命令實際上檢視的是.git/index檔案本身
//輸入如下內容:
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       anotherfile.txt
100644 303bcbd14ef854ea5ac85a4896e5f37a11dd4394 0       hotfixbranch.txt
100644 41d9b439a7c42e9f65c5382d992a0b158ce43956 0       readme.txt
100644 b64aa37a6bd9fc4f66fe86b9c1d07b821fbf3966 0       third

由於我們已經修改了readme.txt檔案並且放到staging area,那麼這時我們來檢視這個檔案的話,

git cat-file -t 41d9b4  一定輸出blob

D:\gittest>git cat-file blob 41d9
readme.txt first version
second version readme
to check the index area

你可以看到最後一行就是最新的更改,但是上述命令卻將整個readme.txt檔案都作為blob來輸出了@

 tracked and untracked

記住:在你的working directory中任何一個檔案都有兩種可能的狀態:tracked or untracked. tracked檔案是那些在最近的snapshot中存在的檔案,他們可以被修改,反修改或者staged. Untracked檔案是任何你的working directory中沒有在你的last snapshot中並且未在staging area中的檔案。也就是說只要是曾經執行過git add命令的,都是tracked file。

 

相關文章