Git 之術(shù)與道 -- 對(duì)象

庖丁為文惠公解牛,游刃有余。
文惠公曰:“善哉,技蓋至此乎?”
庖丁釋刀對(duì)曰:“臣之所好者道也,進(jìn)乎技矣。”

-- 莊子

你已經(jīng)見(jiàn)識(shí)過(guò) Git 的威力,正是因?yàn)?Git,使得社區(qū)協(xié)作變得如此簡(jiǎn)單易行。也許你會(huì)認(rèn)為,強(qiáng)大功能的背后,是一套復(fù)雜艱澀的抽象模型。然而,強(qiáng)大并不意味著復(fù)雜,越是優(yōu)雅的程序,往往也越是高效。事實(shí)上,Git 作為眼下最為流行的版本管理工具,所依托的是一組至為簡(jiǎn)潔的數(shù)據(jù)結(jié)構(gòu),簡(jiǎn)潔到只需要很短的篇幅就能夠把其中的核心概念講解清楚。

Git 維護(hù)著一個(gè)微型的文件系統(tǒng),其中的文件也被稱(chēng)作數(shù)據(jù)對(duì)象。所有的數(shù)據(jù)對(duì)象均存儲(chǔ)于項(xiàng)目下面的 .git/objects
目錄中。

例如,在項(xiàng)目 dota-game 中,創(chuàng)建一個(gè) README 文件并且添加到版本庫(kù)中:

$ git init dota-game && cd dota-game
$ echo -n "42 is the answer to life the universe and everything." > README
$ git add README

此時(shí),我們看到,Git 已經(jīng)把這個(gè)文件記錄在案:

$ find .git/objects -type f
.git/objects/81/f41231377346156ef312dffb6716c88826b97c

這樣的一個(gè)數(shù)據(jù)對(duì)象,被稱(chēng)作 Blob 對(duì)象。我們可以通過(guò)下面的命令把文件內(nèi)容重新打撈回來(lái):

$ git cat-file -p 81f41242 is the answer to life the universe and everything.

版本庫(kù)中的每一個(gè)文件,不論是圖片、源文件還是二進(jìn)制文件,都被映射為一個(gè) Blob 對(duì)象。除了 Blob 對(duì)象,在 Git 的文件系統(tǒng)中還存儲(chǔ)著另外三種數(shù)據(jù)對(duì)象:Tree 對(duì)象,Commit 對(duì)象和 Tag 對(duì)象。

Blob 對(duì)象

Blob 是英文 Binary large object 的縮寫(xiě),一個(gè) Blob 對(duì)象就是一段二進(jìn)制數(shù)據(jù)。

讓我們添加另一個(gè)文件到版本庫(kù)中:

$ echo -n "print 'PHP is the best language in the universe.'" > main.py
$ git add main.py
$
$ find .git/objects -type f
.git/objects/64/fe72272a79bff953d7de2062d3f52b4679c659    *
.git/objects/81/f41231377346156ef312dffb6716c88826b97c

通過(guò)下面的命令查看數(shù)據(jù)對(duì)象的類(lèi)型:

$ git cat-file -t 64fe72
blob

為了把文件映射為 Blob 對(duì)象,Git 做了下面這些工作:

  1. 讀取文件內(nèi)容,添加一段特殊標(biāo)記到頭部,得到新的內(nèi)容,記為 content;
  2. 對(duì)該 content 執(zhí)行 SHA-1 加密,得到一個(gè)長(zhǎng)度為40字符的 hash 值,例如 64fe72272a79bff953d7de2062d3f52b4679c659;
  3. 取該 hash 值的前兩位作為子目錄,剩下的38位作為文件名,在本例中,子目錄名是'64/',文件名是'fe72272a79bff953d7de2062d3f52b4679c659';
  4. 對(duì) content 執(zhí)行 zip 壓縮,得到新的二進(jìn)制內(nèi)容,存入文件中。

這段 Python 代碼幫助我們理解整個(gè)過(guò)程:

import hashlib
import zlib

src = open('README', 'r')
file_content = src.read()    # 42 is the answer to life the universe and everything.
src.close()

