也许你早已经熟悉了Git的日常使用,但是你可曾想过:为什么每次新建Git库时都要执行git init呢?执行git init后生成的.git目录里到底藏了哪些秘密?平常使用Git客户端,以及命令行执行git命令时,Git在背后到底为我们默默地做了些什么呢?阅读本文以后,一切谜团都将引刃而解!

注: 本文的大部分写作灵感来自于“Pro Git book”。感谢原作者的精彩分享。 本文采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。知识共享许可协议

.git目录

众所周知,每次我们在本地新建一个Git库时,都要执行git init命令。Git会在新建库的根目录下为我们自动创建一个.git目录,把所有它要用到的信息都保存在这个神奇的目录里。我们日常对Git的所有操作,本质上讲,就是在对这个目录进行维护。也正是因为这个原因,如果我们想备份Git库的话,实际上只要复制这个.git目录就可以了。

为了方便后面做实验,让我们先在本地新建一个Git库,并观察.git目录下包含的内容。本文推荐大家利用Hello Git提供的两个Docker镜像作为实验环境:一个代表远程Git服务(lab-git-remote),另一个代表本地Git客户端(lab-git-local)。这两个镜像都可以从Docker Hub上找到:

docker pull morningspace/lab-git-remote
docker pull morningspace/lab-git-local

有关这两个Docker镜像的具体使用方法,请见Hello Git项目的README。本文后续讨论的所有动手环节,都将围绕这两个Docker镜像展开。现在,我们就来创建本地Git库:inside-git。

$ git init inside-git
Initialized empty Git repository in /root/inside-git/.git/
$ cd inside-git
$ ls -l .git
total 32
-rw-r--r-- 1 root root   23 Apr 30 00:46 HEAD
drwxr-xr-x 2 root root 4096 Apr 30 00:46 branches
-rw-r--r-- 1 root root   92 Apr 30 00:46 config
-rw-r--r-- 1 root root   73 Apr 30 00:46 description
drwxr-xr-x 2 root root 4096 Apr 30 00:46 hooks
drwxr-xr-x 2 root root 4096 Apr 30 00:46 info
drwxr-xr-x 4 root root 4096 Apr 30 00:46 objects
drwxr-xr-x 4 root root 4096 Apr 30 00:46 refs

因为我们还没有往Git库里添加任何内容,所以.git基本上只是一个空的目录结构。这其中,我们将重点关注两个目录和一个文件,分别是:objects目录,refs目录,以及HEAD文件。那么,它们到底有什么用途呢?别急,我们接着往下看。

认识Git对象

在Git的世界里,Git把它所管理的一切内容都当作了一个个的Git对象(Git object),这些对象就被保存在.git目录下。Git在保存这些对象时,还会为它们生成一个基于SHA-1算法的全局唯一的hash值,以便后面利用这个hash值对Git对象进行存取和引用。

所以,Git的核心,其实就是一个基于文件系统的,存储“键值对”的数据库。或者,更加“正式”一点的说法是,Git是一个在内容上可寻址的文件系统(Content-Addressable Filesystem)。

基于这一认识,我们再回过头来看.git目录下的这两个目录和一个文件:

  • objects: 顾名思义,这个目录里存的都是Git对象。它以Git对象的形式保存了该Git库所包含和管理的全部内容;
  • refs: 保存了指向Git对象的引用。准确地说,这个引用所指向的,是与分支,标签,远程库等相关联的commit对象。Git把它所管理的对象分成了几种类型,commit对象就是其中之一。关于commit对象,引用,以及refs目录,后面我们还会详细介绍;
  • HEAD: 这个很容易理解,它保存了当前分支的head指针,实际上就是指向当前分支最新提交(也是一个commit对象)的引用。

Blob对象

有了前面的铺垫,从这一节开始,我们将真正开启探索Git底层机制的神奇之旅啦!

我们要做的第一件事情,是手工创建一个Git对象。Git对象的创建,在我们日常使用Git时,都是由Git在背后悄悄完成的。通过手工创建,很快我们就会揭开它的神秘面纱,看一看Git到底是如何跟踪和管理内容的。

说是手工创建,其实也不完全是。这里,我们依然要借助于Git的一个特殊命令:git hash-object。利用它,我们可以把内容存入.git/objects目录,并返回指向该内容的唯一键(即hash值)。这个命令,我们在日常使用Git时可能从来都没有用到过,它属于Git的底层命令。我们平时执行的那些git命令,背后其实调用的都是像git hash-object那样更为底层的命令。在后面的内容里,我们将大量使用这些底层命令,来理解Git背后隐藏的秘密。

OK,现在就让我们利用git hash-object,手工把一个新建的文件存入Git库:

$ echo 'Inside Git' > README
$ git hash-object -w README
968b2bf72e28d8c6756054730880cf9f9ab06062

这里,参数-w是为了告诉Git,在返回唯一键的同时,把内容存入Git库。关于存入Git库中的Git对象的格式,后面我们还会详细解释,这里先略过不表。git hash-object的返回结果,即Git对象的唯一键,是一个长度为40个字符的基于SHA-1算法的hash值。

