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

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

Tree对象

这一节,让我们一起来认识一下tree对象。它是Git对象中很重要的一种类型。blob对象只能保存某个文件的内容本身以及它的唯一键,它并不会保存文件名。而tree对象,不仅可以保存文件名,还可以保存多个文件的内容及其唯一键,而且它还允许嵌套子树(subtree),即:让一个tree对象包含另一个tree对象。所以,如果说blob对象和文件相对应,那么tree对象就是和目录相对应的。

下面我们就来手工建一棵树。通常,Git首先会把要保存的内容放入暂存区(或者按照Git的术语,被称为index),然后以一组tree对象的形式将暂存区的内容写入Git数据库。因此,我们首先要更改暂存区的状态(或者称为更新index)。如果这个时候我们用git status查看一下当前的本地Git库,会看到暂存区现在还是空的,这是因为我们还没有把需要提交的变更放入暂存区。

$ git status
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	README

nothing added to commit but untracked files present (use "git add" to track)

如果想更新暂存区(即更新index),比如:把第一版README放入暂存区,我们需要借助git update-index

$ git update-index --add --cacheinfo 100644 968b2bf72e28d8c6756054730880cf9f9ab06062 README

参数--add表示我们要往暂存区里新加文件;参数--cacheinfo则表示,我们要加的文件并不在当前目录下,而是存在于Git的数据库里。的确,我们加入暂存区的是README的第一个版本,而当前目录下保存的则是README的第二个版本。

100644对应的是一个mode值,代表普通文件。在Git里,几个常用的mode值包括:

  • 100644 - 普通文件;
  • 100755 - 可执行文件;
  • 120000 - 符号链接(symbolic link);
  • 040000 - 目录;

随后是文件对应的唯一键,以及文件名。

这个时候如果我们再执行一次git status,就会看到暂存区里有内容了:

$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

	new file:   README

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   README

其中,“Changes to be committed”下的README就是我们存入暂存区,且等待提交的第一版README;“Changes not staged for commit”下的README,则是当前目录下的README,也就是它的第二版。

接下来,我们就可以利用git write-tree把存入暂存区的内容以一个tree对象的形式写入Git数据库了:

$ git write-tree
4bfa25dce3532386b8924ca569efbada55685794

我们再用git cat-file,分别结合-p-t参数,验证一下刚刚存入的tree对象:

$ git cat-file -p 4bfa25dce3532386b8924ca569efbada55685794
100644 blob 968b2bf72e28d8c6756054730880cf9f9ab06062	README
$ git cat-file -t 4bfa25dce3532386b8924ca569efbada55685794
tree

从返回的结果可以看到,唯一键4bfa25d打头的Git对象,的确是tree类型的,并且它包含了一个文件名为README的blob类型的Git对象,其唯一键为968b2bf打头,且是一个普通类型的文件,恰好对应README的第一版。这说明,我们的tree对象已经创建成功了。

紧接着我们再来建一棵树。这次,我们打算新建一个文件放入暂存区,同时还要将README的第二版也加入暂存区:

$ echo '1.0' > VERSION
$ git update-index --add VERSION
$ git update-index --add README
$ git write-tree
955f6fef4f43ee1f5d93cbea718cce3048450f4b

这个时候,我们再用git cat-file查看一下新建的tree对象:

$ git cat-file -p 955f6fef4f43ee1f5d93cbea718cce3048450f4b
100644 blob 4f4fc3399cef946fc77e12211808d0590715793d	README
100644 blob d3827e75a5cadb9fe4a27e1cb9b6d192e7323120	VERSION

通过对比唯一键可以发现,这里所指向的正是第二版README对应的Git对象。并且,我们也注意到了,一个tree对象可以保存不只一个Git对象。

不仅如此,前面提到过,tree对象是可以包含子树的。比如:我们还可以把之前创建的第一颗树作为第二颗树的子树。利用git read-tree把第一棵树整个读入暂存区,然后再写入Git的数据库:

$ git read-tree --prefix=bak 4bfa25dce3532386b8924ca569efbada55685794
$ git write-tree
07ef9d54dd0da246d069dfa2ad2350751203ecb2

这里的--prefix参数,相当于在当前目录下“新建”了一个名为bak的子目录。当然,我们并没有真的在当前目录下创建这个子目录,它只存在于Git的数据库里。所以,这个时候如果我们执行git status,就会发现bak/README会同时出现在“Changes to be committed”和“Changes not staged for commit”下面。前者说明,bak/README以经被暂存,而后者则表明当前目录下和暂存区相比,bak/README是缺失的:

$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

	new file:   README
	new file:   VERSION
	new file:   bak/README

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	deleted:    bak/README

通过执行git checkout命令,我们可以把暂存区的内容恢复到本地(即所谓的工作区)。那样就可以在当前目录下看到这个bak子目录以及它下面的README文件了,而且是README文件的第一版:

