Git详解之三 Git分支(第一部分)

Git 分支

几乎每一种版本控制系统都以某种形式支持分支。使用分支意味着你能够从开发主线上分离开来,而后在不影响主线的同时继续工做。在不少版本控制系统中,这是个昂贵的过程,经常须要建立一个源代码目录的完整副本,对大型项目来讲会花费很长时间。html

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

3.1  何谓分支

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

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

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

$ 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 的样子:测试

图 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-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 鼓励开发者频繁使用分支,正是由于有着这些特性做保障。

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

3.2  分支的新建与合并

如今让咱们来看一个简单的分支与合并的例子,实际工做中大致也会用到这样的工做流程:

1. 开发某个网站。 2. 为实现某个新的需求,建立一个分支。 3. 在这个分支上开展工做。

假设此时,你忽然接到一个电话说有个很严重的问题须要紧急修补,那么能够按照下面的方式处理:

1. 返回到原先已经发布到生产服务器上的分支。 2. 为此次紧急修补创建一个新分支,并在其中修复问题。 3. 经过测试后,回到生产服务器所在的分支,将修补分支合并进来,而后再推送到生产服务器上。 4. 切换到以前实现新需求的分支,继续工做。

分支的新建与切换

首先,咱们假设你正在项目中愉快地工做,而且已经提交了几回更新(见图 3-10)。

图 3-10. 一个简短的提交历史

如今,你决定要修补问题追踪系统上的 #53 问题。顺带说明下,Git 并不一样任何特定的问题追踪系统打交道。这里为了说明要解决的问题,才把新建的分支取名为 iss53。要新建并切换到该分支,运行git checkout 并加上 -b 参数:

$ git checkout -b iss53 Switched to a new branch "iss53"

这至关于执行下面这两条命令:

$ git branch iss53 
$ git checkout iss53

图 3-11 示意该命令的执行结果。


图 3-11. 建立了一个新分支的指针

接着你开始尝试修复问题,在提交了若干次更新后,iss53 分支的指针也会随着向前推动,由于它就是当前分支(换句话说,当前的 HEAD 指针正指向 iss53,见图 3-12):

$ vim index.html 
$ git commit -a -m 'added a new footer [issue 53]'

图 3-12. iss53 分支随工做进展向前推动

如今你就接到了那个网站问题的紧急电话,须要立刻修补。有了 Git ,咱们就不须要同时发布这个补丁和 iss53 里做出的修改,也不须要在建立和发布该补丁到服务器以前花费大力气来复原这些修改。惟一须要的仅仅是切换回master 分支。

不过在此以前,留心你的暂存区或者工做目录里,那些尚未提交的修改,它会和你即将检出的分支产生冲突从而阻止 Git 为你切换分支。切换分支的时候最好保持一个清洁的工做区域。稍后会介绍几个绕过这种问题的办法(分别叫作 stashing 和 commit amending)。目前已经提交了全部的修改,因此接下来能够正常转换到master 分支:

$ git checkout master Switched to branch "master"

此时工做目录中的内容和你在解决问题 #53 以前如出一辙,你能够集中精力进行紧急修补。这一点值得牢记:Git 会把工做目录的内容恢复为检出某分支时它所指向的那个提交对象的快照。它会自动添加、删除和修改文件以确保目录的内容和你当时提交时彻底同样。

接下来,你得进行紧急修补。咱们建立一个紧急修补分支 hotfix 来开展工做,直到搞定(见图 3-13):

$ git checkout -b 'hotfix' Switched to a new branch "hotfix" 
$ vim index.html 
$ git commit -a -m 'fixed the broken email addre

图 3-13. hotfix 分支是从 master 分支所在点分化出来的

有必要做些测试,确保修补是成功的,而后回到 master 分支并把它合并进来,而后发布到生产服务器。用 git merge命令来进行合并:

$ git checkout master 
$ git merge hotfix 
Updating f42c576..3a0874c Fast forward README | 1 - 1 files changed, 0 insertions(+), 1 deletions(-)

请注意,合并时出现了“Fast forward”的提示。因为当前 master 分支所在的提交对象是要并入的 hotfix 分支的直接上游,Git 只需把master 分支指针直接右移。换句话说,若是顺着一个分支走下去能够到达另外一个分支的话,那么 Git 在合并二者时,只会简单地把指针右移,由于这种单线的历史分支不存在任何须要解决的分歧,因此这种合并过程能够称为快进(Fast forward)。

如今最新的修改已经在当前 master 分支所指向的提交对象中了,能够部署到生产服务器上去了(见图 3-14)。


在那个超级重要的修补发布之后,你想要回到被打扰以前的工做。因为当前 hotfix 分支和 master 都指向相同的提交对象,因此hotfix 已经完成了历史使命,能够删掉了。使用 git branch 的 -d 选项执行删除操做:

$ git branch -d hotfix 
Deleted branch hotfix (3a0874c).

如今回到以前未完成的 #53 问题修复分支上继续工做(图 3-15):

$ git checkout iss53 Switched to branch "iss53" $ vim index.html 
$ git commit -a -m 'finished the new footer [issue 53]' [iss

图 3-15. iss53 分支能够不受影响继续推动。

不用担忧以前 hotfix 分支的修改内容还没有包含到 iss53 中来。若是确实须要归入这次修补,能够用git merge master 把 master 分支合并到 iss53;或者等 iss53 完成以后,再将iss53 分支中的更新并入master。

分支的合并

在问题 #53 相关的工做完成以后,能够合并回 master 分支。实际操做同前面合并 hotfix 分支差很少,只需回到master 分支,运行 git merge 命令指定要合并进来的分支:

$ git checkout master 
$ git merge iss53 Merge made by recursive. README | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)

