git对象存储

之前提到当存储数据内容时,同时会有一个文件头被存储起来。我们花些时间来看看 Git 是如何存储对象的。你将看来如何通过 Ruby 脚本语言存储一个 blob 对象 ,有必要了解一下对象是如何被存储的。以git设计哲学中的test1.txt为例分析blob对象如何被计算校验和?如何被存储?是否和直接调用git命令的结果一致?

sha-1校验和计算

我们需要验证通过ruby计算出来的检验和与这个一致。

为了验证,新建一个git仓库

$ git init
Initialized empty Git repository in /Users/lyc/Desktop/tg/.git/

同样新建一个文件test1.txt,内容为test1,并把它加入暂存区

 

gitt使用’blob ‘ + len(content) + ‘\0’ + content作为文件内容
blob表示对象类型为blob类型,

len表示内容的长度

\0为空字节

其中’blob ‘ + len(content) + ‘\0’ 来组成一个header,然后再将这个header与真正的内容拼接起来,并计算拼接后的新内容的 SHA-1 校验和。

使用 irb 命令进入 Ruby 交互式模式:

可见计算出来的校验后与刚开始设定的校验和是一致的。

 

git对象存储

在git设计哲学中我们了解到test1.txt被git add命令添加到git仓库后,存储的路径如下

 

我们通过ruby命令来对数据进行压缩然后存储,看看会发生什么。

Git 用 zlib 对数据内容进行压缩,在 Ruby 中可以用 zlib 库来实现。

最后将用 zlib 压缩后的内容写入磁盘。

git的存储的规则是:sha1值的前2位作为文件夹名,后38位作为文件名

把它存储在.git的objects目录下

这样就完成了一次blob对象的创建。

退出irb后,用git命令验证下创建对象的内容

结果与test1.txt的内容一致。

执行git st命令查看下状态,

发现是untracked

再用git add命令

再看.git/objects目录下,目录没变,还是

 

结论:Git 以对象类型为起始内容构造一个文件头,本例中是一个 blob。然后添加一个空格,接着是数据内容的长度,最后是一个空字节 (null byte),接着用这个文件头和真正的内容拼接起来(不是文件名)计算校验和,然后用zlib对数据进行压缩,按照SHA-1 值的头两个字符作为子目录名称,剩余 38 个字符作为文件名保存压缩后的数据。这样就把一个文件存储到了git仓库中。

git设计哲学

刚开始使用git的时候,总想拿git来和cvs或者svn来作对比,但不久后发现这个想法本身就是错的,git完全就是另外一种物种,一种本属于未来的物种。它的对象存储方式,快照,分支等,都是完全不同的。

相信每个使用git的人,都想了解git内存文件的存储对象,快照,提交在历史和分支等内部的原理。都想知道它是否有传说中的那么强大?

Git 对象

先从本地创建一个空git仓库开始

这时会发现目录下只有一个.git文件夹,进去后长这样

description文件仅供GitWeb使用,不用关心它。
config文件包含了项目特有的配置选项,如最常用的用户名和邮箱。
info目录保存了一份不希望在 .gitignore 文件中管理的忽略模式 (ignored patterns) 的全局可执行文件。这个用得比较少,也不用太关心。
hooks目录保存了客户端或服务端钩子脚本,一般我们都是用默认的,很少改,也不用太关心。

因此,我们需要重点关心另外四个重要的文件或目录:HEAD和index文件,objects和refs目录,因为它们是Git的核心:

objects 目录存储所有数据内容。
refs 目录存储指向数据 (分支) 的提交对象的指针。
HEAD 文件指向当前分支。
index 文件保存了暂存区域信息。

刚初使化的一个 Git 仓库, objects 目录是空的:

Git 初始化了 objects 目录,同时在该目录下创建了 pack 和 info 子目录,但是该目录下没有其他常规文件

接下来,我们新建一个文件test1.txt,内容为test1,并把它加入暂存区

发现.git目录下多了2个文件,且内容都为字节码:

在.git目录下多了一个index文件
在objects下面多了一个a5文件夹,文件夹下有一个文件名为bce3fd2565d8f458555a0c6f42d0504a848bd5的文件

使用如下命令查看test1.txt的hash值

发现结果的前2位是文件夹的名字,后38位是文件的名字。

a5bce3fd2565d8f458555a0c6f42d0504a848bd5这个文件是用zlib压缩的
通过git cat-file 命令可取出文件存储的内容

内容就是test1.txt的文件内容

看看index文件,刚刚了解了index 文件保存了暂存区域信息, 用git ls-files —stage命令可查看index内容

此时index保存的就是新加入暂存区的文件快照

gitt使用’blob ‘ + len(content) + ‘\0’ + content作为文件内容,其sha1值的前2位作为文件夹名,后38位作为文件名
 

Tree(树)对象

接下去来看 tree 对象,tree 对象可以存储文件名,同时也允许存储一组文件。Git 以一种类似 UNIX 文件系统但更简单的方式来存储内容。所有内容以 tree 或 blob 对象存储,其中 tree 对象对应于 UNIX 中的目录,blob 对象则大致对应于 inodes 或文件内容。一个单独的 tree 对象包含一条或多条 tree 记录,每一条记录含有一个指向 blob 或子 tree 对象的 SHA-1 指针,并附有该对象的权限模式 (mode)、类型和文件名信息。

现在把test1.tst提交,作为仓库的第一次提交

发现在objects目录下多了两个文件夹,文件夹下面是对面的sha1文件

分别查看一下内容

其中第一个是commit对象,第二个是tree对象

这个tree对象可以认为是git仓库的根目录,类似unix文件系统中的”/”。

Commit(提交)对象

我们在根目录新建一个文件夹temp,在temp下面添加一个文件test2.txt,文件内容为test2,并提交

发现objects下面又多了4个文件夹,以及文件夹下对面的sha-1文件

通过git log命令来查看提交历史

发现最后一次提交的sha-1文件是d25d2289339c751ff3f7e1ef1865a58c71d0f51c,对应文件是

我们查看d25d2289339c751ff3f7e1ef1865a58c71d0f51c里的内容

发现其中;
tree 35592c587f70cf6ec1b99bb382bec2ef92f83396 是本次提交指向的tree对象(即根目录)对应文件是

parent a3951d57b1413275b171d967fa67fd90eecff648是第二次提交指向第一次提交对象的指针
author和committer分别是作者是提交者

再来看 35592c587f70cf6ec1b99bb382bec2ef92f83396文件的内容

 

这个tree对象也指向一个tree对象和一个blod对象,分别代表temp目录和test1.txt文件的sha1文件
这一行 040000 tree 9e7b8054ac3ca530d8e69556dff5903cdcbdc4d3    temp 的对应文件是

 

再继续看tree对象9e7b8054ac3ca530d8e69556dff5903cdcbdc4d3里面的内容

 

文件内容中只有一个指向blob对象的指针(即test1.txt对象的sha1文件),对应的文件为

至此,新增加的4个文件的关系已找到。

两次提交的关系如图:

这就是git的对象系统,本质上是一个key-value的内容寻址文件系统。

当用git add命令把一个文件或者目录添加到git跟踪的时候,会生成一个blob对象或者tree对象
当用git commit 来提交的时候,会创建一个commit对象,commit包含了提交者信息,还包含一个指向仓库根目录的tree对象。
然后tree对象再包含指向文件的blob对象或者子目录tree对象