×

Git 之术与道 -- 对象

96
song4
2015.09.06 21:16* 字数 1471

庖丁为文惠公解牛,游刃有余。
文惠公曰:“善哉,技盖至此乎?”
庖丁释刀对曰:“臣之所好者道也,进乎技矣。”

-- 庄子

你已经见识过 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 做了下面这些工作:

  1. 读取文件内容,添加一段特殊标记到头部,得到新的内容,记为 content;
  2. 对该 content 执行 SHA-1 加密,得到一个长度为40字符的 hash 值,例如 64fe72272a79bff953d7de2062d3f52b4679c659;
  3. 取该 hash 值的前两位作为子目录,剩下的38位作为文件名,在本例中,子目录名是'64/',文件名是'fe72272a79bff953d7de2062d3f52b4679c659';
  4. 对 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个对象:24647457e44bbc6f97 以及 c5cbfa。除去 c5cbfa2bafd8 两个 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 之术与道 -- 索引》。

Tech
Web note ad 1