当咱们初始化Git仓库的时候,Git会默认建立一个名为master的主分支。在实际工做中,主分支要求是一个稳定、健壮、安全的主线,通常不容许在主分支上直接进行开发,而是拉取一个新的分支,开发、测试完成后,再将分支合并到主分支上。git
使用分支意味着你能够从开发主线上分离开来,而后在不影响主线的同时继续工做。在不少版本控制系统中,这是个昂贵的过程,经常须要建立一个源代码目录的完整副本,对大型项目来讲会花费很长时间。安全
Git 的分支模型可称为“必杀技特性”,而正是由于该特性将 Git 从版本控制系统家族里区分出来,鹤立鸡群。其余版本控制系统如SVN等都有分支管理,可是用过以后你会发现,这些版本控制系统建立和切换分支比蜗牛还慢,简直让人没法忍受,结果分支功能成了摆设,你们都不去用。但Git的分支是不同凡响的,不管建立、切换和删除分支,Git能在瞬间完成!不管你的版本库是1个文件仍是1万个文件。app
Git 鼓励在工做流程中频繁使用分支与合并,哪怕一天以内进行许屡次都没有关系。在实际工做中,每每修复一个bug都会使用一个分支来完成。工具
理解分支的概念并熟练运用后,你才会意识到为何 Git 是一个如此强大而独特的工具,并今后真正改变你的开发方式。测试
分支实现原理
在第一篇文章中提到过,Git 保存的不是文件差别或者变化量,而只是一系列文件快照。this
在 Git 中提交时,会保存一个提交(commit)对象,该对象包含一个指向暂存内容快照的指针,包含本次提交的做者等相关附属信息,包含零个或多个指向该提交对象的父对象指针:首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。spa
为直观起见,咱们假设在工做目录中有三个文件,准备将它们暂存后提交。暂存操做会对每个文件计算校验和(即SHA-1 哈希字串),而后把当前版本的文件快照保存到 Git 仓库中(Git 使用 blob 类型的对象存储这些快照),并将校验和加入暂存区域。版本控制
当使用 Git commit新建一个提交对象前,Git 会先计算每个子目录(本例中就是项目根目录)的校验和,而后在 Git 仓库中将这些目录保存为树(tree)对象。以后 Git 建立的提交对象,除了包含相关提交信息之外,还包含着指向这个树对象(项目根目录)的指针,如此它就能够在未来须要的时候,重现这次快照的内容了。指针
如今,Git 仓库中有五个对象:三个表示文件快照内容的 blob 对象;一个记录着目录树内容及其中各个文件对应 blob 对象索引的 tree 对象;以及一个包含指向 tree 对象(根目录)的索引和其余提交信息元数据的 commit 对象:code
做些修改后再次提交,那么此次的提交对象会包含一个指向上次提交对象的指针(即下图中的 parent 对象)。两次提交后,仓库历史会这个样子:
Git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。Git 会使用 master 做为分支的默认名字。在若干次提交后,其实已经有了一个指向最后一次提交对象的 master 分支,它在每次提交的时候都会自动向前移动。
建立分支
建立一个新的名为“testing”分支,可使用“git branch<branchName>”命令:
git branch testing该命令会在当前 commit 对象上新建一个指针:
那么,Git 是如何知道你当前在哪一个分支上工做的呢?其实答案也很简单,它保存着一个名为HEAD 的特别指针。在 Git 中,它是一个指向正在工做中的本地分支的指针(能够将 HEAD 想象为当前分支的别名)。运行Git branch 命令,仅仅是创建了一个新的分支,但不会自动切换到这个分支中去,因此在这个例子中,咱们依然还在 master 分支里工做。
使用不带任何参数的“git branch”命令能够查看当前的分支状况:
* master testingGit显示,共有两个分支,当前工做分支为master,分支列表中的星号“*”至关于HEAD指针,标注了当前工做分支。
切换分支
命令“git checkout <branchName>”能够将当前工做分支切换到名为branchName的分支。好比,运行命令:
git checkout testingGit会提示:
Switched to branch 'testing'这样 HEAD 就指向了 testing 分支:
如今咱们若是修改了工做区的文件,全部commit操做都是提交到testing分支,而非master。
如今 testing 分支向前移动了一步,而 master 分支仍然指向原先 git checkout 时所在的 commit 对象。如今从新切换到master分支:
git checkout master
这条命令作了两件事。它把 HEAD 指针移回到 master 分支,并把工做目区的文件换成了 master 分支所指向的快照内容。也就是说,如今开始所作的改动,将始于本项目中一个较老的版本。它的主要做用是将 testing 分支里做出的修改暂时取消,这样你就能够向另外一个方向进行开发。
在mast分支上再作些修改,而后提交。如今咱们的项目提交历史产生了分叉,由于刚才咱们建立了一个分支testing,转换到其中进行了一些工做,而后又回到原来的master主分支进行了另一些工做。
这些改变分别孤立在不一样的分支里。咱们能够在不一样分支里反复切换,并在时机成熟时把它们合并到一块儿。
因为 Git 中的分支实际上仅是一个包含所指对象校验和(40 个字符长度 SHA-1 字串)的文件,因此建立和销毁一个分支就变得很是廉价。说白了,新建一个分支就是向一个文件写入 41 个字节(外加一个换行符)那么简单,固然也就很快。
合并分支
模拟这样的一个场景,早上到了公司接到新任务,新建一个名为“iss53”的分支来进行开发工做。要新建并切换到该分支,运行git checkout 并加上 -b 参数:
git checkout -b iss53这至关于执行下面这两条命令:
git branch iss53 git checkout iss53
而后不断地写代码,提交代码:
忽然,接到通知,须要当即修复master分支上的一个严重bug。
第一步确定须要切换到master。若是当前工做区与暂存区都是干净的,OK,直接切换回master便可。可是若是iss53分支上的开发尚未完成,而且不便于commit到版本库,怎么办?一旦切回到其余分支,工做区与暂存区就会被清空、覆盖。实际上,若是工做区或暂存区不是干净的,存在没有提交到版本库的更改,Git是不容许切换分支的,会提示:
error: Your local changes to the following files wouldbe overwritten by checkout: readme.txt Please, commit your changes or stash them before you can switch branches.解决这个问题的办法就是git stash命令。
该命令能够获取工做目录的中间状态——也就是修改过的被追踪的文件和暂存的变动——并将它保存到一个未完结变动的堆栈中,随时能够从新应用。
运行“git stash”命令以后,iss53分支上未commit得变动就会被“储藏”起来,能够顺利地切换到master分支了。要查看现有的储藏,你可使用 git stash list,会的到这样的一个列表:
stash@{0}: WIP on testing: 049d078 … stash@{1}: WIP on testing: c264051 … stash@{2}: WIP on testing: 21d80a5 …列出的是该分支上全部被stash过的编号,使用命令“git stash apply”便可恢复到最新stash过的场景。若是想应用更早的储藏,能够经过名字指定它,像这样:git stash apply stash@{2}。若是不指明编号,Git 默认使用最近的储藏并尝试应用它。
题归正转,咱们切换到master分支,拉去一个名为“hotfix”的分支来紧急修复bug。
git checkout -b 'hotfix'修复好以后,commit到版本库,则如今Git的分支结构以下图所示:
经测试以后,该bug成功修复,而后须要将该分支合并到master,首先依然要切换到master,而后使用命令“git merge”合并分支:
git checkout master git merge hotfixGit提示:
Updating 771f6de..adea62a Fast-forward …请注意,合并时出现了“Fast forward”的提示。因为当前 master 分支所在的提交对象是要并入的 hotfix 分支的直接上游,Git 只需把master 分支指针直接右移。换句话说,若是顺着一个分支走下去能够到达另外一个分支的话,那么 Git在合并二者时,只会简单地把指针右移,由于这种单线的历史分支不存在任何须要解决的分歧,因此这种合并过程能够称为快进(Fast forward)。
如今最新的修改已经在当前master 分支所指向的提交对象中了:
在那个超级重要的修补发布之后,就能够回继续以前未完成的工做。因为当前 hotfix 分支和 master 都指向相同的提交对象,因此hotfix 已经完成了历史使命,能够删掉了。使用 git branch 的 -d 选项执行删除操做:
git branch -d hotfix不用担忧以前 hotfix 分支的修改内容还没有包含到 iss53 中来。若是确实须要归入这次修补,能够用git merge master 把 master 分支合并到 iss53;或者等 iss53 完成以后,再将iss53 分支中的更新并入 master。
如今回到以前未完成的 iss53分支上继续工做,完成后commit到版本库。
在问iss53 分支上的工做完成以后,能够合并回 master 分支。实际操做同前面合并 hotfix 分支差很少,只需回到master分支,运行 git merge 命令指定要合并进来的分支。
请注意,此次合并操做的底层实现,并不一样于以前 hotfix 的并入方式。由于此次开发历史是从更早的地方开始分叉的。因为当前master 分支所指向的提交对象(C4)并非 iss53 分支的直接祖先,Git 不得不进行一些额外处理。就此例而言,Git 会用两个分支的末端(C4 和 C5)以及它们的共同祖先(C2)进行一次简单的三方合并计算。下图用红框标出了Git 用于合并的三个提交对象:
此次,Git 没有简单地把分支指针右移,而是对三方合并后的结果从新作一个新的快照,并自动建立一个指向它的提交对象(C6)。这个提交对象比较特殊,它有两个祖先(C4 和 C5)。
值得一提的是 Git 能够本身裁决哪一个共同祖先才是最佳合并基础,不须要开发者手工指定合并基础。此特性让Git 的合并操做比其余系统都要简单很多。
解决冲突
有时候合并操做并不会如此顺利。若是在不一样的分支中都修改了同一个文件的同一部分,Git 就没法干净地把二者合到一块儿,逻辑上说,这种问题只能由人来裁决。这时候若是合并分支就会出现下面的结果:
Auto-merging readme.txt CONFLICT (content): Merge conflictin readme.txt Automatic merge failed; fix conflicts and then committhe result.Git 做了合并,但没有提交,它会停下来等你解决冲突。要看看哪些文件在合并时发生冲突,能够用git status 查阅:
On branch master You have unmerged paths. (fix conflictsand run "git commit") Unmerged paths: (use "gitadd <file>..." to mark resolution) bothmodified: readme.txt no changes added to commit (use "git add"and/or "git commit -a")任何包含未解决冲突的文件都会以未合并(unmerged)的状态列出。Git 会在有冲突的文件里加入标准的冲突解决标记,能够经过它们来手工定位并解决这些冲突。能够看到此文件包含相似下面这样的部分:
this is my first git project <<<<<<<HEAD add row on master branch add annother row on master branch ======= add row on testing branch add another row on testing branch >>>>>>>testing能够看到 ======= 隔开的上半部分,是 HEAD(即 master 分支,在运行merge 命令时所切换到的分支)中的内容,下半部分是在 iss53 分支中的内容。解决冲突的办法无非是两者选其一或者由你亲自整合到一块儿。固然,Git插入的额外标记行也须要删除。
在解决了全部文件里的全部冲突后,运行 git add将把它们标记为已解决状态(实际上就是来一次快照保存到暂存区域)。由于一旦暂存,就表示冲突已经解决。
删除分支
分支合并到master以后,若是无特殊用途,应该及时删除分支。
要从该清单中筛选出已经(或还没有)与当前分支合并的分支,能够用 “--merged”和“--no-merged” 选项。
好比用“git branch --merged” 查看哪些分支已被并入当前分支,也就是说哪些分支是当前分支的直接上游:
* master testing证实testing分支已经合并到master分支当中了,能够删除:
git branch –d testing
使用“git branch --no-merge”查看尚未合并的分支:
newTesting
若是使用命令“git branch–d newTesting”删除该分支,Git会提示:error: The branch 'newTesting' is not fully merged. If you are sure you want to delete it, run 'git branch -D newTesting'.因为这些分支中还包含着还没有合并进来的工做成果,因此简单地用 Git branch -d 删除该分支会提示错误,由于那样作会丢失数据。
不过,若是你确实想要删除该分支上的改动,能够用大写的删除选项 -D 强制执行,就像上面提示信息中给出的那样。