之前说过,Git 保存的不是文件差异或者变化量,而是一系列文件快照。
在Git中提交时,会保存一个提交(commit)对象,该对象包含一个指向暂存内容快照的指针,包含本次提交的作者等相关附加信息,包含0个或多个指向该提交对象的父对象指针:首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。
为直接起见,我们假设在工作目录中有三个文件,准备将它们暂存后提交。暂存操作会对每一个文件计算校验和,然后把当前版本的文件快照保存到Git仓库中,并将校验和加入暂存区域:
$ git add README test.rb LICENSE $ git commit -m 'initial commit of my project'
当使用 git commit
新建一个提交对象前,Git 会先计算每一个子目录的校验和,然后在Git仓库中将这些目录保存为树对象。之后Git创建的提交对象,除了包含相关提交信息以外,还包含着指向这个数对象的指针,如此它就可以在将来需要的时候,重现此次快照的内容了。
现在,Git 仓库中有五个对象:三个表示文件快照内容的 blob 对象;一个记录着目录树内容及其中各个文件对应 blob 对象索引的 tree 对象;以及一个包含指向 tree 对象(根目录)的索引和其他提交信息元数据的 commit 对象。概念上来说,仓库中的各个对象保存的数据和相互关系看起来如图 3-1 所示:
做些修改后再次提交,那么这次的提交对象会包含一个指向上次提交的对象的指针。两次提交后,仓库历史会变成 3-2的样子:
现在来谈分支。Git中的分支,其实本质上仅仅是个指向commit对象的可变指针。Git会使用master作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的master分支,它在每次提交的时候都会自动向前移动。
那么,Git又是如何创建一个新的分支的呢?创建一个新的分支指针,比如新建一个testing分支,可以使用 git branch
命令:
$ git branch testing
这会在当前commit对象上新建一个分支指针
那么,Git是如何知道你当前在哪个分支上工作的呢?其实它保存着一个名为HEAD的特别指针。在Git中,它是一个指向你正在工作中的本地分支的指针。运行git branch
命令,仅仅是建立了一个新的分支,但不会自动切换到这个分支中去,所以在这个例子中,我们依然还在master分支里工作。
要切换到其他分支,可以执行 git checkout
命令。我们现在切换到新建的testing分支:
$ git checkout testing
这样HEAD就指向了testing分支,如图:
这样的实现方式会给我们带来什么好处呢?
$ vim test.rb $ git commit -a -m 'made a change'
下图展示了提交后的结果:
非常有趣,现在testing分支向前移动了一格,而master分支依然指向原先 git checkout
时所在的commit对象。现在我们回到master分支看看:
$ git checkout master
这条命令做了两件事。它把HEAD指针移回到master分支,并把工作目录中的文件换成了master分支所指向的快照内容。也就是说,现在开始所作的改动,将始于本项目中一个较老的版本。它的主要作用是将testing分支里做出的修改暂时取消,这样你就可以向另一个方向进行开发。
我们做些修改再次提交:
$ vim test.rb $ git commit -a -m 'made other changes'
现在我们的项目提交历史产生了分叉(如图 3-9 所示),因为刚才我们创建了一个分支,转换到其中进行了一些工作,然后又回到原来的主分支进行了另外一些工作。这些改变分别孤立在不同的分支里:我们可以在不同分支里反复切换,并在时机成熟时把它们合并到一起。而所有这些工作,仅仅需要 branch和checkout这两条命令就可以完成。
由于 Git 中的分支实际上仅是一个包含所指对象校验和(40 个字符长度 SHA-1 字串)的文件,所以创建和销毁一个分支就变得非常廉价。说白了,新建一个分支就是向一个文件写入 41 个字节(外加一个换行符)那么简单,当然也就很快了。
这和大多数版本控制系统形成了鲜明对比,它们管理分支大多采取备份所有项目文件到特定目录的方式,所以根据项目文件数量和大小不同,可能花费的时间也会有相当大的差别,快则几秒,慢则数分钟。而 Git 的实现与项目复杂度无关,它永远可以在几毫秒的时间内完成分支的创建和切换。同时,因为每次提交时都记录了祖先信息(译注:即 parent
对象),将来要合并分支时,寻找恰当的合并基础(译注:即共同祖先)的工作其实已经自然而然地摆在那里了,所以实现起来非常容易。Git 鼓励开发者频繁使用分支,正是因为有着这些特性作保障。
接下来看看,我们为什么应该频繁使用分支。