基于 Git 的使用,已经在前文有过相关的介绍,使用 Git 用作日常的开发基本上是足够的。现在,本文将详细介绍一些有关 Git 的实现原理。
一般情况下,正常使用的 Git 命令,如 git add
、git checkout
等都是由 Git 封装好的上层命令,这对于一般的用户来讲是友好的。但是,有时候如果想要在底层执行一些必要的操作,这时就需要使用底层命令了。
早期的 Git 是 Linus 为了管理 Linux 内核的版本而设计的,当时的 Git 设计的更加符合 Unix 的风格,了解 Git 的底层命令对于学习 Git 来讲也是至关重要的。
.git
目录文件结构 在使用 git init
命令初始化一个空的 Git 仓库时,会在当前目录下创建一个 .git
文件夹,里面的内容看起来可能跟下面的有些相似。
文件说明:
hooks
:钩子脚本文件的存放目录,钩子类似于一个代理,在执行提交操作之前执行对应的一些脚本,这个一般在自己搭建 Git 服务器的时候使用。info
:包含一个全局排除(global exclude)文件,用于放置那些在 .gitignore
文件中配置的忽略跟踪文件objects
:存储所有的数据内容refs
:存储指向数据的提交对象(分支、标签、远程仓库等)的指针config
:包含一些特有的项目配置选项description
:可以忽略的文件head
:指向当前检出的分支 Git 更加像是一个键值对数据库,在向 Git 中写入内容时,都会得到唯一的一个键,然后通过这个键就可以再得到这个对象。可以通过 git hash-object
进行这个操作
# 将 "Hello World!" 字符串对象使用 Git 生成对应的键,--stdin 表示从标准输入流中获取对象,如果不指定 --stdin 选项,那么就需要在命令行尾部添加对应的存储文件路径
echo "Hello World!" | git hash-object --stdin
你可能会看到类似下面的输出:
980a0d5f19a64b4b30a87d4206aade58726b60e3
此时只是简单地执行一次 hash
操作,并没有将这个对象放入 Git 中进行存储,如果想要存储到 Git,需要添加-w
选项
# 将 "Hello World!" 字符串对象生成对应的键,并且存储到 Git 中
echo "Hello World!" | git hash-object -w --stdin
对 objects
进行查找,可以看到对应的对象已经被存储到 Git 中了。
现在进入 .git/objects
目录,你会看到一个 名为 98
(对象 hash
值的前两个字符)目录,进入这个目录,你可以看到对应的文件,即上图中以 “0a0d”(对象 hash
值的后面部分) 开头的文件,该文件存储的是之前保存的 “Hello World!” 字符串对象的二进制对象。
可以使用 git cat-file
来查看这个二进制对象(blob)文件的内容。
# 使用 git cat-file 来查看对应的二进制对象的内容,使用 -p 选项表示漂亮地打印对象的内容
git cat-file -p 980a0d5f19a64b4b30a87d4206aade58726b60e3
输出的内容:
Hello World!
现在,对于 Git 对于对象的存储的过程已经有了一个基本的了解,接下来看看对于修改的文件对象的存储。
首先,创建一个文件并且写入一些内容,按照上面的方法将文件存储到 Git 中:
# 创建一个文件,输入一些内容
echo "test.txt version-1" > test.txt
# 生成 test.txt 的 hash 键并且存储到 Git 中
git hash-object -w test.txt
# 这里输出生成的对象的键
335d079908a9ed113c12509b3e41b2d35f0610fd
然后,修改 test.txt
文件中的内容,然后再重新添加到 Git 中
# 修改 test.txt 文件中的内容
echo "test.txt version-2" > test.txt
# 再次生成 test.txt 的 hash 键并且存储到 Git 中
git hash-object -w test.txt
# 这里输出生成的对象的键
e3d5c7939df71039542c56017d0258d11ea4051d
现在,查看 objects
目录下的文件内容:
现在,创建的 test.txt
的两个不同版本的文件就都存储在 objects
的目录中了。
此时 Git 的状态就类似于使用 git commit
命令将文件存储到本地数据库了。即使此时 test.txt
文件被删除了,也能通过对应的 SHA1
哈希值找回该对象,现在,删除 test.txt
文件对象。
rm -i test.txt
然后通过 git cat-file
命令来找回该对象,这里以找回第一个版本的 test.txt
文件对象为例。
# 使用 `git cat-file` 从对象树中取回 335d0799 文件对象,将得到的输入重定向到 test.txt 文件中
git cat-file -p 335d079908a9ed113c12509b3e41b2d35f0610fd > test.txt
# 查看得到的 test.txt 文件对象
cat test.txt
可以看到,test.txt
文件对象的第一个版本已经被恢复了。
这就是 Git 存储文件对象的基本原理,通过生成对应的 SHA1
哈希值,将对应的文件放入由该哈希值组成的 目录 + 文件名的结构中,即可完成对文件对象的存储;通过对不同的文件生成相对应的哈希值,即可完成对文件版本的控制!
然而,如果在现实生活中记住这些哈希值时不可能的,因此,Git 引入了树对象进行进一步的管理
Git 通过一种类似于 Unix
文件系统的方式对存储相关的内容,所有的内容都以二进制数据对象和树对象的形式进行存储。其中,树对象对应 Unix
文件系统中的目录项,二进制数据对象则大致对应了 inodes
或者文件内容。一个树对象包含了一条或多条树对象记录,每条记录都包含着一个指向数据对象或者子树对象的 SHA1
指针,以及相应的模式、类型、文件名信息。
# 查看 master 分支下面的最新提交指向的树对象,# 注意,使用之前的 `git hash-object -w` 写入对象时不会初始化默认的分支 master,这里的 master 分支是另一个 Git 存储库的分支。git cat-file -p master^{tree}
可以看到,这里的 tree
表示的就是一个树对象,使用 git cat-file
查看该树对象
git cat-file -p d9bf1fc487b0165948a2bf981804a3090d8f82b3
可能会看到类似的输出:
100644 blob 20d36530d4b131c26649c0291a0a5f912cd266ea file
此时,该仓库内部存储的数据组成如下图所示:
通常情况下,Git 根据某一时刻暂存区所表示的状态创建并记录一个对应的树对象,如此便可以依次记录某一时间段内的一系列树对象。
如果想要创建一个树对象,首先需要通过暂存一些文件来创建一个暂存区。通过使用 git update-index
可以为一个单独的文件创建一个暂存区。
# 为 text.txt 创建一个暂存区,这里 335d079908a9ed113c12509b3e41b2d35f0610fd(参见上文) 代表的是 test.txt 文件的第一个版本
# --add 是一个必须的选项,因为在此操作之前该文件并不存在于暂存区中
# --cacheinfo 表示的是加载不在当前工作目录下的文件,可以回忆一下使用 `git reset --hard` 将 Git 中的文件加载到工作区。在这里,是将 Git 仓库中的 test.txt 文件,SHA 为 335…… 的普通文本文件加载到工作区。使用 --cacheinfo 选项时需要指定这三个参数,具体形式为 --cacheinfo