请注意,此次合并操做的底层实现,并不一样于以前 hotfix 的并入方式。由于此次你的开发历史是从更早的地方开始分叉的。因为当前 master 分支所指向的提交对象(C4)并非 iss53 分支的直接祖先,Git 不得不进行一些额外处理。就此例而言,Git 会用两个分支的末端(C4 和 C5)以及它们的共同祖先(C2)进行一次简单的三方合并计算。图 3-16 用红框标出了 Git 用于合并的三个提交对象:

图 3-16. Git 为分支合并自动识别出最佳的同源合并点。

此次,Git 没有简单地把分支指针右移,而是对三方合并后的结果从新作一个新的快照,并自动建立一个指向它的提交对象(C6)(见图 3-17)。这个提交对象比较特殊,它有两个祖先(C4 和 C5)。

值得一提的是 Git 能够本身裁决哪一个共同祖先才是最佳合并基础;这和 CVS 或 Subversion(1.5 之后的版本)不一样,它们须要开发者手工指定合并基础。因此此特性让 Git 的合并操做比其余系统都要简单很多。

图 3-17. Git 自动建立了一个包含了合并结果的提交对象。

既然以前的工做成果已经合并到 master 了,那么 iss53 也就没用了。你能够就此删除它,并在问题追踪系统里关闭该问题。

既然以前的工做成果已经合并到 master 了,那么 iss53 也就没用了。你能够就此删除它,并在问题追踪系统里关闭该问题。

$ git branch -d iss53

遇到冲突时的分支合并

有时候合并操做并不会如此顺利。若是在不一样的分支中都修改了同一个文件的同一部分,Git 就没法干净地把二者合到一块儿(译注:逻辑上说,这种问题只能由人来裁决。)。若是你在解决问题 #53 的过程当中修改了hotfix 中修改的部分,将获得相似下面的结果:

$ git merge iss53 
Auto-merging index.html CONFLICT (content): Merge conflict in index.html Automatic merge failed; fix conflicts and then commit the result.

Git 做了合并,但没有提交,它会停下来等你解决冲突。要看看哪些文件在合并时发生冲突,能够用 git status 查阅:

[master*]$ git status
 index.html: needs merge # On branch master # Changed but not updated: # (use "git add ..." to update what will be committed) # (use "git checkout -- ..." to discard changes in working directory) # #	unmerged: index.html #

任何包含未解决冲突的文件都会以未合并(unmerged)的状态列出。Git 会在有冲突的文件里加入标准的冲突解决标记,能够经过它们来手工定位并解决这些冲突。能够看到此文件包含相似下面这样的部分:

<<<<<<< HEAD:index.htmlcontact : email.support@github.com
=======please contact us at support@github.com
>>>>>>> iss53:index.html

能够看到 ======= 隔开的上半部分,是 HEAD(即 master 分支,在运行merge 命令时所切换到的分支)中的内容,下半部分是在 iss53 分支中的内容。解决冲突的办法无非是两者选其一或者由你亲自整合到一块儿。好比你能够经过把这段内容替换为下面这样来解决:

please contact us at email.support@github.com

这个解决方案各采纳了两个分支中的一部份内容,并且我还删除了 <<<<<<<,======= 和 >>>>>>> 这些行。在解决了全部文件里的全部冲突后,运行 git add 将把它们标记为已解决状态(译注:实际上就是来一次快照保存到暂存区域。)。由于一旦暂存,就表示冲突已经解决。若是你想用一个有图形界面的工具来解决这些问题,不妨运行git mergetool,它会调用一个可视化的合并工具并引导你解决全部冲突:

$ git mergetool 
merge tool 
candidates: kdiff3 tkdiff xxdiff meld gvimdiff opendiff emerge vimdiff Merging the files: index.html Normal merge conflict for 'index.html': {local}: modified {remote}: modified Hit return to start merge resolution tool (opendiff):

若是不想用默认的合并工具(Git 为我默认选择了 opendiff,由于我在 Mac 上运行了该命令),你能够在上方”merge tool candidates”里找到可用的合并工具列表,输入你想用的工具名。咱们将在第七章讨论怎样改变环境中的默认值。

退出合并工具之后,Git 会询问你合并是否成功。若是回答是,它会为你把相关文件暂存起来,以代表状态为已解决。

再运行一次 git status 来确认全部冲突都已解决:

$ git status 
# On branch master # Changes to be committed: # (use "git reset HEAD ..." to unstage) # #	modified: index.html #

若是以为满意了,而且确认全部冲突都已解决,也就是进入了暂存区,就能够用 git commit 来完成此次合并提交。提交的记录差很少是这样:

Merge branch 'iss53' Conflicts: index.html # # It looks like you may be committing a MERGE. # If this is not correct, please remove the file # .git/MERGE_HEAD # and try again. #

若是想给未来看此次合并的人一些方便,能够修改该信息,提供更多合并细节。好比你都做了哪些改动,以及这么作的缘由。有时候裁决冲突的理由并不直接或明显,有必要略加注解。

相关文章
相关标签/搜索