几乎每一种版本控制系统都以某种形式支持分支。使用分支意味着你能够从开发主线上分离开来,而后在不影响主线的同时继续工做。在不少版本控制系统中,这是个昂贵的过程,经常须要建立一个源代码目录的完整副本,对大型项目来讲会花费很长时间。javascript
有人把 Git 的分支模型称为“必杀技特性”,而正是由于它,将 Git 从版本控制系统家族里区分出来。Git 有何特别之处呢?Git 的分支可谓是难以置信的轻量级,它的新建操做几乎能够在瞬间完成,而且在不一样分支间切换起来也差很少同样快。和许多其余版本控制系统不一样,Git 鼓励在工做流程中频繁使用分支与合并,哪怕一天以内进行许屡次都没有关系。理解分支的概念并熟练运用后,你才会意识到为何 Git 是一个如此强大而独特的工具,并今后真正改变你的开发方式。html
【读后总结】java
每建立一个分支就必须完整复制全部的源代码文件的版本控制系统,分支的建立代价大——时间和空间,时间上包括建立和切换分支所需的时间;空间上,若是项目过大、分支过多,将极大的占用硬盘空间。而Git的分支管理倒是轻量级的,时间上,建立和切换一瞬间完成;空间上,当你修改了某个文件,Git才会再额外生成该修改文件的文件快照(注:Git保存的文件快照是规定的文件头后面再链接文件内容,而后再通过必定压缩的一个文件)。若是你只建立分支而不修改文件,那么它只生成一个记录分支的引用文件(注:引用文件保在.git/refs/heads文件夹里,名字为分支名,文件内容只是当前分支中最新提交的commit对象的SHA-1值),这个引用文件很小。若是在几MB甚至大到几GB的项目中,修改为千上万个源文件中的一个,那么Git只会把修改的文件保存一个新的快照,而后更新保存在相关tree对象中的旧文件的SHA-1值,使它指向新的文件快照。git
为了理解 Git 分支的实现方式,咱们须要回顾一下 Git 是如何储存数据的。或许你还记得第一章的内容,Git 保存的不是文件差别或者变化量,而只是一系列文件快照。github
在 Git 中提交时,会保存一个提交(commit)对象,该对象包含一个指向暂存内容快照的指针,包含本次提交的做者等相关附属信息,包含零个或多个指向该提交对象的父对象指针:首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。数据库
为直观起见,咱们假设在工做目录中有三个文件,准备将它们暂存后提交。暂存操做会对每个文件计算校验和(即第一章中提到的 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 所示:网络
做些修改后再次提交,那么此次的提交对象会包含一个指向上次提交对象的指针(译注:即下图中的 parent 对象)。两次提交后,仓库历史会变成图 3-2 的样子:数据结构
如今来谈分支。Git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。Git 会使用 master 做为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的 master 分支,它在每次提交的时候都会自动向前移动。
那么,Git 又是如何建立一个新的分支的呢?答案很简单,建立一个新的分支指针。好比新建一个 testing 分支,可使用 git branch
命令:
$ git branch testing
这会在当前 commit 对象上新建一个分支指针(见图 3-4)。
那么,Git 是如何知道你当前在哪一个分支上工做的呢?其实答案也很简单,它保存着一个名为 HEAD 的特别指针。请注意它和你熟知的许多其余版本控制系统(好比 Subversion 或 CVS)里的 HEAD 概念大不相同。在 Git 中,它是一个指向你正在工做中的本地分支的指针(译注:将 HEAD 想象为当前分支的别名。)。运行git branch
命令,仅仅是创建了一个新的分支,但不会自动切换到这个分支中去,因此在这个例子中,咱们依然还在 master 分支里工做(参考图 3-5)。
要切换到其余分支,能够执行 git checkout
命令。咱们如今转换到新建的 testing 分支:
$ git checkout testing
这样 HEAD 就指向了 testing 分支(见图3-6)。
这样的实现方式会给咱们带来什么好处呢?好吧,如今不妨再提交一次:
$ vim test.rb
$ git commit -a -m 'made a change'
图 3-7 展现了提交后的结果。
很是有趣,如今 testing 分支向前移动了一格,而 master 分支仍然指向原先 git checkout
时所在的 commit 对象。如今咱们回到 master 分支看看:
$ git checkout master
图 3-8 显示告终果。
这条命令作了两件事。它把 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 鼓励开发者频繁使用分支,正是由于有着这些特性做保障。
接下来看看,咱们为何应该频繁使用分支。
【读后总结】
(1)git add暂存操做会计算每一个文件的校验和sha-1,而后把它们的快照保存到git仓库中,并将sha-1加入暂存区域
(2)Git保存的是文件的快照,保存位置为.git/objects目录下,Git相似一个key-value数据存储系统,每保存一个文件到仓库中,将会在该目录下生成一个子目录和一个文件,子目录以生成的sha-1值的前两个字符命名,文件以剩下的38个字符命名。普通的blob类型对象的文件内容包括一个文件头和数据内容,文件头为”blob #{content.length}\0“,再和数据内容链接,再通过压缩最后才生成.git/objects目录下的文件
(3)git commit以前,git会计算每个子目录的sha-1,而后在git仓库中将这些目录保存为tree对象,将暂存区的内容添加到tree对象,以后git建立commit对象。
(4)tree对象,包含了多条记录,每条记录包含了一个指向blob或子tree类型对象的指针,同时还包含了对象的类型、名字、sha-1值、权限属性(普通、可执行仍是符号连接)等信息
(5)一个commit对象,包含做者、提交做者、注释信息、当前时间、该时间点项目快照的顶层树对象、祖先commit对象指针等,首个commit对象没有祖先。
(6)第二次commit,commit对象包含有前一次commit的commit对象的sha-1值,这个commit有了祖先。
(7)git的分支实际上是一个指向commit对象的可变指针,当你使用git branch <分支名称>建立一个新的分支时,git实际上是在.git/refs下生成一个以分支名称命名的文件,该文件内容为当前分支的最后一次提交的commit对象的sha-1值,只是一个引用而已,此时当前分支不变
(8)HEAD,git使用它保存当前所在分支的sha-1值,但它不是直接保存sha-1值,而是在.git目录下的HEAD文件中保存当前分支最后一次提交的commit对象的引用路径。把HEAD当成是当前分支的别名一点都没错的。使用git checkout <分支名称>能够改变HEAD的内容中的引用路径
(9)每次commit后,HEAD中路径所指向的引用会自动前移指向最新的commit对象。
(10)git commit 加上-a选项,Git 就会自动把全部已经跟踪过的文件暂存起来一并提交,从而跳过git add 步骤。
(11)若是你只建立分支而不修改文件,那么它只生成一个记录分支的引用文件(注:引用文件保在.git/refs/heads文件夹里,名字为分支名,文件内容只是当前分支中最新提交的commit对象的SHA-1值),这个引用文件很小。若是在几MB甚至大到几GB的项目中,修改为千上万个源文件中的一个,那么Git只会把修改的文件保存一个新的快照,而后更新保存在相关tree对象中的旧文件的SHA-1值,使它指向新的文件快照。
如今让咱们来看一个简单的分支与合并的例子,实际工做中大致也会用到这样的工做流程:
1. 开发某个网站。 2. 为实现某个新的需求,建立一个分支。 3. 在这个分支上开展工做。
假设此时,你忽然接到一个电话说有个很严重的问题须要紧急修补,那么能够按照下面的方式处理:
1. 返回到原先已经发布到生产服务器上的分支。 2. 为此次紧急修补创建一个新分支,并在其中修复问题。 3. 经过测试后,回到生产服务器所在的分支,将修补分支合并进来,而后再推送到生产服务器上。 4. 切换到以前实现新需求的分支,继续工做。
首先,咱们假设你正在项目中愉快地工做,而且已经提交了几回更新(见图 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 示意该命令的执行结果。
接着你开始尝试修复问题,在提交了若干次更新后,iss53
分支的指针也会随着向前推动,由于它就是当前分支(换句话说,当前的 HEAD
指针正指向 iss53
,见图 3-12):
$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'
如今你就接到了那个网站问题的紧急电话,须要立刻修补。有了 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 address'
[hotfix]: created 3a0874c:"fixed the broken email address"
1 files changed,0 insertions(+),1 deletions(-)
有必要做些测试,确保修补是成功的,而后回到 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]'
[iss53]: created ad82d7a:"finished the new footer [issue 53]"
1 files changed,1 insertions(+),0 deletions(-)
不用担忧以前 hotfix
分支的修改内容还没有包含到 iss53
中来。若是确实须要归入这次修补,能够用git merge master
把 master 分支合并到 iss53
;或者等 iss53
完成以后,再将iss53
分支中的更新并入 master
。
【读后总结】
(1)分支的建立:
git branch <分支名称>
(2)分支的切换:
git checkout <分支名称>
(3)分支的建立且同时切换:
git checkout -b <分支名称>
(4)在切换到其余分支前,留心你的暂存区或者工做目录里,那些尚未提交的修改,它会和你即将检出的分支产生冲突从而阻止 Git 为你切换分支。为何呢?由于在切换时,git会自动的恢复工做目录到想切换的分支指针所指的commit对象的所指的整棵树的文件状态。若是没有提交修改,则会产生冲突。同时暂存区的目录树的内容恢复到和分支的版本库同样。
(5)当你在A分支(不是master分支)上进行工做时,忽然有新的紧急任务,此时应该提交A分支的修改,而后切换到master分支,在master上建立一个新分支B而且换换到新的分支B,工紧急任务完成后想要继续在A分支下工做,先切换到master分支而后合并B分支,删除B分支接着切换到A分支便可。
(6)git branch <分支名称>命令详解:
调用git branch 命令后,git只是调用一个底层命令git update-ref refs/head/<引用名> sha-1值,sha-1值是当前分支的最后一次提交的commit对象,而后在.git/refs/heads下生成一个以引用名命名的文件,注意:此时当前分支不会自动切换到新建的分支。
git是如何知道当前分支的最后一次提交的commit对象的sha-1值呢?在.git目录下有一个HEAD文件,该文件里存放了一个路径,路径为一个指向当前分支的最后一次提交的commit对象的引用文件(位于.git/objects/目录下)。
(7)git checkout <分支名称>命令详解:
git调用了一个git symbolic-ref HEAD refs/heads/<分支名称>底层命令,更改.git目录下HEAD的内容为refs/heads/<分支名称>。这样便改变了当前分支。并把工做目录的内容恢复为切换分支所指向的commit对象的快照内容。
(8)分支合并的步骤:
1.切换到所要合并的分支,命令为
git checkout <分支名>
2.合并分支,命令为
git merge <被合并分支名>
3.合并后删除分支,命令为
git branch -d <分支名>
在问题 #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 用于合并的三个提交对象:
此次,Git 没有简单地把分支指针右移,而是对三方合并后的结果从新作一个新的快照,并自动建立一个指向它的提交对象(C6)(见图 3-17)。这个提交对象比较特殊,它有两个祖先(C4 和 C5)。
值得一提的是 Git 能够本身裁决哪一个共同祖先才是最佳合并基础;这和 CVS 或 Subversion(1.5 之后的版本)不一样,它们须要开发者手工指定合并基础。因此此特性让 Git 的合并操做比其余系统都要简单很多。
既然以前的工做成果已经合并到 master
了,那么 iss53
也就没用了。你能够就此删除它,并在问题追踪系统里关闭该问题。
$ git branch -d iss53
【读后总结】
(1)分支合并状况有多种,一种是当前master分支所指向的提交对象为新建分支的直接祖先,一种则不是。
第一种状况的合并比较简单,合并时,只须要前移master指针使它指向新建分支所指向的commit对象。这种状况下,若是在master和新建分支之间还有多个直接祖先为master的分支,直接和最新的合并,那么若是再和上游的分支合并,会提示already up-to-date。若是合并的是上游的,那么能够一步一步合并到最新的分支。
第二种状况的合并比较复杂,由于当前master分支所指向的commit对象不是新建分支的直接祖先,Git不得不进行一些额外的处理,通常会取新建分支所指向的commit、master所指向的commit和新建分支和master的共同祖先所指向的commit这三个对象合并为一个新的commit对象,并生成结果的文件快照,该文件快照是一些合并时被修改的文件快照。
(2)Git会自动选取合适的新建分支和master的共同祖先做为合并基础,这一点和CVS和subversion的人工选择不一样,极大的简化操做流程。
(3)将某个分支合并到当前分支的命令:
git merge <分支名>
(4)删除分支的命令:
git branch -d <分支名>
有时候合并操做并不会如此顺利。若是在不一样的分支中都修改了同一个文件的同一部分,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.html
=======
>>>>>>> iss53:index.html
能够看到 =======
隔开的上半部分,是 HEAD
(即 master
分支,在运行merge
命令时所切换到的分支)中的内容,下半部分是在 iss53
分支中的内容。解决冲突的办法无非是两者选其一或者由你亲自整合到一块儿。好比你能够经过把这段内容替换为下面这样来解决:
这个解决方案各采纳了两个分支中的一部份内容,并且我还删除了 <<<<<<<
,=======
和 >>>>>>>
这些行。在解决了全部文件里的全部冲突后,运行 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
Hitreturn 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.
#
若是想给未来看此次合并的人一些方便,能够修改该信息,提供更多合并细节。好比你都做了哪些改动,以及这么作的缘由。有时候裁决冲突的理由并不直接或明显,有必要略加注解。
【读后总结】
(1)若是在不一样的分支中都修改了同一个文件的同一部分,Git 就没法干净地把二者合到一块儿(译注:逻辑上说,这种问题只能由人来裁决。)。
(2)冲突下合并
1.人工修改冲突的地方
2.查看git status
3.git add <文件名>标记已解决
4.git commit -m "备注"
到目前为止,你已经学会了如何建立、合并和删除分支。除此以外,咱们还须要学习如何管理分支,在往后的常规工做中会常常用到下面介绍的管理命令。
git branch
命令不只仅能建立和删除分支,若是不加任何参数,它会给出当前全部分支的清单:
$ git branch
iss53
* master
testing
注意看 master
分支前的 *
字符:它表示当前所在的分支。也就是说,若是如今提交更新,master
分支将随着开发进度前移。若要查看各个分支最后一个提交对象的信息,运行git branch -v
:
$ git branch -v
iss53 93b412c fix javascript issue
* master 7a98805Merge branch 'iss53'
testing 782fd34 add scott to the author list in the readmes
要从该清单中筛选出你已经(或还没有)与当前分支合并的分支,能够用 --merge
和 --no-merged
选项(Git 1.5.6 以上版本)。好比用git branch --merge
查看哪些分支已被并入当前分支(译注:也就是说哪些分支是当前分支的直接上游。):
$ git branch --merged
iss53
* master
以前咱们已经合并了 iss53
,因此在这里会看到它。通常来讲,列表中没有 *
的分支一般均可以用 git branch -d
来删掉。缘由很简单,既然已经把它们所包含的工做整合到了其余分支,删掉也不会损失什么。
另外能够用 git branch --no-merged
查看还没有合并的工做:
$ git branch --no-merged
testing
它会显示还未合并进来的分支。因为这些分支中还包含着还没有合并进来的工做成果,因此简单地用 git branch -d
删除该分支会提示错误,由于那样作会丢失数据:
$ git branch -d testing
error:The branch 'testing' is not an ancestor of your current HEAD.
If you are sure you want to delete it, run 'git branch -D testing'.
不过,若是你确实想要删除该分支上的改动,能够用大写的删除选项 -D
强制执行,就像上面提示信息中给出的那样。
【读后总结】
(1)tree df46f73f364fcbfa79b371bf3252339e815c2005
parent 0c195c0fb1f2f1198851afe46dd55be3c9e74cc8
parent 116da69fe1acced56e181018fb3fff43d64b65bf
author cposture <cposture@126.com> 1419081675 +0800
committer cposture <cposture@126.com> 1419081675 +0800
如今咱们已经学会了新建分支和合并分支,能够(或应该)用它来作点什么呢?在本节,咱们会介绍一些利用分支进行开发的工做流程。而正是因为分支管理的便捷,才衍生出了这类典型的工做模式,你能够根据项目的实际状况选择一种用用看。
因为 Git 使用简单的三方合并,因此就算在较长一段时间内,反复屡次把某个分支合并到另外一分支,也不是什么难事。也就是说,你能够同时拥有多个开放的分支,每一个分支用于完成特定的任务,随着开发的推动,你能够随时把某个特性分支的成果并到其余分支中。
许多使用 Git 的开发者都喜欢用这种方式来开展工做,好比仅在 master
分支中保留彻底稳定的代码,即已经发布或即将发布的代码。与此同时,他们还有一个名为develop
或 next
的平行分支,专门用于后续的开发,或仅用于稳定性测试 — 固然并非说必定要绝对稳定,不过一旦进入某种稳定状态,即可以把它合并到master
里。这样,在确保这些已完成的特性分支(短时间分支,好比以前的 iss53
分支)可以经过全部测试,而且不会引入更多错误以后,就能够并到主干分支中,等待下一次的发布。
本质上咱们刚才谈论的,是随着提交对象不断右移的指针。稳定分支的指针老是在提交历史中落后一大截,而前沿分支老是比较靠前(见图 3-18)。
或者把它们想象成工做流水线,或许更好理解一些,通过测试的提交对象集合被遴选到更稳定的流水线(见图 3-19)。
你能够用这招维护不一样层次的稳定性。某些大项目还会有个 proposed
(建议)或 pu
(proposed updates,建议更新)分支,它包含着那些可能尚未成熟到进入next
或 master
的内容。这么作的目的是拥有不一样层次的稳定性:当这些分支进入到更稳定的水平时,再把它们合并到更高层分支中去。再次说明下,使用多个长期分支的作法并不是必需,不过通常来讲,对于特大型项目或特复杂的项目,这么作确实更容易管理。
【读后总结】
(1)所谓三方合并前面已经说过,这里再总结一下。三方合并是属于前面讲的第二种状况:在未合并以前,多条分支有着共同的祖先;当master合并了某一条分支,因为该分支指向的提交对象位于master分支的直接下游,Git经过fast forword简单直接地将master右移到合并分支所在的提交对象,换句话说,若是顺着一个分支走下去能够到达另外一个分支的话,那么 Git 在合并二者时,只会简单地把指针右移,由于这种单线的历史分支不存在任何须要解决的分歧,因此这种合并过程能够称为快进(Fast forward),这个时候,Git没有生成新的提交对象,只是改变.git/refs/heads下master文件中的SHA-1值。此时,master分支和其余分支不像合并前有着共同的直接祖先了,它们有着一个交叉点,为间接的祖先,master分支合并某个分支时,通常会取合并分支所指向的commit、master所指向的commit和合并分支和master的共同祖先所指向的commit这三个对象合并为一个新的commit对象,并生成结果的文件快照,该文件快照是一些合并时被修改的文件快照。Git会自动选取合适的新建分支和master的共同祖先做为合并基础,这一点和CVS和subversion的人工选择不一样,极大的简化操做流程。
(2)分支流水线:主支是彻底稳定的流水线,主支下面有多支用于稳定测试或特性分支流水线。稳定的流水线老是落后于不稳定的流水线,不稳定的流水线中通过测试的提交对象达到某种稳定的状态,能够被遴选到更稳定的流水线,把它们合并到更高层的分支中去。
(3)在最稳定的master分支一般用于管理已经发布或即将发布的代码,在develop分支管理稳定测试的代码,特性分支管理实现某单一特性的代码。
在任何规模的项目中均可以使用特性(Topic)分支。一个特性分支是指一个短时间的,用来实现单一特性或与其相关工做的分支。可能你在之前的版本控制系统里从未作过相似这样的事情,由于一般建立与合并分支消耗太大。然而在 Git 中,一天以内创建、使用、合并再删除多个分支是常见的事。
咱们在上节的例子里已经见过这种用法了。咱们建立了 iss53
和 hotfix
这两个特性分支,在提交了若干更新后,把它们合并到主干分支,而后删除。该技术容许你迅速且彻底的进行语境切换 — 由于你的工做分散在不一样的流水线里,每一个分支里的改变都和它的目标特性相关,浏览代码之类的事情于是变得更简单了。你能够把做出的改变保持在特性分支中几分钟,几天甚至几个月,等它们成熟之后再合并,而不用在意它们创建的顺序或者进度。
如今咱们来看一个实际的例子。请看图 3-20,由下往上,起先咱们在 master
工做到 C1,而后开始一个新分支 iss91
尝试修复 91 号缺陷,提交到 C6 的时候,又冒出一个解决该问题的新办法,因而从以前 C4 的地方又分出一个分支iss91v2
,干到 C8 的时候,又回到主干 master
中提交了 C9 和 C10,再回到 iss91v2
继续工做,提交 C11,接着,又冒出个不太肯定的想法,从 master
的最新提交 C10 处开了个新的分支dumbidea
作些试验。
如今,假定两件事情:咱们最终决定使用第二个解决方案,即 iss91v2
中的办法;另外,咱们把 dumbidea
分支拿给同事们看了之后,发现它居然是个天才之做。因此接下来,咱们准备抛弃原来的iss91
分支(实际上会丢弃 C5 和 C6),直接在主干中并入另外两个分支。最终的提交历史将变成图 3-21 这样:
请务必牢记这些分支所有都是本地分支,这一点很重要。当你在使用分支及合并的时候,一切都是在你本身的 Git 仓库中进行的 — 彻底不涉及与服务器的交互。
【读后总结】
(1)一个特性分支是指一个短时间的,用来实现单一特性或与其相关工做的分支。在任何规模的项目中均可以使用特性(Topic)分支。每一个分支里的改变都和它的目标特性相关,浏览代码之类的事情于是变得更简单了。你能够把做出的改变保持在特性分支中几分钟,几天甚至几个月,等它们成熟之后再合并,而不用在意它们创建的顺序或者进度。
(2)上面特性分支的讲解:
1.从C1到C2为建立一个iss91分支,修改并提交而造成的。
2.从C6回到C4再到C7可使用Git的版本回退功能,具体命令为:
git reset --hard <SHA-1值>
SHA-1值查询方法使用下面命令
git reflog,查询版本历史
回到C4后可使用第一点的建立分支。
3.从C8到C9可使用Git的分支切换功能,命令为
git checkout <分支名>
4.最后的合并步骤:
假如当前分支是dumbidea,先切换当前分支到master分支,再分别合并dumbidea和iss91v2分支,具体命令:
git checkout master
git merge dumbidea
git merge iss91v2
远程分支(remote branch)是对远程仓库中的分支的索引。它们是一些没法移动的本地分支;只有在 Git 进行网络交互时才会更新。远程分支就像是书签,提醒着你上次链接远程仓库时上面各分支的位置。
咱们用 (远程仓库名)/(分支名)
这样的形式表示远程分支。好比咱们想看看上次同 origin
仓库通信时master
的样子,就应该查看 origin/master
分支。若是你和同伴一块儿修复某个问题,但他们先推送了一个iss53
分支到远程仓库,虽然你可能也有一个本地的 iss53
分支,但指向服务器上最新更新的却应该是 origin/iss53
分支。
可能有点乱,咱们不妨举例说明。假设大家团队有个地址为 git.ourcompany.com
的 Git 服务器。若是你从这里克隆,Git 会自动为你将此远程仓库命名为origin
,并下载其中全部的数据,创建一个指向它的 master
分支的指针,在本地命名为 origin/master
,但你没法在本地更改其数据。接着,Git 创建一个属于你本身的本地master
分支,始于 origin
上 master
分支相同的位置,你能够就此开始工做(见图 3-22):
若是你在本地 master
分支作了些改动,与此同时,其余人向 git.ourcompany.com
推送了他们的更新,那么服务器上的master
分支就会向前推动,而于此同时,你在本地的提交历史正朝向不一样方向发展。不过只要你不和服务器通信,你的 origin/master
指针仍然保持原位不会移动(见图 3-23)。
能够运行 git fetch origin
来同步远程服务器上的数据到本地。该命令首先找到 origin
是哪一个服务器(本例为git.ourcompany.com
),从上面获取你还没有拥有的数据,更新你本地的数据库,而后把 origin/master
的指针移到它最新的位置上(见图 3-24)。
为了演示拥有多个远程分支(在不一样的远程服务器上)的项目是如何工做的,咱们假设你还有另外一个仅供你的敏捷开发小组使用的内部服务器 git.team1.ourcompany.com
。能够用第二章中提到的git remote add
命令把它加为当前项目的远程分支之一。咱们把它命名为 teamone
,以便代替原始的 Git 地址(见图 3-25)。
如今你能够用 git fetch teamone
来获取小组服务器上你尚未的数据了。因为当前该服务器上的内容是你 origin
服务器上的子集,Git 不会下载任何数据,而只是简单地建立一个名为teamone/master
的分支,指向 teamone
服务器上 master
分支所在的提交对象31b8e
(见图 3-26)。
(远程仓库名)/(分支名)
这样的形式表示远程分支
origin
,并下载其中全部的数据,创建一个指向它的
master
分支的指针,在本地命名为
origin/master
,但你没法在本地更改其数据。接着,Git 创建一个属于你本身的本地
master
分支,始于
origin
上
master
分支相同的位置
origin/master
指针仍然保持原位不会移动
git fetch origin,将其下载下来
该命令首先找到 origin
是哪一个服务器(本例为git.ourcompany.com
),从上面获取你还没有拥有的数据,更新你本地的数据库,而后把 origin/master
的指针移到它最新的位置上
git remote add <仓库名>
git://github.com/<用户名>/<仓库名>.git
要想和其余人分享某个本地分支,你须要把它推送到一个你拥有写权限的远程仓库。你的本地分支不会被自动同步到你引入的远程服务器上,除非你明确执行推送操做。换句话说,对于无心分享的分支,你尽管保留为私人分支好了,而只推送那些协同工做要用到的特性分支。
若是你有个叫 serverfix
的分支须要和他人一块儿开发,能够运行 git push (远程仓库名) (分支名)
:
$ git push origin serverfix
Counting objects:20,done.
Compressing objects:100%(14/14),done.
Writing objects:100%(15/15),1.74KiB,done.
Total15(delta 5), reused 0(delta 0)
To git@github.com:schacon/simplegit.git
*[new branch] serverfix -> serverfix
这其实有点像条捷径。Git 自动把 serverfix
分支名扩展为 refs/heads/serverfix:refs/heads/serverfix
,意为“取出我在本地的 serverfix 分支,推送到远程仓库的 serverfix 分支中去”。咱们将在第九章进一步介绍refs/heads/
部分的细节,不过通常使用的时候均可以省略它。也能够运行 git push origin serverfix:serferfix
来实现相同的效果,它的意思是“上传我本地的 serverfix 分支到远程仓库中去,仍旧称它为 serverfix 分支”。经过此语法,你能够把本地分支推送到某个命名不一样的远程分支:若想把远程分支叫做awesomebranch
,能够用 git push origin serverfix:awesomebranch
来推送数据。
接下来,当你的协做者再次从服务器上获取数据时,他们将获得一个新的远程分支 origin/serverfix
:
$ git fetch origin
remote:Counting objects:20,done.
remote:Compressing objects:100%(14/14),done.
remote:Total15(delta 5), reused 0(delta 0)
Unpacking objects:100%(15/15),done.
From git@github.com:schacon/simplegit
*[new branch] serverfix -> origin/serverfix
值得注意的是,在 fetch
操做下载好新的远程分支以后,你仍然没法在本地编辑该远程仓库中的分支。换句话说,在本例中,你不会有一个新的serverfix
分支,有的只是一个你没法移动的 origin/serverfix
指针。
若是要把该内容合并到当前分支,能够运行 git merge origin/serverfix
。若是想要一份本身的 serverfix
来开发,能够在远程分支的基础上分化出一个新的分支来:
$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "serverfix"
这会切换到新建的 serverfix
本地分支,其内容同远程分支 origin/serverfix
一致,这样你就能够在里面继续开发了。
从远程分支 checkout
出来的本地分支,称为跟踪分支(tracking branch)。跟踪分支是一种和远程分支有直接联系的本地分支。在跟踪分支里输入git push
,Git 会自行推断应该向哪一个服务器的哪一个分支推送数据。反过来,在这些分支里运行 git pull
会获取全部远程索引,并把它们的数据都合并到本地分支中来。
在克隆仓库时,Git 一般会自动建立一个名为 master
的分支来跟踪 origin/master
。这正是git push
和 git pull
一开始就能正常工做的缘由。固然,你能够为所欲为地设定为其它跟踪分支,好比origin
上除了 master
以外的其它分支。刚才咱们已经看到了这样的一个例子:git checkout -b [分支名] [远程名]/[分支名]
。若是你有 1.6.2 以上版本的 Git,还能够用--track
选项简化:
$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "serverfix"
要为本地分支设定不一样于远程分支的名字,只需在前个版本的命令里换个名字:
$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "sf"
如今你的本地分支 sf
会自动向 origin/serverfix
推送和抓取数据了。
若是再也不须要某个远程分支了,好比搞定了某个特性并把它合并进了远程的 master
分支(或任何其余存放稳定代码的地方),能够用这个很是无厘头的语法来删除它:git push [远程名] :[分支名]
。若是想在服务器上删除serverfix
分支,运行下面的命令:
$ git push origin :serverfix
To git@github.com:schacon/simplegit.git
-[deleted] serverfix
咚!服务器上的分支没了。你最好特别留心这一页,由于你必定会用到那个命令,并且你极可能会忘掉它的语法。有种方便记忆这条命令的方法:记住咱们不久前见过的 git push [远程名] [本地分支]:[远程分支]
语法,若是省略 [本地分支]
,那就等因而在说“在这里提取空白而后把它变成[远程分支]
”。
把一个分支整合到另外一个分支的办法有两种:merge
和 rebase
(译注:rebase
的翻译暂定为“衍合”,你们知道就能够了。)。在本章咱们会学习什么是衍合,如何使用衍合,为何衍合操做如此富有魅力,以及咱们应该在什么状况下使用衍合。
请回顾以前有关合并的一节(见图 3-27),你会看到开发进程分叉到两个不一样分支,又各自提交了更新。
以前介绍过,最容易的整合分支的方法是 merge
命令,它会把两个分支最新的快照(C3 和 C4)以及两者最新的共同祖先(C2)进行三方合并,合并的结果是产生一个新的提交对象(C5)。如图 3-28 所示:
其实,还有另一个选择:你能够把在 C3 里产生的变化补丁在 C4 的基础上从新打一遍。在 Git 里,这种操做叫作衍合(rebase)。有了 rebase
命令,就能够把在一个分支里提交的改变移到另外一个分支里重放一遍。
在上面这个例子中,运行:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
它的原理是回到两个分支最近的共同祖先,根据当前分支(也就是要进行衍合的分支 experiment
)后续的历次提交对象(这里只有一个 C3),生成一系列文件补丁,而后以基底分支(也就是主干分支master
)最后一个提交对象(C4)为新的出发点,逐个应用以前准备好的补丁文件,最后会生成一个新的合并提交对象(C3’),从而改写 experiment
的提交历史,使它成为 master
分支的直接下游,如图 3-29 所示:
如今回到 master
分支,进行一次快进合并(见图 3-30):
如今的 C3’ 对应的快照,其实和普通的三方合并,即上个例子中的 C5 对应的快照内容如出一辙了。虽然最后整合获得的结果没有任何区别,但衍合能产生一个更为整洁的提交历史。若是视察一个衍合过的分支的历史记录,看起来会更清楚:仿佛全部修改都是在一根线上前后进行的,尽管实际上它们本来是同时并行发生的。
通常咱们使用衍合的目的,是想要获得一个能在远程分支上干净应用的补丁 — 好比某些项目你不是维护者,但想帮点忙的话,最好用衍合:先在本身的一个分支里进行开发,当准备向主项目提交补丁的时候,根据最新的origin/master
进行一次衍合操做而后再提交,这样维护者就不须要作任何整合工做(译注:其实是把解决分支补丁同最新主干代码之间冲突的责任,化转为由提交补丁的人来解决。),只需根据你提供的仓库地址做一次快进合并,或者直接采纳你提交的补丁。
请注意,合并结果中最后一次提交所指向的快照,不管是经过衍合,仍是三方合并,都会获得相同的快照内容,只不过提交历史不一样罢了。衍合是按照每行的修改次序重演一遍修改,而合并是把最终结果合在一块儿。
衍合也能够放到其余分支进行,并不必定非得根据分化以前的分支。以图 3-31 的历史为例,咱们为了给服务器端代码添加一些功能而建立了特性分支 server
,而后提交 C3 和 C4。而后又从 C3 的地方再增长一个client
分支来对客户端代码进行一些相应修改,因此提交了 C8 和 C9。最后,又回到 server
分支提交了 C10。
假设在接下来的一次软件发布中,咱们决定先把客户端的修改并到主线中,而暂缓并入服务端软件的修改(由于还须要进一步测试)。这个时候,咱们就能够把基于 server
分支而非 master
分支的改变(即 C8 和 C9),跳过 server
直接放到master
分支中重演一遍,但这须要用 git rebase
的 --onto
选项指定新的基底分支master
:
$ git rebase --onto master server client
这比如在说:“取出 client
分支,找出 client
分支和 server
分支的共同祖先以后的变化,而后把它们在master
上重演一遍”。是否是有点复杂?不过它的结果如图 3-32 所示,很是酷(译注:虽然 client
里的 C8, C9 在 C3 以后,但这仅代表时间上的前后,而非在 C3 修改的基础上进一步改动,由于server
和 client
这两个分支对应的代码应该是两套文件,虽然这么说不是很严格,但应理解为在 C3 时间点以后,对另外的文件所作的 C8,C9 修改,放到主干重演。):
如今能够快进 master
分支了(见图 3-33):
$ git checkout master
$ git merge client
如今咱们决定把 server
分支的变化也包含进来。咱们能够直接把 server
分支衍合到 master
,而不用手工切换到 server
分支后再执行衍合操做 — git rebase [主分支] [特性分支]
命令会先取出特性分支server
,而后在主分支 master
上重演:
$ git rebase master server
因而,server
的进度应用到 master
的基础上,如图 3-34 所示:
而后就能够快进主干分支 master
了:
$ git checkout master
$ git merge server
如今 client
和 server
分支的变化都已经集成到主干分支来了,能够删掉它们了。最终咱们的提交历史会变成图 3-35 的样子:
$ git branch -d client
$ git branch -d server
呃,奇妙的衍合也并不是天衣无缝,要用它得遵照一条准则:
一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行衍合操做。
若是你遵循这条金科玉律,就不会出差错。不然,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。
在进行衍合的时候,实际上抛弃了一些现存的提交对象而创造了一些相似但不一样的新的提交对象。若是你把原来分支中的提交对象发布出去,而且其余人更新下载后在其基础上开展工做,而稍后你又用git rebase
抛弃这些提交对象,把新的重演后的提交对象发布出去的话,你的合做者就不得不从新合并他们的工做,这样当你再次从他们那里获取内容时,提交历史就会变得一团糟。
下面咱们用一个实际例子来讲明为何公开的衍合会带来问题。假设你从一个中央服务器克隆而后在它的基础上搞了一些开发,提交历史相似图 3-36 所示:
如今,某人在 C1 的基础上作了些改变,并合并他本身的分支获得结果 C6,推送到中央服务器。当你抓取并合并这些数据到你本地的开发分支中后,会获得合并结果 C7,历史提交会变成图 3-37 这样:
接下来,那个推送 C6 上来的人决定用衍合取代以前的合并操做;继而又用 git push --force
覆盖了服务器上的历史,获得 C4’。而以后当你再从服务器上下载最新提交后,会获得:
下载更新后须要合并,但此时衍合产生的提交对象 C4’ 的 SHA-1 校验值和以前 C4 彻底不一样,因此 Git 会把它们看成新的提交对象处理,而实际上此刻你的提交历史 C7 中早已经包含了 C4 的修改内容,因而合并操做会把 C7 和 C4’ 合并为 C8(见图 3-39):
C8 这一步的合并是早晚会发生的,由于只有这样你才能和其余协做者提交的内容保持同步。而在 C8 以后,你的提交历史里就会同时包含 C4 和 C4’,二者有着不一样的 SHA-1 校验值,若是用git log
查看历史,会看到两个提交拥有相同的做者日期与说明,使人费解。而更糟的是,当你把这样的历史推送到服务器后,会再次把这些衍合后的提交引入到中央服务 器,进一步困扰其余人(译注:这个例子中,出问题的责任方是那个发布了 C6 后又用衍合发布 C4’ 的人,其余人会所以反馈双重历史到共享主干,从而混淆你们的视听。)。
若是把衍合当成一种在推送以前清理提交历史的手段,并且仅仅衍合那些还没有公开的提交对象,就没问题。若是衍合那些已经公开的提交对象,而且已经有人基于这些提交对象开展了后续开发工做的话,就会出现叫人沮丧的麻烦。
读到这里,你应该已经学会了如何建立分支并切换到新分支,在不一样分支间转换,合并本地分支,把分支推送到共享服务器上,使用共享分支与他人协做,以及在分享以前进行衍合。