Git 底层数据结构研究

Git 作为当前最好用的版本管理工具,在之前的文章中,已经介绍了基本的用法,在这篇文章中,我们来探究一下 Git 的底层是如何构成的。

任意一个使用 git 进行版本管理的目录都会有一个 .git 目录,在这个目录中存储了这个项目的所有的版本管理的信息。

.git 目录解析

使用 git init git-test 初始化一个 git 项目后,在项目目录底下的 .git 目录中发现有如下的内容:

在上面的图片中,有多个文件和文件夹,咱们现在只看最重要的那几个。

  • HEAD
  • branches
  • config
  • objects
  • refs

执行以下命令:

1
2
$ cat HEAD
ref: refs/heads/master

通过上面的操作我们发现 HEAD 的值是一个引用,指向了 refs/heads/master,而我们当前所在的分支就是 master,也就是说 HEAD 的值就是当前所处的分支。

进入 refs 目录发现有如下内容:

再进入 heads 目录中,内容如下:

heads 目录中有 devmaster 两个文件。也就是对应我们的 devmaster 两个分支。那么这两个文件中存储的到底是什么内容呢:

1
2
$ cat master
9fa28775a75a2a0d156e0c37a9b20f04ec94b47a

可以看到 master 文件中储存的是一个哈希值。这个哈希值代表了什么呢?可以通过 Git 提供的接口进行查询:

1
2
$ git cat-file -t 9fa28775a75a2a0d156e0c37a9b20f04ec94b47a
commit

结果得到了一个叫做 commit 的东西,至于这个 commit 时什么我们稍后再说。

heads 目录中,还有一个 tags 目录,这里面存储的就是而我们之前的创建的 tag。来看一下 tag 中存储的内容:

1
2
$ cat v0.0.1
9fa28775a75a2a0d156e0c37a9b20f04ec94b47a

tag 本身也是一个哈希值。同样来看一下 tag 的类型:

1
2
$ git cat-file -t 9fa28775a75a2a0d156e0c37a9b20f04ec94b47a
tag

这里出现了一个 tag 类型(需要注意的是轻量级的标签指向的是一个 commit 而不是单独创建一个 tag 对象,所以在创建 tag 的使用建议带上 -m 参数)。

1
2
3
4
5
6
7
$ git cat-file -p 9fa28775a75a2a0d156e0c37a9b20f04ec94b47a
object 9fa28775a75a2a0d156e0c37a9b20f04ec94b47a
type commit
tag v0.0.2
tagger rayjun <leijun19930412@sina.com> 1549899267 +0800

第二个标签

通过将 git cat-file 的 -t 参数改成 -p,可以看到 tag 储存的其实就是一个 commit。

那么 commit 中储存了什么呢?来看一下:

1
2
3
4
5
6
$ git cat-file -p 9fa28775a75a2a0d156e0c37a9b20f04ec94b47a
tree 291f8d9c7f50c3ab750c2ef8dd182b3903d13292
author rayjun <leijun19930412@sina.com> 1549894768 +0800
committer rayjun <leijun19930412@sina.com> 1549894768 +0800

Update

通过 git cat-file 的 -p 参数,可以看到 commit 中储存了一个 tree 类型的对象。 继续往下看:

1
2
$ git cat-file -p 291f8d9c7f50c3ab750c2ef8dd182b3903d13292
100644 blob d67064b5343698146a1d22c063f80b039020314a readme.md

然后发现这个 tree 中出现了一个 blob 的类型,并且指向一个文件名。再往下看:

1
2
$ git cat-file -p d67064b5343698146a1d22c063f80b039020314a
Read me

发现 blob 对象存储的就是文件的内容。看到这里,基本就明白了,git 的整个数据结构就是由tag、commit、tree 和 blob 等几类基础的数据结构构成。但 tag 其实也就是 commit,所以基础的数据结构也就是 commit、tree 和 blob。这些类型的对象都存储在 objects 文件夹中。也就是是说在 objects 存储了所有文件的版本信息。

config 中保存着当前仓库的配置。

commit、tree 和 blob

Git 有着丰富的操作,那么底层的数据结构是如何支撑起这些复杂的操作的。其实 Git 底层的数据结构很简单,但是却足够灵活,所有的其他复杂的功能都是在这简单的数据结构上构建起来的。主要有 committreeblob 三种数据结构组成。

commit 的概念很简单,在每次执行 git commit 之后都会生成一个 commit 对象,每一个commit 对象都会对应一个仅一个 tree。而 blob 对象就是具体的文件了,每一个文件都是一个 blob 对象。

这三者的关系如下图所示:

在 Git 中,如果一个文件没有被改变,那么在每次提交 commit 时,Git 不会为这个文件生成新的对象,而是使用之前的对象。

随着项目的进行,被添加进文件越来越多,那么 Git 需要管理的对象也就越来越多。那么势必会降低效率。所以 Git 使用了两个方式来提高效率:

  • 除了初始对象外,后续的对象存储的都是文件的差异,而不是全部的文件
  • Git 自带 gc,随着需要管理的对象数量的增加,Git会将部分对象进行打包存储,提高效率

Git 最创建一个新的对象时在磁盘中存储的称之为松散对象,随着对象数量的增加,Git 会自动将这些对象打包成二进制文件,当然也可以手动执行 git gc 来进行对象的打包。如果有远程服务器,每次推送到远程服务器时,都会触发打包过程。************

(完)

参考文献:

  1. https://github.com/progit/progit2
  2. https://git-scm.com/docs

微信公众号

© 2018 ray