Git之旅 - 四个对象的恩怨纠葛

在上一篇文章中(Git之旅 - 代码仓库与数据库)进行了一次完整的提交,并抛出2个问题:

1.每次提交是不是至少会多出来3个文件?
2.这几个文件的类型与关系是怎样的?

本文会给出答案。

1. 关于对象

日期:某年某月某日 / 地点:家里

我对老婆说:“你知道吗,亲爱的。我每天都面向对象编程。”
这时候,我老婆拿来搓衣板并扔到地上道:“说吧,面向的哪个对象?”
然后,我就对他说:“不,不是你想的那样。”
“那是那样?”

于是,我就战战兢兢的开始科普。从创建一个类一直讲到实例化类之后生成一个对象。
当然,今天讲的对象既不是编程语言的对象,也不是我老婆眼中的那个对象。是Git中至关重要的四个结构体对象。

2. Git的对象

talk is cheap,show me the object,从图中可以看出来:

1. blob、tree、commit、tag中都包含一个object结构体
2. object结构体包含一个object_id结构体
3. object_id包含一个字符串,存储根据sha256生成的字符串标识

struct commit在执行git commit命令执行之后生成(包含提交信息、目录树等信息),其中包含整个项目根目录tree,该tree又包含根目录下面每一个文件夹(对应一个tree),每一个文件(对应一个blob)。

其中,每一个blob、tree、commit、tag都会有一个唯一的标识符字符串,用来标识其对象。

2.1 简述Git的四个对象

  • Blob
    用来存放项目文件的内容,但是不包括文件的路径、名字、格式等其它描述信息。项目的任意文件的任意版本都是以 Blob 的形式存放的。
  • Tree
    Tree用来表示目录。我们知道项目就是一个目录,目录中有文件、有子目录。因此Tree中有Blob、子Tree,且都是使用 SHA256值引用的,这是与目录对应的。从顶层的Tree纵览整个树状的结构,叶子结点就是Blob,表示文件的内容,非叶子结点表示项目的子目录,顶层的 Tree 对象就代表了当前项目的快照。
  • Commit
    表示一次提交,有Parent字段,用来引用父提交。指向了一个顶层Tree,表示了项目的快照,还有一些其它的信息,比如上一个提交Committer、Author、Message等信息。
  • tag
    不会移动的引用(指向一个commit对象)。

3. 一次代码提交论证

  1. 初始化git仓库test1

  2. 添加若干文件与目录

  3. 查看 .git目录下 object中生成的文件的类型

使用git cat-file -t “sha256 string”,分别查看每个文件的类型 当前提交共生成5个新的文件。

看到文件的类型为不包含tag的3种类型。根据上面4个对象的作用,可推测出:

I. 一次提交对应一个commit对象
II. 根目录与dir1两个目录对应两个tree对象
III. 1.txt与2.txt两个文件对应两个blob对象

查看每个对象具体的内容使用:git cat-file -p “sha256 string”

到这里,感觉豁然开朗,与上面的推测完全相符合。

4. 详说Git的四个对象

Blob对象、tree对象、commit对象、tag对象

4.1 Blob对象

Git 是一个内容寻址文件系统。意味着:Git 的核心部分是一个简单的键值对数据库(key-value data store),可以向该数据库插入任意类型的内容,它会返回一个键值,通过该键值可以在任意时刻再次检索(retrieve)该内容。可以通过底层命令 hash-object 将任意数据保存于 .git 目录,并返回相应的键值。

1
2
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

-w 选项指示 hash-object 命令存储数据对象;若不指定此选项,则该命令仅返回对应的键值。
--stdin 选项则指示该命令从标准输入读取内容; 若不指定此选项, 则须在命令尾部给出待存储文件的路径. 该命令输出一个长度为 40 个字符的校验和. 这是一个 SHA-1 哈希值:一个将待存储的数据外加一个头部信息(header)一起做 SHA 校验运算而得的校验和。

objects 目录下看到一些文件。这就是开始时 Git 存储内容的方式:一个文件对应一条内容,以该内容加上特定头部信息一起的SHA校验和为文件命名。校验和的前两个字符用于命名子目录,余下的38个字符则用作文件名 (Git 使用 zlib 压缩这些文件的内容)。

可以通过 cat-file 命令从Git那里取回数据。这个命令简直就是一把剖析 Git 对象的瑞士军刀。为 cat-file 指定 -p 选项可指示该命令自动判断内容的类型,并为我们显示格式友好的内容:

1
2
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

现在对一个文件进行简单的版本控制:

1
2
3
4
5
6
7
$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

数据库记录下了该文件的两个不同版本,当然之前我们存入的第一条内容也还在:

1
$ find .git/objects -type f

现在把文件内容恢复到第一个版本:

1
2
3
$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1

或者第二个版本:

1
2
3
$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2

文件的每一个版本(快照)所对应的 SHA值并不一样;另一个问题是,在这个(简单的版本控制)系统中,文件名并没有被保存——我们仅保存了文件的内容。上述类型的对象我们称之为数据对象(blob object)。利用 cat-file -t 命令,可以让 Git 告诉我们其内部存储的任何对象类型,只要给定该对象的 SHA值:

1
2
$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

4.2 tree对象

现在要探讨的是树对象(tree object)。它能解决文件名保存的问题,也允许我们将多个文件组织到一起。Git以一种类似于UNIX文件系统的方式存储内容。所有内容均以树对象和数据对象的形式存储,其中树对象对应了UNIX中的目录项,数据对象则大致上对应了inodes或文件内容。一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的SHA指针,以及相应的模式、类型、文件名信息。例如,某项目当前对应的最新树对象可能是这样的:

1
2
3
4
$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README
100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib

master^{tree} 语法表示 master 分支上最新的提交所指向的树对象。请注意,lib 子目录(所对应的那条树对象记录)并不是一个数据对象,而是一个指针,其指向的是另一个树对象:

1
2
$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b simplegit.rb

4.3 commit对象

有了树对象,即:有了想要跟踪的不同项目快照。然而问题依旧:若想用这些快照,必须记住所有 SHA哈希值。并且,你也完全不知道是谁保存了这些快照,在什么时刻保存的,以及为什么保存这些快照。而以上这些,正是提交对象(commit object)能为你保存的基本信息。

创建一个提交对象,为此需要指定一个树对象的 SHA值,以及该提交的父提交对象。

每次我们运行 git add 和 git commit 命令时,Git 所做的实质工作:将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。

这三种主要的 Git 对象——数据对象、树对象、提交对象:最初均以单独文件的形式保存在 .git/objects 目录下。

4.4 tag对象

一句话tag对象概括:不会移动的引用。
用一幅图说明4个对象的关系:

用另一张图来强化你的理解:

我们通过HEAD找到一个当前分支,然后通过当前分支的引用找到最新的commit。
然后,通过commit可以找到整个对象关系模型。

5. 多次提交的关系

如果进行多次提交,仓库的提交历史会像这样 (第一个没parent,所以没有指向. 使用链表实现)。
还记得commit的结构体中那个叫 parent的字段吗(对,就是它)。
到这里,关于四个对象的关系已经基本说明完毕,具体的可以自己按照文章中实例进行操作之后,查看.git/object中各个文件的变化。