# 添加特殊標(biāo)記到內(nèi)容的頭部
new_content = 'blob %u\0%s' % (len(file_content), file_content)

# 對(duì)內(nèi)容執(zhí)行 SHA-1 加密
sha1 = hashlib.sha1()
sha1.update(new_content)
hash_str = sha1.hexdigest()  # 81f41231377346156ef312dffb6716c88826b97c

# 對(duì)內(nèi)容執(zhí)行 zip 壓縮
compressed_content = zlib.compress(new_content)

# 存儲(chǔ)
dst = open('.git/objects/%s/%s' % (hash_str[:2], hash_str[2:]), 'wb+):
dst.write(compressed_content)
dst.close()

Tree 對(duì)象

Git 使用一種與 UNIX 文件系統(tǒng)相似的方式來(lái)管理內(nèi)容,Blob 相當(dāng)于磁盤(pán)文件,Tree 則相當(dāng)于文件夾。Tree 中既可以包含 Blob,也可以包含其他 Tree。

向版本庫(kù)中提交當(dāng)前的修改:

$ git commit -m "first commit"
$
$ find .git/objects -type f
.git/objects/2b/afd8d408af85faf951445e3aea7d7f874cb806    *
.git/objects/64/fe72272a79bff953d7de2062d3f52b4679c659
.git/objects/81/f41231377346156ef312dffb6716c88826b97c
.git/objects/e5/526c066cdb2b17fc37ba2f2f44cdaca86b7bf2    *

.git/objects 目錄下面多出了兩個(gè)對(duì)象,這兩個(gè)對(duì)象的類(lèi)型分別是 commit 和 tree:

$ git cat-file -t 2bafd8
commit
$
$ git cat-file -t e5526c
tree

下文會(huì)講到 Commit 對(duì)象,暫且先不管它。查看 e5526c 這個(gè)對(duì)象的內(nèi)容:

$ git cat-file -t e5526c
100644 blob 81f41231377346156ef312dffb6716c88826b97c    README
100644 blob 64fe72272a79bff953d7de2062d3f52b4679c659    main.py

可見(jiàn)這顆樹(shù)就相當(dāng)于項(xiàng)目的根目錄。

添加另一個(gè)文件 src/hero.py 到版本庫(kù)中:

$ mkdir src
$ echo -n "print 'hero'" > src/hero.py
$ git add src/hero.py
$ git commit -m "second commit"
$
$ find .git/objects -type f
.git/objects/24/6474cab5a5019936a54041ccdddd07398cdf94    *
.git/objects/2b/afd8d408af85faf951445e3aea7d7f874cb806
.git/objects/57/e44b9798892d4ac1b63963d7e6a5653dddde7e    *
.git/objects/64/fe72272a79bff953d7de2062d3f52b4679c659
.git/objects/81/f41231377346156ef312dffb6716c88826b97c
.git/objects/bc/6f978c49b6a6f1190fdb25eabba78494e2606b    *
.git/objects/c5/cbfa0f491087c575d8856632451f8d8763b94f    *
.git/objects/e5/526c066cdb2b17fc37ba2f2f44cdaca86b7bf2

現(xiàn)在,版本庫(kù)中又多出來(lái)4個(gè)對(duì)象:24647457e44bbc6f97 以及 c5cbfa。除去 c5cbfa2bafd8 兩個(gè) commit 對(duì)象之外,其他對(duì)象的關(guān)系如下圖所示:

Commit 對(duì)象

一個(gè) Commit 對(duì)象代表了一次提交對(duì)象,它包含了下面這些信息:

  • 何人何時(shí)作了該次提交
  • 該次提交的簡(jiǎn)略說(shuō)明
  • 一棵樹(shù)
  • 父級(jí) Commit 對(duì)象

其中,這顆樹(shù)也被稱(chēng)作項(xiàng)目快照(snapshort),通過(guò)項(xiàng)目快照,我們可以把項(xiàng)目還原成項(xiàng)目在該次提交時(shí)的樣子。一般來(lái)說(shuō),commit 對(duì)象總有一個(gè)父級(jí) commit 對(duì)象,一個(gè)又一個(gè) commit 對(duì)象通過(guò)這種方式鏈接起來(lái),就構(gòu)成了一條提交歷史。第一次提交的 commit 對(duì)象沒(méi)有父級(jí) commit 對(duì)象,分支合并所產(chǎn)生的新的 commit 對(duì)象可以有兩個(gè)或者多個(gè)父級(jí) commit 對(duì)象。

例如,c5cbfa 這個(gè)對(duì)象的內(nèi)容為:

$ git cat-file -p c5cbfa
tree 57e44b9798892d4ac1b63963d7e6a5653dddde7e
parent 2bafd8d408af85faf951445e3aea7d7f874cb806
author xxx <xxx@gmail.com> 1434966496 +0800
committer xxx <xxx@gmail.com> 1434966496 +0800

second commit

經(jīng)過(guò)兩次提交之后,版本庫(kù)中所有對(duì)象的關(guān)系如下圖所示:

Tag 對(duì)象

Tag 指向一次特征提交。

在 Git 中有兩種 tag,第一種 tag 并不在 .git/objects 目錄下面創(chuàng)建新的對(duì)象,只是在 .git/refs/tags 目錄中新建一個(gè)文件,文件的內(nèi)容就是所指向的 commit 對(duì)象的 hash 值:

$ git tag v1.0
$
$ find .git/refs/tags -type f
v1.0
$
$ cat .git/refs/tags/v1.0
c5cbfa0f491087c575d8856632451f8d8763b94f

另一種 tag 則會(huì)在 .git/objects 目錄下面創(chuàng)建對(duì)象,這種 tag 被稱(chēng)作注解標(biāo)簽(annotated tag):

$ git tag -a v1.0 -m "Version 1.1"
$
$ find .git/objects -type f
.git/objects/24/6474cab5a5019936a54041ccdddd07398cdf94
.git/objects/2b/afd8d408af85faf951445e3aea7d7f874cb806
.git/objects/57/e44b9798892d4ac1b63963d7e6a5653dddde7e
.git/objects/64/fe72272a79bff953d7de2062d3f52b4679c659
.git/objects/81/f41231377346156ef312dffb6716c88826b97c
.git/objects/bc/6f978c49b6a6f1190fdb25eabba78494e2606b
.git/objects/c5/cbfa0f491087c575d8856632451f8d8763b94f
.git/objects/e5/526c066cdb2b17fc37ba2f2f44cdaca86b7bf2
.git/objects/ec/7ed5c26520dd5d16b5189b6fbc7914c56b081a    *

git cat-file 命令同樣可以用在 tag 對(duì)象上面:

$ git cat-file -t ec7ed5
tag
$
$ git cat-file -p ec7ed5
object c5cbfa0f491087c575d8856632451f8d8763b94f
type commit
tag v1.1
tagger xxx <xxx@gmail.com> 1434970701 +0800

Version 1.1

總結(jié)

在 Git 的底層,有四種數(shù)據(jù)結(jié)構(gòu),它們分別是:

  • Blob
  • Tree
  • Commit
  • Tag

Git 把版本庫(kù)中的每一個(gè)文件都轉(zhuǎn)換為一個(gè) blob 對(duì)象進(jìn)行存儲(chǔ),而用 tree 對(duì)象來(lái)表達(dá)文件的層次結(jié)構(gòu)。

Commit 對(duì)象代表了一次提交操作,它包含了當(dāng)前的項(xiàng)目快照以及提交人和提交日期等諸多信息。所有的 commit 對(duì)象串接起來(lái),組成一個(gè)有向無(wú)環(huán)圖。從版本控制的角度看,這些 commit 對(duì)象構(gòu)成了一個(gè)完整的版本提交記錄;從項(xiàng)目開(kāi)發(fā)的角度看,它們描述了項(xiàng)目是如何從無(wú)到有一點(diǎn)一滴地構(gòu)建起來(lái)的。

Tag 對(duì)象指向一個(gè) commit 對(duì)象,我們可以通過(guò) tag 對(duì)象快速訪問(wèn)到項(xiàng)目的某一次特征提交。

敬請(qǐng)期待筆者的下一篇文章:《Git 之術(shù)與道 -- 索引》。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容