# 对象

Git 是一个开源的分布式版本控制系统,可用于敏捷高效地处理任何或小或大的项目。

# 基础

当在一个新目录或已有目录执行 git init 时,Git 会创建一个 .git 目录。 这个目录包含了几乎所有 Git 存储和操作的对象。 如若想备份或复制一个版本库,只需把这个目录拷贝至另一处即可。

$ ls .git/
HEAD # -------------------> 指示目前被检出的分支
config* # ----------------> 包含项目特有的配置选项
description # ------------> 仅供 GitWeb 程序使用,一般我们无需关心
hooks/ # -----------------> 包含客户端或服务端的钩子脚本
info/ # ------------------> 包含一个全局性排除(global exclude)文件,用以放置那些不希望被记录在 .gitignore 文件中的忽略模式(ignored patterns)
objects/ # ---------------> 存储所有数据内容
refs/ # ------------------> 存储指向数据(分支)的提交对象的指针

该目录下可能还会包含其他文件(比如 index 文件:保存暂存区信息),不过对于一个初始化的版本库,这是的默认结构。

Git 是一个内容寻址文件系统,也就是说它的核心部分是一个简单的键值对数据库(key-value data store)。当我们向该数据库插入任意类型的内容时,它会返回一个键值,通过该键值可以在任意时刻再次检索(retrieve)该内容。

现在,先来了解一下底层命令 hash-object,它会计算出 commitID(一个将待存储的数据外加一个头部信息(header)一起做 SHA-1 校验运算而得的校验和)并根据需要从文件创建 blob 对象。

# -w 选项指明需要存储数据对象,--stdin 选项则表示从标准输入读取内容
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

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

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

通过 cat-file 命令从 Git 那里取回数据。

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

接下来我们同样可以将这些操作应用于文件,创建一个新文件并将其内容存入数据库。

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

接着,向文件里写入新内容,并再次将其存入数据库。

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

# 树对象

目前位置,数据库中已经有了三条记录,不过我们查看时需要记住很长的 SHA-1 值,而且每条记录仅仅记录了文本内容。这很麻烦,好在树对象为我们解决了文件名保存的问题,并且让我们可以将多个文件组织到一起。

在 Git 中所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容。

一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。

# 查看 master 分支上最新的提交所指向的树对象
$ git cat-file -p master^{tree}
# 模式  类型               SHA-1 值                     文件名信息
100644 blob 13847f68514564c4882d515fff065e177618bd7a    a.txt

通常,Git 根据某一时刻暂存区所表示的状态创建并记录一个对应的树对象,如此重复便可依次记录(某个时间段内)一系列的树对象。

因此,为创建一个树对象,首先需要通过暂存一些文件来创建一个暂存区。现在就来把 test.txt 文件的首个版本人为地加入一个新的暂存区。

# 此前该文件并不在暂存区中需要加上 --add 选项,并指定 --cacheinfo 选项,因为将要添加的文件已经位于 Git 数据库中,而不是位于当前目录
# 同时,需要指定文件模式、SHA-1 值与文件名
$ git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt

然后,可以通过 write-tree 命令将暂存区内容写入一个树对象。

# 如果某个树对象此前并不存在的话,当调用 write-tree 命令时,它会根据当前暂存区状态自动创建一个新的树对象
$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579

接着我们来创建一个新的树对象,它包括 test.txt 文件的第二个版本,以及一个新的文件。

$ echo 'new file' > new.txt
$ git update-index --cacheinfo 100644 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt
$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341

通过调用 read-tree 命令,可以把树对象读入暂存区。

# --prefix 选项,将一个已有的树对象作为子树读入暂存区
$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614

如果基于这个新的树对象创建一个工作目录,你会发现工作目录的根目录包含两个文件以及一个名为 bak 的子目录,该子目录包含 test.txt 文件的第一个版本。

# 提交对象

现在有三个树对象,分别代表了我们想要跟踪的不同项目快照。但是想要重用这些快照的话,你必须记住所有三个 SHA-1 哈希值。 并且,你也完全不知道是谁保存了这些快照,在什么时刻保存的,以及为什么保存这些快照。而以上这些,正是提交对象能为你保存的基本信息。

通过调用 commit-tree 命令创建一个提交对象,为此需要指定一个树对象的 SHA-1 值,以及该提交的父提交对象(如果有的话)。以之前创建的第一个树对象为例。

$ echo 'first commit' | git commit-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
b3bafe1f77c27fa37cc51cc84a16e191da9c2d3b

接下来为另外两个树对象创建提交对象,这两个提交对象分别引用各自的上一个提交(作为其父提交对象)。

# 使用 -p 选项指定父提交对象
$ echo 'second commit' | git commit-tree 0155eb4229851634a0f03eb265b69f5a2d56f341 -p b3bafe1f77c27fa37cc51cc84a16e191da9c2d3b
97bf78eb45434f4aad7821f3c056885fe4d197d8
$ echo 'third commit' | git commit-tree 3c4e9cd789d88d8d89c1073707c3585e41b0e614 -p 97bf78eb45434f4aad7821f3c056885fe4d197d8
a495e2cc498bfcf1f676fff2a905ec26b09f7015

# 对象存储

前面提到的,在存储内容时,会有个头部信息一并被保存。那么究竟是怎么做的呢?

Git 以对象类型作为开头来构造一个头部信息,它可能是 “commit”、“tree” 和 “blob” 其中一个字符串。接着 Git 会添加一个空格,随后是数据内容的长度,最后是一个空字节(null byte)。

再接着将上述头部信息和原始数据拼接起来,并计算出这条新内容的 SHA-1 校验和。

Git 会通过 zlib 压缩这条新内容,最后将这条经由 zlib 压缩的内容写入磁盘上路径以 SHA-1 值的前两个字符作为子目录名称,后 38 个字符则作为子目录内文件的名称的对象里。

所有的 Git 对象均以这种方式存储,虽然数据对象的内容几乎可以是任何东西,但提交对象和树对象的内容却有各自固定的格式。

# 总结

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

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