Git 之术与道 -- 对象

作者: song4 | 来源:发表于2015-09-06 21:16 被阅读2787次

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

    -- 庄子

    你已经见识过 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 之术与道 -- 索引》。

    相关文章

      网友评论

      • 小凡凡520:good mark 学习了
      • 8a9237f48c12:看懂了, :smile:
      • c16dbc31c04f:最近刚开始接触git,很受用,期待更新!
      • amdops:我想问一下,你提到的对象之间的关系图,是你自己通过绘图软件绘制的,还是说,有什么工具可以直接看到.git/objects对象之间的关系呢?
        amdops:@song4 厉害。
        song4:@amdops 图都是我自己画的。
      • fewerworld:来拜技术文,git是目前我个人最想学也最学不会的东西,Towards大神把它搞得神乎其神的……
        fewerworld:@song4 你说的这个问题我还真想过,但是换做是我,我就麻爪了,不会像他一样去想问题,毕竟跟人家也不是一个级别,我也许会继续忍受CVS的重负……
        song4:@fewerworld 可以试着从 Torvalds 的角度想一想,当初他面对的是什么实际问题,换做你的话你会怎么解决。其实 Git 的核心概念还是很容易理解的,再难的路,只要肯走总会走到终点。
      • Boy_iOS:在于我们朝着
      • 妙法莲花1234:谢谢分享,很好理解
        微凉一季:@追风917 不客气

      本文标题:Git 之术与道 -- 对象

      本文链接:https://www.haomeiwen.com/subject/seuecttx.html