此时,我们会发现在.git目录的objects子目录下多了一个新文件。文件名和上级目录名称的组合刚好构成了git hash-object返回的唯一键:

$ find .git/objects -type f
.git/objects/96/8b2bf72e28d8c6756054730880cf9f9ab06062

利用另一个Git的底层命令git cat-file,我们还可以还原刚才所保存的内容正文:

$ git cat-file -p 968b2bf72e28d8c6756054730880cf9f9ab06062
Inside Git

接下来,我们再对README的内容做些修改,然后存成一个新的Git对象:

$ echo 'Welcome to Inside Git' > README
$ git hash-object -w README
4f4fc3399cef946fc77e12211808d0590715793d

现在再来看一下我们的.git目录:

$ find .git/objects -type f
.git/objects/96/8b2bf72e28d8c6756054730880cf9f9ab06062
.git/objects/4f/4fc3399cef946fc77e12211808d0590715793d

这个时候.git目录下已经有两个Git对象了,它们分别对应于README文件的两个不同版本。如果在执行git cat-file命令时使用-t参数,我们还能看到Git对象的所属类型:

$ git cat-file -t 968b2bf72e28d8c6756054730880cf9f9ab06062
blob
$ git cat-file -t 4f4fc3399cef946fc77e12211808d0590715793d
blob

可以看到,与文件相对应的Git对象都是blob类型的。很快我们就会看到,除了blob对象,Git还支持很多其他类型的对象。

Git对象的存储

前面提到过,存入Git库中的Git对象是有着特定格式的。具体来说,它包含了一个header字段,后跟一个’\0’字符,然后才是内容正文。整个Git对象在存入Git库时,还会经过一次zlib的deflate压缩处理:

<header字段>\0<内容正文>

其中,header字段的格式为:

<对象类型> <内容正文长度>

这里的对象类型指的就是Git对象的类型,比如:blob,tree,commit,tag等。事实上,对header字段采用SHA-1算法计算得到的结果,就是Git对象的唯一键。它是一个长度为40个字符的字符串。

为了验证这一点,下面我们将再次手工创建一个Git对象。和之前不同的是,这里我们不再使用git提供的现成命令git hash-object了,而是采用第三方命令行工具:sha1sum和pigz。

注意:如果你使用的是Hello Git提供的Docker镜像,那么这些命令行工具都已经预装了。否则需要自行安装,比如在Ubuntu上安装pigz,可以执行如下命令:

$ apt-get update
$ apt-get install pigz

假设我们的内容正文是“What is inside Git?”,字符串长度为19,对应的Git对象类型为blob。按照上面的格式把header字段和内容正文用’\0’字符隔开,然后利用sha1sum计算SHA-1的hash值如下:

$ echo -ne 'blob 19\0What is inside Git?' | sha1sum | awk '{print $1}'
5fe76ad3e039c1fdc74201715f8c150bed50e351

我们在执行echo时,加上了参数-n-e。前者是为了避免echo在输出字符串时自动添加换行符;后者是为了让echo能够识别反斜杠转义符(即字符串中的’\0’)。

然后,我们再用git hash-object来验证一下,这个用sha1sum生成的hash值是否是正确的:

$ echo -n 'What is inside Git?' | git hash-object --stdin
5fe76ad3e039c1fdc74201715f8c150bed50e351

返回的结果表明,用sha1sum生成的结果和git hash-object是完全一致的。这里,使用参数--stdin是为了告诉git,从标准输入中读取内容。

接下来,我们再来看一下Git对象的存储。按照规则,我们把前面生成的唯一键拆成两个部分:前两位作为目录名,在.git/objects下新建一个子目录;后38位则作为文件名,在该子目录下新建一个文件。同时,在把Git对象写入该文件时调用pigz,对包含header值在内的完整内容进行压缩处理:

$ mkdir .git/objects/5f
$ echo -ne 'blob 19\0What is inside Git?' | pigz -cz > .git/objects/5f/e76ad3e039c1fdc74201715f8c150bed50e351

我们为pigz指定了两个参数:参数-c表示将所有信息输出到标准输出设备。当然,因为我们使用了重定向符,所以标准输出设备的输出最终被存成了文件。另一个参数-z,则指示pigz在进行压缩处理时采用zlib的deflate算法。默认情况下pigz使用的是gzip算法,两者在格式上有所不同。

如果此时我们执行git cat-file查看该Git对象的内容:

$ git cat-file -p 5fe76ad3e039c1fdc74201715f8c150bed50e351
What is inside Git?

就会发现,返回的结果和我们用pigz进行压缩处理之前的内容正文是一摸一样的。这和我们用pigz的-d参数对Git对象进行解压得到的结果也完全一致:

$ pigz -d < .git/objects/5f/e76ad3e039c1fdc74201715f8c150bed50e351
blob 19What is inside Git

这里的返回结果包含了header字段和内容正文,中间的’\0’由于是不可见字符,所以没有显示出来。

好了,今天就先到这里啦。在下篇文章里,我们将讨论Git中的另外两种重要的对象,它们分别是:Tree和Commit!

留下评论

您的电子邮箱地址并不会被展示。请填写标记为必须的字段。 *

正在加载...