原文: http://rypress.com/tutorials/git/plumbinghtml
本文詳細介紹GIT Plumbing--更加底層的git命令,你將會對git在內部是如何管理和呈現一個項目repo有一個深刻的理解。git
除非你想通讀Git源代碼,你可能永遠沒有必要使用下面的命令。可是經過手工的操做一個repo將會讓你對於GIT如何保存數據的概念細節有個深刻理解,你也將對於git是如何工做的有更好的理解。數據庫
咱們首先來檢閱Git的object database,而後咱們使用git的低級命令手工建立和commit一個snapshot安全
首先咱們經過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.spa
一個tree object是git對於一個"snapshot"的表明。他們保存了一個目錄在特定時刻的狀態,該object沒有任何關於時間或者做者的信息。爲了將tree和項目一向的歷史信息關聯起來,GIT將每個tree對象包裝在一個commit對象中,而且指定一個parent,而這個parent實際上就是另一個commit.經過遍歷每個commit的parent,你就能夠遍歷完項目的整個歷史。3d
注意每個commit refers to one and only one tree object,也就是說commit和tree是一一對應的。從git cat-file輸出的內容看,咱們可使用SHA checksum來表明那個tree.這個SHA CHECKSUM對於GIT內部每個變量都是適用的。code
下面咱們使用git cat-file命令來檢閱一個TREE對象。注意將相關的id更新爲你的tree的id:htm
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之間的關係能夠用下面的圖形象展現:
咱們來看看和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也有一些增量修改的機制來保證這個尺寸增長不是很大的問題。
第四個也是最後一個git object是tag對象.咱們可使用git cat-file命令來顯示tag的detail存儲信息:
git cat-file tag v2.0
上面的命令將輸出和v2.0這個tag相關的commitID,以及tag的名稱,做者,建立時間和附加信息。下面更新最後版本的git對象圖:
咱們如今有了可以徹底瀏覽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,來接着作一些其餘的實驗:
當咱們有一個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 ,
gitcat-fileblob7a52bb8
:該命令就是從新組合咱們的object,而且若是發現它是blob對象,咱們就將獲得其內容,若是是tree對象,則用lstree命令
隨着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的內容。
到如今,咱們已經討論過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了。
記住:全部的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
gitls-tree5f44809
因此,咱們已經有了咱們的tree object了,可是咱們必需將他放到咱們項目的歷史中去。
爲了commit the new tree object,咱們能夠手工地找到parent commit的ID:
git log --oneline -n 1
這條命令將輸出下面的內容,咱們將使用這個commitID來指定新的commit對象的parent
Add .gitignore file3329762
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信息的:
既然咱們並非在一個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"時全部發送在背後的真實事情。你是否以爲仍是不用超低級命令的好呢?
注意:在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來輸出了@
記住:在你的working directory中任何一個文件都有兩種可能的狀態:tracked or untracked. tracked文件是那些在最近的snapshot中存在的文件,他們能夠被修改,反修改或者staged. Untracked文件是任何你的working directory中沒有在你的last snapshot中而且未在staging area中的文件。也就是說只要是曾經執行過git add命令的,都是tracked file。