庖丁为文惠公解牛,游刃有余。
文惠公曰:“善哉,技盖至此乎?”
庖丁释刀对曰:“臣之所好者道也,进乎技矣。”-- 庄子
你已经见识过 Git 的威力,正是因为 Git,使得社区协作变得如此简单易行。也许你会认为,强大功能的背后,是一套复杂艰涩的抽象模型。然而,强大并不意味着复杂,越是优雅的程序,往往也越是高效。事实上,Git 作为眼下最为流行的版本管理工具,所依托的是一组至为简洁的数据结构,简洁到只需要很短的篇幅就能够把其中的核心概念讲解清楚。
Git 维护着一个微型的文件系统,其中的文件也被称作数据对象。所有的数据对象均存储于项目下面的 .git/objects
目录中。
例如,在项目 dota-game
中,创建一个 README
文件并且添加到版本库中:
$ git init dota-game && cd dota-game
$ echo -n "42 is the answer to life the universe and everything." > README
$ git add README
此时,我们看到,Git 已经把这个文件记录在案:
$ find .git/objects -type f
.git/objects/81/f41231377346156ef312dffb6716c88826b97c
这样的一个数据对象,被称作 Blob 对象。我们可以通过下面的命令把文件内容重新打捞回来:
$ git cat-file -p 81f41242 is the answer to life the universe and everything.
版本库中的每一个文件,不论是图片、源文件还是二进制文件,都被映射为一个 Blob 对象。除了 Blob 对象,在 Git 的文件系统中还存储着另外三种数据对象:Tree 对象,Commit 对象和 Tag 对象。
Blob 对象
Blob 是英文 Binary large object 的缩写,一个 Blob 对象就是一段二进制数据。
让我们添加另一个文件到版本库中:
$ 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
通过下面的命令查看数据对象的类型:
$ git cat-file -t 64fe72
blob
为了把文件映射为 Blob 对象,Git 做了下面这些工作:
- 读取文件内容,添加一段特殊标记到头部,得到新的内容,记为 content;
- 对该 content 执行 SHA-1 加密,得到一个长度为40字符的 hash 值,例如 64fe72272a79bff953d7de2062d3f52b4679c659;
- 取该 hash 值的前两位作为子目录,剩下的38位作为文件名,在本例中,子目录名是'64/',文件名是'fe72272a79bff953d7de2062d3f52b4679c659';
- 对 content 执行 zip 压缩,得到新的二进制内容,存入文件中。
这段 Python 代码帮助我们理解整个过程:
import hashlib
import zlib
src = open('README', 'r')
file_content = src.read() # 42 is the answer to life the universe and everything.
src.close()
# 添加特殊标记到内容的头部
new_content = 'blob %u\0%s' % (len(file_content), file_content)
# 对内容执行 SHA-1 加密
sha1 = hashlib.sha1()
sha1.update(new_content)
hash_str = sha1.hexdigest() # 81f41231377346156ef312dffb6716c88826b97c
# 对内容执行 zip 压缩
compressed_content = zlib.compress(new_content)
# 存储
dst = open('.git/objects/%s/%s' % (hash_str[:2], hash_str[2:]), 'wb+):
dst.write(compressed_content)
dst.close()
Tree 对象
Git 使用一种与 UNIX 文件系统相似的方式来管理内容,Blob 相当于磁盘文件,Tree 则相当于文件夹。Tree 中既可以包含 Blob,也可以包含其他 Tree。
向版本库中提交当前的修改:
$ 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
目录下面多出了两个对象,这两个对象的类型分别是 commit 和 tree:
$ git cat-file -t 2bafd8
commit
$
$ git cat-file -t e5526c
tree
下文会讲到 Commit 对象,暂且先不管它。查看 e5526c
这个对象的内容:
$ git cat-file -t e5526c
100644 blob 81f41231377346156ef312dffb6716c88826b97c README
100644 blob 64fe72272a79bff953d7de2062d3f52b4679c659 main.py
可见这颗树就相当于项目的根目录。
添加另一个文件 src/hero.py
到版本库中:
$ 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
现在,版本库中又多出来4个对象:246474
、57e44b
、bc6f97
以及 c5cbfa
。除去 c5cbfa
与 2bafd8
两个 commit 对象之外,其他对象的关系如下图所示:
Commit 对象
一个 Commit 对象代表了一次提交对象,它包含了下面这些信息:
- 何人何时作了该次提交
- 该次提交的简略说明
- 一棵树
- 父级 Commit 对象
其中,这颗树也被称作项目快照(snapshort),通过项目快照,我们可以把项目还原成项目在该次提交时的样子。一般来说,commit 对象总有一个父级 commit 对象,一个又一个 commit 对象通过这种方式链接起来,就构成了一条提交历史。第一次提交的 commit 对象没有父级 commit 对象,分支合并所产生的新的 commit 对象可以有两个或者多个父级 commit 对象。
例如,c5cbfa
这个对象的内容为:
$ 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
经过两次提交之后,版本库中所有对象的关系如下图所示:
Tag 对象
Tag 指向一次特征提交。
在 Git 中有两种 tag,第一种 tag 并不在 .git/objects
目录下面创建新的对象,只是在 .git/refs/tags
目录中新建一个文件,文件的内容就是所指向的 commit 对象的 hash 值:
$ git tag v1.0
$
$ find .git/refs/tags -type f
v1.0
$
$ cat .git/refs/tags/v1.0
c5cbfa0f491087c575d8856632451f8d8763b94f
另一种 tag 则会在 .git/objects
目录下面创建对象,这种 tag 被称作注解标签(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 对象上面:
$ 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
总结
在 Git 的底层,有四种数据结构,它们分别是:
- Blob
- Tree
- Commit
- Tag
Git 把版本库中的每一个文件都转换为一个 blob 对象进行存储,而用 tree 对象来表达文件的层次结构。
Commit 对象代表了一次提交操作,它包含了当前的项目快照以及提交人和提交日期等诸多信息。所有的 commit 对象串接起来,组成一个有向无环图。从版本控制的角度看,这些 commit 对象构成了一个完整的版本提交记录;从项目开发的角度看,它们描述了项目是如何从无到有一点一滴地构建起来的。
Tag 对象指向一个 commit 对象,我们可以通过 tag 对象快速访问到项目的某一次特征提交。
敬请期待笔者的下一篇文章:《Git 之术与道 -- 索引》。