# 对象
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 add
和 git commit
命令时,Git 所做的实质工作——将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。
这三种主要的 Git 对象——数据对象、树对象、提交对象——最初均以单独文件的形式保存在 .git/objects
目录中。