git 使用详解(8)-- 分支HEAD

         有人把 Git 的分支模型称为“必杀技特性”,而正是由于它,将 Git 从版本控制系统家族里区分出来。Git 有何特别之处呢?Git 的分支可谓是难以置信的轻量级,它的新建操做几乎能够在瞬间完成,而且在不一样分支间切换起来也差很少同样快。和许多其余版本控制系统不一样,Git鼓励在工做流程中频繁使用分支与合并,哪怕一天以内进行许屡次都没有关系。理解分支的概念并熟练运用后,你才会意识到为何 Git 是一个如此强大而独特的工具,并今后真正改变你的开发方式。git

1  何谓分支vim

为了理解 Git 分支的实现方式,咱们须要回顾一下 Git 是如何储存数据的。或许你还记得第一章的内容,Git 保存的不是文件差别或者变化量,而只是一系列文件快照。数据结构

在 Git 中提交时,会保存一个提交(commit)对象,该对象包含一个指向暂存内容快照的指针,包含本次提交的做者等相关附属信息,包含零个或多个指向该提交对象的父对象指针:首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。工具

为直观起见,咱们假设在工做目录中有三个文件,准备将它们暂存后提交。暂存操做会对每个文件计算校验和(即第一章中提到的 SHA-1 哈希字串),而后把当前版本的文件快照保存到 Git 仓库中(Git 使用 blob 类型的对象存储这些快照),并将校验和加入暂存区域:.net

$ git add README test.rb LICENSE
$ git commit -m 'initial commit of my project'
当使用 git commit 新建一个提交对象前,Git 会先计算每个子目录(本例中就是项目根目录)的校验和,而后在 Git 仓库中将这些目录保存为树(tree)对象。以后 Git 建立的提交对象,除了包含相关提交信息之外,还包含着指向这个树对象(项目根目录)的指针,如此它就能够在未来须要的时候,重现这次快照的内容了。版本控制

如今,Git 仓库中有五个对象:三个表示文件快照内容的 blob 对象;一个记录着目录树内容及其中各个文件对应 blob 对象索引的 tree 对象;以及一个包含指向 tree 对象(根目录)的索引和其余提交信息元数据的 commit 对象。概念上来讲,仓库中的各个对象保存的数据和相互关系看起来如图 3-1 所示:指针

图 3-1. 单个提交对象在仓库中的数据结构对象

做些修改后再次提交,那么此次的提交对象会包含一个指向上次提交对象的指针(译注:即下图中的 parent 对象)。两次提交后,仓库历史会变成图 3-2 的样子:blog

图 3-2. 多个提交对象之间的连接关系索引

如今来谈分支。Git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。Git 会使用 master 做为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的 master 分支,它在每次提交的时候都会自动向前移动。

图 3-3. 分支其实就是从某个提交对象往回看的历史

那么,Git 又是如何建立一个新的分支的呢?答案很简单,建立一个新的分支指针。好比新建一个 testing 分支,可使用 git branch 命令:

$ git branch testing
这会在当前 commit 对象上新建一个分支指针(见图 3-4)。

图 3-4. 多个分支指向提交数据的历史

那么,Git 是如何知道你当前在哪一个分支上工做的呢?其实答案也很简单,它保存着一个名为 HEAD 的特别指针。请注意它和你熟知的许多其余版本控制系统(好比 Subversion 或 CVS)里的 HEAD 概念大不相同。在 Git 中,它是一个指向你正在工做中的本地分支的指针(译注:将 HEAD 想象为当前分支的别名。)。运行git branch 命令,仅仅是创建了一个新的分支,但不会自动切换到这个分支中去,因此在这个例子中,咱们依然还在 master 分支里工做(参考图 3-5)。

图 3-5. HEAD 指向当前所在的分支

要切换到其余分支,能够执行 git checkout 命令。咱们如今转换到新建的 testing 分支:

$ git checkout testing
这样 HEAD 就指向了 testing 分支(见图3-6)。

图 3-6. HEAD 在你转换分支时 指向 新的分支

这样的实现方式会给咱们带来什么好处呢?好吧,如今不妨再提交一次:

$ vim test.rb
$ git commit -a -m 'made a change'
图 3-7 展现了提交后的结果。

图 3-7. 每次提交后 HEAD 随着分支一块儿向前移动

很是有趣,如今 testing 分支向前移动了一格,而 master 分支仍然指向原先 git checkout 时所在的 commit 对象。如今咱们回到 master 分支看看:

$ git checkout master
图 3-8 显示告终果。

图 3-8. HEAD 在一次 checkout 以后移动到了另外一个分支

这条命令作了两件事。它把 HEAD 指针移回到 master 分支,并把工做目录中的文件换成了 master 分支所指向的快照内容。也就是说,如今开始所作的改动,将始于本项目中一个较老的版本。它的主要做用是将 testing 分支里做出的修改暂时取消,这样你就能够向另外一个方向进行开发。

咱们做些修改后再次提交:

$ vim test.rb
$ git commit -a -m 'made other changes'
如今咱们的项目提交历史产生了分叉(如图 3-9 所示),由于刚才咱们建立了一个分支,转换到其中进行了一些工做,而后又回到原来的主分支进行了另一些工做。这些改变分别孤立在不一样的分支里:咱们能够在不一样分支里反复切换,并在时机成熟时把它们合并到一块儿。而全部这些工做,仅仅须要branch 和checkout 这两条命令就能够完成。

图 3-9. 不一样流向的分支历史

因为 Git 中的分支实际上仅是一个包含所指对象校验和(40 个字符长度 SHA-1 字串)的文件,因此建立和销毁一个分支就变得很是廉价。说白了,新建一个分支就是向一个文件写入 41 个字节(外加一个换行符)那么简单,固然也就很快了。

这和大多数版本控制系统造成了鲜明对比,它们管理分支大多采起备份全部项目文件到特定目录的方式,因此根据项目文件数量和大小不一样,可能花费的时间也会有至关大的差异,快则几秒,慢则数分钟。而 Git 的实现与项目复杂度无关,它永远能够在几毫秒的时间内完成分支的建立和切换。同时,由于每次提交时都记录了祖先信息(译注:即parent 对象),未来要合并分支时,寻找恰当的合并基础(译注:即共同祖先)的工做其实已经天然而然地摆在那里了,因此实现起来很是容易。Git 鼓励开发者频繁使用分支,正是由于有着这些特性做保障。

接下来看看,咱们为何应该频繁使用分支。