$ git checkout -- bak/README
$ ls
README  VERSION  bak
$ cat bak/README 
Inside Git

最后,我们再次利用git cat-file,查看一下新建tree对象的详情:

$ git cat-file -p 07ef9d54dd0da246d069dfa2ad2350751203ecb2
100644 blob 4f4fc3399cef946fc77e12211808d0590715793d	README
100644 blob d3827e75a5cadb9fe4a27e1cb9b6d192e7323120	VERSION
040000 tree 4bfa25dce3532386b8924ca569efbada55685794	bak

最后一行表明,我们的bak子目录(即唯一键为4bfa25d打头的tree对象)的确已经被作为子树加入到当前的tree对象里了。如下图所示:

Commit对象

利用tree对象,我们实际上为本地Git库保存了一份反映当前更改的快照(snapshot),因为它包含了本次要提交的全部更改。但问题在于,我们必须记住这些快照的唯一键,才能利用键值得到快照所对应的变更。并且,除了具体变更内容外,我们并不知道是谁,在什么时候,保存了这份快照,以及为什么要保存(相当于git commit时提供的备注信息)。这些信息都可以包含在一个commit类型的Git对象里。下面我们就来看一下,如何手工创建一个commit对象,来模拟git commit的过程。

创建commit对象,需要用到git commit-tree,并通过唯一键指定我们需要提交的tree对象,以及对应的备注信息:

$ echo 'first commit' | git commit-tree 4bfa25
4c50701f89265f9ca6eeb3ddffae450da55f9bd5

注意,我们在指定tree对象的时候并没有使用完整的唯一键,而是截取了前面几位。这在命令行下执行git命令时是很常见的,只要它能唯一标识对应的Git对象就行。

上述命令的执行,将会为我们建立一个commit对象。其返回的结果,就是指向该commit对象的唯一键。利用返回的唯一键,我们用git cat-file查看一下这个commit对象的详情:

$ git cat-file -p 4c50701f89265f9ca6eeb3ddffae450da55f9bd5
tree 4bfa25dce3532386b8924ca569efbada55685794
author dev <dev@example.com> 1556718994 +0000
committer dev <dev@example.com> 1556718994 +0000

first commit

可以看到,返回结果包含了我们要提交的tree对象的唯一键,作者和提交者的个人信息,以及提交时所提供的备注信息。这就相当于我们执行了一次git commit

下面,我们再多建几个commit对象:

$ echo 'second commit' | git commit-tree 955f6f -p 4c50701
04081ddb43269238a1cb8a61a2d04a36986febfa
$ echo 'third commit'  | git commit-tree 07ef9d -p 04081dd
e0ec828eda6b51b170fff6b5fdfa03a3cb70a13e

这个时候,如果我们执行一下git log就会发现,不经意间我们已经建立起了完整的提交历史。而这一提交历史,全部都是利用git底层命令完成的。效果和执行git commit是一摸一样的!在执行git log时,加上与“third commit”对应的唯一键,我们可以查看到从该提交开始往前的所有提交历史:

$ git log e0ec82
commit e0ec828eda6b51b170fff6b5fdfa03a3cb70a13e
Author: dev <dev@example.com>
Date:   Wed May 1 14:06:37 2019 +0000

    third commit

commit 04081ddb43269238a1cb8a61a2d04a36986febfa
Author: dev <dev@example.com>
Date:   Wed May 1 14:05:43 2019 +0000

    second commit

commit 4c50701f89265f9ca6eeb3ddffae450da55f9bd5
Author: dev <dev@example.com>
Date:   Wed May 1 13:56:34 2019 +0000

    first commit

Git的commit对象与相应tree对象之间的关系如下图所示:

本质上,我们在执行git add/commit命令时,Git在背后做的就是上面这些工作:把修改过的文件存成blob对象,更新index,写入tree对象,写入commit对象并指向tree对象,在多个commit对象之间建立起前后相继的关联关系。包括blob,tree,以及commit对象在内,每个Git对象都会对应到.git/objects目录下的一个文件。下面是到目前为止我们在inside-git的本地Git库里手工创建出来的所有Git对象:

$ find .git/objects -type f
.git/objects/d3/827e75a5cadb9fe4a27e1cb9b6d192e7323120
.git/objects/07/ef9d54dd0da246d069dfa2ad2350751203ecb2
.git/objects/96/8b2bf72e28d8c6756054730880cf9f9ab06062
.git/objects/95/5f6fef4f43ee1f5d93cbea718cce3048450f4b
.git/objects/4f/4fc3399cef946fc77e12211808d0590715793d
.git/objects/4c/50701f89265f9ca6eeb3ddffae450da55f9bd5
.git/objects/e0/ec828eda6b51b170fff6b5fdfa03a3cb70a13e
.git/objects/04/081ddb43269238a1cb8a61a2d04a36986febfa
.git/objects/4b/fa25dce3532386b8924ca569efbada55685794

留下评论

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

正在加载...