我們揭開(kāi)Git神秘面紗,往里瞧瞧它是如何創(chuàng)造奇跡的。我會(huì)跳過(guò)細(xì)節(jié)。更深入的描述參 見(jiàn)?用戶手 冊(cè)。
Git怎么這么謙遜寡言呢?除了偶爾提交和合并外,你可以如常工作,就像不知道版本控 制系統(tǒng)存在一樣。那就是,直到你需要它的時(shí)候,而且那是你歡欣的時(shí)候,Git一直默默 注視著你。
其他版本控制系統(tǒng)強(qiáng)迫你與繁文縟節(jié)和官僚主義不斷斗爭(zhēng)。文件的權(quán)限可能是只讀的, 除非你顯式地告訴中心服務(wù)器哪些文件你打算編輯。即使最基本的命令,隨著用戶數(shù)目 的增多,也會(huì)慢的像爬一樣。中心服務(wù)器可能正跟蹤什么人,什么時(shí)候check out了什么 代碼。當(dāng)網(wǎng)絡(luò)連接斷了的時(shí)候,你就遭殃了。開(kāi)發(fā)人員不斷地與這些版本控制系統(tǒng)的種 種限制作斗爭(zhēng)。一旦網(wǎng)絡(luò)或中心服務(wù)器癱瘓,工作就嘎然而止。
與之相反,Git簡(jiǎn)單地在你工作目錄下的.git`目錄保存你項(xiàng)目的歷史。這是你自己的歷 史拷貝,因此你可以保持離線,直到你想和他人溝通為止。你擁有你的文件命運(yùn)完全的 控制權(quán),因?yàn)镚it可以輕易在任何時(shí)候從
.git`重建一個(gè)保存狀態(tài)。
很多人把加密和保持信息機(jī)密關(guān)聯(lián)起來(lái),但一個(gè)同等重要的目標(biāo)是保證信息安全。合理 使用哈希加密功能可以防止無(wú)意或有意的數(shù)據(jù)損壞行為。
一個(gè)SHA1哈希值可被認(rèn)為是一個(gè)唯一的160位ID數(shù),用它可以唯一標(biāo)識(shí)你一生中遇到的每 個(gè)字節(jié)串。 實(shí)際上不止如此:每個(gè)字節(jié)串可供任何人用好多輩子。
對(duì)一個(gè)文件而言,其整體內(nèi)容的哈希值可以被看作這個(gè)文件的唯一標(biāo)識(shí)ID數(shù)。
因?yàn)橐粋€(gè)SHA1哈希值本身也是一個(gè)字節(jié)串,我們可以哈希包括其他哈希值的字節(jié)串。這 個(gè)簡(jiǎn)單的觀察出奇地有用:查看“哈希鏈”。我們之后會(huì)看Git如何利用這一點(diǎn)來(lái)高效地 保證數(shù)據(jù)完整性。
簡(jiǎn)言之,Git把你數(shù)據(jù)保存在.git/objects
子目錄,那里看不到正常文件名,相反你只 看到ID。通過(guò)用ID作為文件名,加上一些文件鎖和時(shí)間戳技巧,Git把任意一個(gè)原始的文 件系統(tǒng)轉(zhuǎn)化為一個(gè)高效而穩(wěn)定的數(shù)據(jù)庫(kù)。
Git是如何知道你重命名了一個(gè)文件,即使你從來(lái)沒(méi)有明確提及這個(gè)事實(shí)?當(dāng)然,你或許 是運(yùn)行了?git mv?,但這個(gè)命令和?git add?緊接?git rm?是完全一樣的。
Git啟發(fā)式地找出相連版本之間的重命名和拷貝。實(shí)際上,它能檢測(cè)文件之間代碼塊的移 動(dòng)或拷貝!盡管它不能覆蓋所有的情況,但它已經(jīng)做的很好了,并且這個(gè)功能也總在改 進(jìn)中。如果它在你那兒不工作的話,可以嘗試打開(kāi)開(kāi)銷更高的拷貝檢測(cè)選項(xiàng),并考慮升 級(jí)。
為每個(gè)加入管理的文件,Git在一個(gè)名為“index”的文件里記錄統(tǒng)計(jì)信息,諸如大小, 創(chuàng)建時(shí)間和最后修改時(shí)間。為了確定文件是否更改,Git比較其當(dāng)前統(tǒng)計(jì)信息與那些在索 引里的統(tǒng)計(jì)信息。如果一致,那Git就跳過(guò)重新讀文件。
因?yàn)榻y(tǒng)計(jì)信息的調(diào)用比讀文件內(nèi)容快的很多,如果你僅僅編輯了少數(shù)幾個(gè)文件,Git幾乎 不需要什么時(shí)間就能更新他們的統(tǒng)計(jì)信息。
我們前面講過(guò)索引是一個(gè)中轉(zhuǎn)區(qū)。為什么一堆文件的統(tǒng)計(jì)數(shù)據(jù)是一個(gè)中轉(zhuǎn)區(qū)?因?yàn)樘砑?命令將文件放到Git的數(shù)據(jù)庫(kù)并更新它們的統(tǒng)計(jì)信息,而無(wú)參數(shù)的提交命令創(chuàng)建一個(gè)提交, 只基于這些統(tǒng)計(jì)信息和已經(jīng)在數(shù)據(jù)庫(kù)里的文件。
這個(gè)?Linux內(nèi)核郵件列表帖子?描述了導(dǎo)致Git 的一系列事件。整個(gè)討論線索是一個(gè)令人著迷的歷史探究過(guò)程,對(duì)Git史學(xué)家而言。
你數(shù)據(jù)的每個(gè)版本都保存在“對(duì)象數(shù)據(jù)庫(kù)”里,其位于子目錄.git/objects`;其他位 于
.git/`的較少數(shù)據(jù):索引,分支名,標(biāo)簽,配置選項(xiàng),日志,頭提交的當(dāng)前位置等。 對(duì)象數(shù)據(jù)庫(kù)樸素而優(yōu)雅,是Git的力量之源。
.git/objects
里的每個(gè)文件是一個(gè)對(duì)象。有3中對(duì)象跟我們有關(guān):“blob”對(duì)象, “tree”對(duì)象,和“commit”對(duì)象。
首先來(lái)一個(gè)小把戲。去一個(gè)文件名,任意文件名。在一個(gè)空目錄:
$ echo sweet > YOUR_FILENAME
$ git init
$ git add .
$ find .git/objects -type f
你將看到?.git/objects/aa/823728ea7d592acc69b36875a482cdf3fd5c8d
?。
我如何在不知道文件名的情況下知道這個(gè)?這是因?yàn)橐韵聝?nèi)容的SHA1哈希值:
"blob" SP "6" NUL "sweet" LF
是 aa823728ea7d592acc69b36875a482cdf3fd5c8d,這里SP是一個(gè)空格,NUL是一個(gè)0字節(jié), LF是一個(gè)換行符。你可以驗(yàn)證這一點(diǎn),鍵入:
$ printf "blob 6\000sweet\n" | sha1sum
Git基于“內(nèi)容尋址”:文件并不按它們的文件名存儲(chǔ),而是按它們包含內(nèi)容的哈希值, 在一個(gè)叫“blob對(duì)象”的文件里。我們可以把文件內(nèi)容的哈希值看作一個(gè)唯一ID,這樣 在某種意義上我們通過(guò)他們內(nèi)容放置文件。開(kāi)始的“blob 6”只是一個(gè)包含對(duì)象類型與 其長(zhǎng)度的頭;它簡(jiǎn)化了內(nèi)部存儲(chǔ)。
這樣我可以輕易語(yǔ)言你所看到的。文件名是無(wú)關(guān)的:只有里面的內(nèi)容被用作構(gòu)建blob對(duì)象。
你可能想知道對(duì)相同的文件什么會(huì)發(fā)生。試圖加一個(gè)你文件的拷貝,什么文件名都行。 在?.git/objects
?的內(nèi)容保持不變,不管你加了多少。Git只存儲(chǔ)一次數(shù)據(jù)。
順便說(shuō)一句,在?.git/objects
?里的文件用zlib壓縮,因此你不應(yīng)該直接查看他們。 可以通過(guò)zpipe -d?管道, 或者鍵入:
$ git cat-file -p aa823728ea7d592acc69b36875a482cdf3fd5c8d
這漂亮地打印出給定的對(duì)象。
但文件名在哪?它們必定在某個(gè)階段保存在某個(gè)地方。Git在提交時(shí)得到文件名:
$ git commit # 輸入一些信息。
$ find .git/objects -type f
你應(yīng)看到3個(gè)對(duì)象。這次我不能告訴你這兩個(gè)新文件是什么,因?yàn)樗糠忠蕾嚹氵x擇的文 件名。我繼續(xù)進(jìn)行,假設(shè)你選了‘`rose’'。如果你沒(méi)有,你可以重寫(xiě)歷史以讓它看起來(lái) 像似你做了:
$ git filter-branch --tree-filter 'mv YOUR_FILENAME rose'
$ find .git/objects -type f
現(xiàn)在你硬看到文件?.git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9
?,因?yàn)檫@是以下內(nèi)容的SHA1哈希值:
"tree" SP "32" NUL "100644 rose" NUL 0xaa823728ea7d592acc69b36875a482cdf3fd5c8d
檢查這個(gè)文件真的包含上面內(nèi)容通過(guò)鍵入:
$ echo 05b217bb859794d08bb9e4f7f04cbda4b207fbe9 | git cat-file --batch
使用zpipe,驗(yàn)證哈希值是容易的:
$ zpipe -d < .git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9 | sha1sum
與查看文件相比,哈希值驗(yàn)證更技巧一些,因?yàn)槠漭敵霾恢拱嘉磯嚎s文件。
這個(gè)文件是一個(gè)“tree”對(duì)象:一組數(shù)據(jù)包含文件類型,文件名和哈希值。在我們的例 子里,文件類型是100644,這意味著“rose”是一個(gè)一般文件,并且哈希值指blob對(duì)象, 包含“rose”的內(nèi)容。其他可能文件類型有可執(zhí)行,鏈接或者目錄。在最后一個(gè)例子里, 哈希值指向一個(gè)tree對(duì)象。
在一些過(guò)渡性的分支,你會(huì)有一些你不在需要的老的對(duì)象,盡管有寬限過(guò)期之后,它們 會(huì)被自動(dòng)清除,現(xiàn)在我們還是將其刪除,以使我們比較容易跟上這個(gè)玩具例子。
$ rm -r .git/refs/original
$ git reflog expire --expire=now --all
$ git prune
在真實(shí)項(xiàng)目里你通常應(yīng)該避免像這樣的命令,因?yàn)槟阍谄茡Q備份。如果你期望一個(gè)干凈 的倉(cāng)庫(kù),通常最好做一個(gè)新的克隆。還有,直接操作?.git
?時(shí)一定要小心:如果 Git命令同時(shí)也在運(yùn)行會(huì)怎樣,或者突然停電?一般,引用應(yīng)由?git update-ref -d?刪除,盡管通常手工刪除?refs/original
?也是安全的。
我們已經(jīng)解釋了三個(gè)對(duì)象中的兩個(gè)。第三個(gè)是“commit”對(duì)象。其內(nèi)容依賴于提交信息 以及其創(chuàng)建的日期和時(shí)間。為滿足這里我們所有的,我們不得不調(diào)整一下:
$ 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
你現(xiàn)在應(yīng)看到?.git/objects/49/993fe130c4b3bf24857a15d7969c396b7bc187
?是下列 內(nèi)容的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
和前面一樣,你可以運(yùn)行zpipe或者cat-file來(lái)自己看。
這是第一個(gè)提交,因此沒(méi)有父提交,但之后的提交將總有至少一行,指定一個(gè)父提交。
Git的秘密似乎太簡(jiǎn)單??雌饋?lái)似乎你可以整合幾個(gè)shell腳本,加幾行C代碼來(lái)弄起來(lái), 也就幾個(gè)小時(shí)的事:一個(gè)基本文件操作和SHA1哈希化的混雜,用鎖文件裝飾一下,文件 同步保證健壯性。實(shí)際上,這準(zhǔn)確描述了Git的最早期版本。盡管如此,除了巧妙地打包 以節(jié)省空間,巧妙地索引以省時(shí)間,我們現(xiàn)在知道Git如何靈巧地改造文件系統(tǒng)成為一個(gè) 對(duì)版本控制完美的數(shù)據(jù)庫(kù)。
例如,如果對(duì)象數(shù)據(jù)庫(kù)里的任何一個(gè)文件由于硬盤(pán)錯(cuò)誤損毀,那么其哈希值將不再匹配, 這個(gè)錯(cuò)誤會(huì)報(bào)告給我們。通過(guò)哈?;渌麑?duì)象的哈希值,我們?cè)谒袑用婢S護(hù)數(shù)據(jù)完整 性。Commit對(duì)象是原子的,也就是說(shuō),一個(gè)提交永遠(yuǎn)不會(huì)部分地記錄變更:在我們已經(jīng) 存儲(chǔ)所有相關(guān)tree對(duì)象,blob對(duì)象和父commit對(duì)象之后,我們才可以計(jì)算提交的的哈希 值并將其存儲(chǔ)在數(shù)據(jù)庫(kù),對(duì)象數(shù)據(jù)庫(kù)不受諸如停電之類的意外中斷影響。
我們打敗即使是最狡猾的對(duì)手。假設(shè)有誰(shuí)試圖悄悄修改一個(gè)項(xiàng)目里一個(gè)遠(yuǎn)古版本文件的 內(nèi)容。為使對(duì)象據(jù)庫(kù)看起來(lái)健康,他們也必須修改相應(yīng)blob對(duì)象的哈希值,既然它現(xiàn)在 是一個(gè)不同的字節(jié)串。這意味著他們講不得不引用這個(gè)文件的tree對(duì)象的哈希值,并反 過(guò)來(lái)改變所有與這個(gè)tree相關(guān)的commit對(duì)象的哈希值,還要加上這些提交所有后裔的哈 希值。這暗示官方head的哈希值與這個(gè)壞倉(cāng)庫(kù)不同。通過(guò)跟蹤不匹配哈希值線索,我 們可以查明殘缺文件,以及第一個(gè)被破壞的提交。
總之,只要20個(gè)字節(jié)代表最后一次提交的是安全的,不可能篡改一個(gè)Git倉(cāng)庫(kù)。
那么Git的著名功能怎樣呢?分支?合并?標(biāo)簽?單純的細(xì)節(jié)。當(dāng)前head保存在文件?.git /HEAD
?,其中包含了一個(gè)commit對(duì)象的哈希值。該哈希值在運(yùn)行提交以及其他命 令是更新。分支幾乎一樣:它們是保存在?.git/refs/heads
?的文件。標(biāo)簽也是:它們 住在住在?.git/refs/tags
?,但它們由一套不同的命令更新。
更多建議: