为了真正理解 Git 处理分支的方式,咱们须要回顾一下 Git 是如何保存数据的。git
或许你还记得 起步 的内容,Git 保存的不是文件的变化或者差别,而是一系列不一样时刻的文件快照。算法
在进行提交操做时,Git 会保存一个提交对象(commit object)。知道了 Git 保存数据的方式,咱们能够很天然的想到——该提交对象会包含一个指向暂存内容快照的指针。 但不只仅是这样,该提交对象还包含了做者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。首次提交产生的提交对象没有父对象,普通提交操做产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象,vim
为了更加形象地说明,咱们假设如今有一个工做目录,里面包含了三个将要被暂存和提交的文件。 暂存操做会为每个文件计算校验和(使用咱们在 起步 中提到的 SHA-1 哈希算法),而后会把当前版本的文件快照保存到 Git 仓库中(Git 使用 blob 对象来保存它们),最终将校验和加入到暂存区域等待提交:网站
$ git add README test.rb LICENSE $ git commit -m 'The initial commit of my project'
当使用 git commit 进行提交操做时,Git 会先计算每个子目录(本例中只有项目根目录)的校验和,而后在 Git 仓库中这些校验和保存为树对象。 随后,Git 便会建立一个提交对象,它除了包含上面提到的这些信息外,还包含指向这个树对象(项目根目录)的指针。如此一来,Git 就能够在须要的时候重现这次保存的快照。版本控制
如今,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个树对象(记录着目录结构和 blob 对象索引)以及一个提交对象(包含着指向前述树对象的指针和全部提交信息)。指针
作些修改后再次提交,此次产生的提交对象会包含一个指向上次提交对象(父对象)的指针。code
Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master。 在屡次提交操做以后,你其实已经有一个指向最后这个提交对象的 master 分支。 它会在每次的提交操做中自动向前移动。orm
Git 的 “master” 分支并非一个特殊分支。 它就跟其它分支彻底没有区别。 之因此几乎每个仓库都有 master 分支,是由于 git init 命令默认建立它,而且大多数人都懒得去改动它。对象
分支建立blog
Git 是怎么建立新分支的呢? 很简单,它只是为你建立了一个能够移动的新的指针。 好比,建立一个 testing 分支, 你须要使用 git branch 命令:
$ git branch testing
这会在当前所在的提交对象上建立一个指针。 两个指向相同提交历史的分支。
Git 又是怎么知道当前在哪个分支上呢? 也很简单,它有一个名为 HEAD 的特殊指针。 请注意它和许多其它版本控制系统(如 Subversion 或 CVS)里的 HEAD 概念彻底不一样。 在 Git 中,它是一个指针,指向当前所在的本地分支(译注:将 HEAD 想象为当前分支的别名)。 在本例中,你仍然在 master 分支上。 由于 git branch 命令仅仅 建立 一个新分支,并不会自动切换到新分支中去。 HEAD 指向当前所在的分支。
你能够简单地使用 git log 命令查看各个分支当前所指的对象。 提供这一功能的参数是 --decorate。
$ git log --oneline --decorate f30ab (HEAD, master, testing) add feature #32 - ability to add new 34ac2 fixed bug #1328 - stack overflow under certain conditions 98ca9 initial commit of my project
正如你所见,当前 “master” 和 “testing” 分支均指向校验和以 f30ab 开头的提交对象。 分支切换
要切换到一个已存在的分支,你须要使用 git checkout 命令。 咱们如今切换到新建立的 testing 分支去:
$ git checkout testing
这样 HEAD 就指向 testing 分支了。 HEAD 指向当前所在的分支。
这样的实现方式会给咱们带来什么好处呢? 如今不妨再提交一次:
$ vim test.rb $ git commit -a -m 'made a change'
HEAD 分支随着提交操做自动向前移动。
如图所示,你的 testing 分支向前移动了,可是 master 分支却没有,它仍然指向运行 git checkout 时所指的对象。 这就有意思了,如今咱们切换回 master 分支看看:
$ git checkout master
检出时 HEAD 随之移动。
这条命令作了两件事。 一是使 HEAD 指回 master 分支,二是将工做目录恢复成 master 分支所指向的快照内容。 也就是说,你如今作修改的话,项目将始于一个较旧的版本。 本质上来说,这就是忽略 testing 分支所作的修改,以便于向另外一个方向进行开发。 分支切换会改变你工做目录中的文件
在切换分支时,必定要注意你工做目录里的文件会被改变。 若是是切换到一个较旧的分支,你的工做目录会恢复到该分支最后一次提交时的样子。 若是 Git 不能干净利落地完成这个任务,它将禁止切换分支。
咱们不妨再稍微作些修改并提交:
$ vim test.rb $ git commit -a -m 'made other changes'
如今,这个项目的提交历史已经产生了分叉(参见 项目分叉历史)。 由于刚才你建立了一个新分支,并切换过去进行了一些工做,随后又切换回 master 分支进行了另一些工做。 上述两次改动针对的是不一样分支:你能够在不一样分支间不断地来回切换和工做,并在时机成熟时将它们合并起来。 而全部这些工做,你须要的命令只有 branch、checkout 和 commit。 项目分叉历史。
你能够简单地使用 git log 命令查看分叉历史。 运行 git log --oneline --decorate --graph --all ,它会输出你的提交历史、各个分支的指向以及项目的分支分叉状况。
$ git log --oneline --decorate --graph --all * c2b9e (HEAD, master) made other changes | * 87ab2 (testing) made a change |/ * f30ab add feature #32 - ability to add new formats to the * 34ac2 fixed bug #1328 - stack overflow under certain conditions * 98ca9 initial commit of my project
因为 Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件,因此它的建立和销毁都异常高效。 建立一个新分支就至关于往一个文件中写入 41 个字节(40 个字符和 1 个换行符),如此的简单能不快吗?
这与过去大多数版本控制系统造成了鲜明的对比,它们在建立分支时,将全部的项目文件都复制一遍,并保存到一个特定的目录。 完成这样繁琐的过程一般须要好几秒钟,有时甚至须要好几分钟。所需时间的长短,彻底取决于项目的规模。而在 Git 中,任何规模的项目都能在瞬间建立新分支。 同时,因为每次提交都会记录父对象,因此寻找恰当的合并基础(译注:即共同祖先)也是一样的简单和高效。 这些高效的特性使得 Git 鼓励开发人员频繁地建立和使用分支。