在这一篇文章中,我会来介绍一个我一直在使用的开发模型,这个模型从一年前,我就使用在我所负责过的几个项目中(有公司的,有私人的),幸运的是,这些项目后来都被证明是很成功的,其实,很长时间以来,我都在考虑将这个模型用文章描述出来,但是一直没有机会,在文章中,我不会涉及到项目的任何细节,仅仅是谈项目中的分支场景的设置和发布的管理。

下面的描述中,我重点会围绕使用Git来作为源代码的版本管理工具。
为什么使用Git?
要看Git方式和集中的源代码版本控制的具体的分析,请查阅网站的具体的例子,但是,请注意的,这2个阵营也一直在讨论,甚至战争(多是口水战),作为一个开发者,我觉得Git是这些工具中最好用,最值得推荐的。Git彻底改变了程序员关于合并和分支的想法。在之前一直使用传统的CVS/SVN的时候,合并/分支一直意见非常头疼的事情(合并的时候总是有不断的冲突,这些问题会不断的出现,这真是让人太恶心了),还有一些别的事情,有可能从头到尾,你只会执行一次。
但是,如果使用Git的话,你会发现,上面说的这些动作是如此自然的就发生了,甚至是如此的便宜和简单,而且,这会变成你每天工作的一个核心部分。举例来说吧,在相关的CVS/SVN的书籍中,分支和合并是在书籍的最后一张才会被介绍(介绍给高级用户,因为它们假定一般的用户是不会用到这部分功能的),但是,反观Git的书籍中,一般到了第三章的时候,就会介绍这些内容(因为对Git用户来说,分支合并是最基本的技能)。
因为它的简单性和重复性,分支和合并将永远不是一件让人头疼的事情。版本控制的工具中关于合并和分支的功能设计往往都比别的功能要重要的多。
关于工具的讨论已经足够了吧,我们开始谈论我们今天要讨论的重点吧,Git开发模型,我将在这里提出的模型基本上是一个方法论,程序开发中的一个流程组合,每个团队成员都必须遵循,以来管理软件开发过程中。
分散且集中的管理模式
我们使用的这个仓库,可以看做是一个“真实”存在的中央仓库,它可以很好的与我们这一套开发模型很好的工作。但是请注意,这个仓库仅仅是被理解为中央仓库而已,因为Git是一个DVSC,在技术层面来说,其实并不存在这样的中央仓库。我们一般称为origin
,这个名称对所有的Git用户来说都要是很容易理解的(其实可以理解为GitHub中的master分支)。

每个开发人员都是对原始分支进行pull和push操作。但是除了中心化的push-pull操作关系外,每一个程序员有可能还会对其他的分支进行操作,来形成子(小)团队。比喻说,在与其他(多个)的程序员合作来进行一个大的新的功能开发的时候,就需要先对子分支做操作,然后对主分支做操作,在这种情况下可能就会出现小团队,Alice和Bob,Alice和Davic,Clair和David。
技术上,其实上面的操作是没有任何问题的,其实就是,Alice定义了一个远程Git分支,命名为bob,指向了Bob在操作的仓库,反之亦然。
主分支

在核心部分,开发模型还是跟目前的模型有很大的相同点。中心的仓库还是有2个大的主分支,代表了各自的生命周期:
- master
- develop
master分支对所有的Git用户都是友好的,熟悉的,与master分支对应的平行的,另一个分支叫做develop分支。
我们可以理解origin/master
是一个主分支,这个主分支的源代码的HEAD
信息永远代码一个随时可发布版本状态。
我们指定origin/develop
分支是一个包含下次版本发的时候需要包含的交付功能的HEAD信息的分支。有的人会称呼这个分支另一个名字“集成分支”. 在这个分支上,每日构建会被执行,也会进行一些集中的单元测试。
当develop分支中的代码达到了一个稳定可以发布的状态的时候,所有的代码需要进行合并到master分支,并被打上标签,标记发布版本。以上的操作,我们在之后会进行更详细的介绍。
因此,所有的修改被合并到master
分支之后,一个产品的发布就被定义出来了。这个部分需要进行仔细而严格的处理,所以理论上,一旦有对master的commit操作的话,我们可以使用一个Git的hook(钩子)程序来进行自动构建,推送我们的软件到生产服务器(Production Server)上。
支持与协作分支(Supporting branches )
在master和develop分支之外,还有另外一个类型的技术支持分支,我们的开发模型中定义了多个这种技术支持分支,用来帮助团队成员间进行并行的开发,跟踪各种产品功能的执行状况,准备发布新的版本,并随时准备快速的处理生产系统中的问题。与主分支不同的是,这种类型的分支的生命周期往往是有限的,最终都是要被移除掉的。
关于支持协作的分支,我们又可以分为下面几种不同的类型:
- 特性分支(Feature branches)
- 发布分支(Release branches)
- 快速修复分支(Hotfix branches)
每一个分支产生都一个非常特定的目标(这个分支的产生是被严格定义的),每一个分支的源分支和可以被合并到的目标分支都需要被严格的定义。
但是,要分清楚的是,其实在技术层面上来说的,这些分支并没有什么特殊的地方,只是这种类型的分支规定了,我们将会如何的使用它们。他们其实还是非常普通的Git分支,非常普通。
特性分支(Feature branches )

可以从什么地方产生: develop
可以合并到什么分支: develop
分支命名规范: 不能使用master,develop,realse-,hotfix-,其他的都是可以的。
特性分支(有的时候称呼为标题分支),用来进行一个功能特性的开发或者一个很久才会发布的功能集而进行设置。开始一个特性的开发工作的时候,该特性功能会被哪一次发布中包含其实是不确定的,无法预知的。这个分支的存在会一直持续到这个特性的开发结束且并合并到了develop分支(定义一个计划好的发布的版本)或discard分支(如果发现开发的功能感觉很不好,决定丢弃的分支)。
特性分支一般存在于开发者的仓库中,并不会出现在origin里。
创建一个特性分支
我们从develop分支中,创建一个新的特性分支
$ git checkout -b myfeature develop
Switched to a new branch "myfeature"
合并结束的特性分支到develop分支
结束的特性分支合并到develop分支后会被最终合并到接下来的发布中
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff myfeature
Updating ea1b82a..05e9557
(Summary of changes)
$ git branch -d myfeature
Deleted branch myfeature (was 05e9557).
$ git push origin develop
--no-ff
标记会让merge操作每次都会创建一个新的commit对象,即使merge操作是在fast-forward的时候也会执行。这样会避免丢失关于功能分支的存在性的历史记录信息,而且会在提交时候附加所有的commit信息。
比较图:

在图片的右边的情况下处理中,在之后的查看Git的历史纪录的时候,不能再看到在实现一个功能的时候提交的对象信息了,如果你真的想要看到这些信息的话,只能去手动的读取所有的日志消息信息,回滚(还原)整个的功能(比喻说一个组的提交),右边的做法来说,简直说一件极其头疼的事情,但是如果使用来 --no-ff 比较的话,事情就变的简单多来。
当然,有的时候,我们确实会创建很多的空的提交对象,但是没有日志所带来的代价要大的多的多。
发布分支(Release branches )
分支的由来: develop
可以合并回到的分支: develop and master
分支命名规范: release-*
发布分支用来准备为了产品的发布而做的准备。这些分支允许做项目上线前的确认和否定,而且这些分支还支持做一个小规模的Bug修复或者为发布而准备一些元数据信息(meta-data),比喻说,版本号,构建日期啊,等等。通过在发布分支上进行这些操作,develop分支被清理,来接受下次大的版本发布。
从develop分支创建一个发布分支的关键时刻,是当预定的发布目标已经实现,或接近实现的时候。至少所有的发布所要求包含的功能,需要在这个合并develop分支中(用来创建发布分支的时候包含),那些之后发布时候才需要包含的功能,可以在这次做发布分支的时候不合并,这些功能需要在创建完发布分支的时候,才被合并。 只有当下一次发布计划出来,然后创建发布分支的时候,才需要为发布指定一个版本,这不需要很早来进行定义。在这个时刻之前,开发分支只需要知道是为了下个版本的发布在开发,而不需要知道下一个版本是什么,下一个版本有可能是0.3也有可能是1.0 。只需要在创建release的时候决定就好了,而且这个确实依赖于项目中关于发布版本的相关的规定。
创建一个发布分支
发布分支是从开发分支上进行创建的,比喻说,Version1.1.5 是当前产品版本,我们在这次的开发中有一个大版本升级,当前的开发状态已经达到了下一个发布的要求了,并且我们做决定,我们已经叫做1.2版本,而不是1.1.6或者2.0版本,那么我们在创建分支的时候,就需要使用决定的版本来对分支进行定义。
$ git checkout -b release-1.2 develop
Switched to a new branch "release-1.2"
$ ./bump-version.sh 1.2
Files modified successfully, version bumped to 1.2.
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d9424] Bumped version number to 1.2
1 files changed, 1 insertions(+), 1 deletions(-)
上面的操作,我们是创建了一个新的分支,并切换到了这个分支,我们定义了一个版本号,代码中的 bump-version.sh 是一个虚构出来的Shell脚本,主要负责将需要对应的进行变化的文件做版本标记(当然在你的项目中也许这个工作是手动完成的,其实这无关紧要)。 这个新的分支有可能会存在一段时间,直到这个发布的版本被最终推出。在这个期间,Bug的修复功能依然后在这个分支上进行(而不是在开发分支上进行),但是切记,在这个分支上进行大的功能的修改或操作是绝对不允许的,因为所有的修改都需要合并会开发分支,所以,如果有这样的需求,下次发布的时候再考虑吧。
结束一个发布分支
当一个发布分支最终变成一个真实可以发布的版本的时候,有一些操作必须执行。首先,发布分支需要合并到master分支上(所以其实,对master的每一次commit都是准备要发布的),然后,合并到的master上之后,需要为这次合并发布做标签进行标记,以备之后查看历史的时候,快速的找到,最后,所有在发布分支上进行的操作都需要合并回开发分支去,因为在发布分支上也许有关于bug的修复工作,需要在下次开发分支变成发布分支的时候需要包含。
第一步和第二步可以使用下面的语句来完成。
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2
发布到目前为止已经结束了,而且我们为之后的查看做了标签标记。
提醒:你可以使用 -s 或 -u 的方式对标签信息进行加密处理。
为了保留在发布分支上的修改,我们最后,需要将修改合并回开发分支,在Git中我们需要按照下面的步骤
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)
上面的步骤也许会带来一次合并操作,如果真的出现的话,也只能进行修改,并且实现最终的提交。 处理接收,我们就可以最终移除这个分支了,因为我们已经不在需要这个分支了。
$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).
快速修复分支(Hotfix branches )
分支可以从何处而来: master
需要合并回: develop and master
分支命名规范: hotfix-*
快速修复分支与发布分支在很多方面是类似的,他们都是为了实现一次新的产品发布,尽管快速修复是计划外。这种类型的分支只在已上线产品的功能出现了之前未覆盖的功能测试引发的系统运行错误的时候才会被创建出来。一个产品版本中出现的严重的系统问题当然是需要马上被解决的,快速修复分支可能是根据在master分支上做的标签来创建,快速的定位到问题出现的分支。

其实质是团队成员的工作(在开发分支)可以继续,而另一个人正在准备一个快速修复。
创建快速修复分支
快速修复分支是从master分支创建的,比喻说,当前的版本号是1.2,也是线上运行的版本,目前在系统中发现了一个错误,需要马上解决,但是,在开发版本上的其他的开发工作还是不稳定不可以发布的,我们就可以创建一个快速修复分支来解决这个问题:
$ git checkout -b hotfix-1.2.1 master
Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
Files modified successfully, version bumped to 1.2.1.
$ git commit -a -m "Bumped version number to 1.2.1"
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1
1 files changed, 1 insertions(+), 1 deletions(-)
不要忘记在创建分支后需要进行版本上的调整。 然后需要快速的修改系统中存在的Bug,然后使用一次或多次提交来完成版本更新。
$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)
结束一个快速修复分支
完成Bug的修复之后,我们需要将快速修复分支中的修改合并回master分支,同样需要合并回开发分支,确保在下一次版本发布的时候,已经包含了bug的修复,这一系列的操作跟发布分支的操作是类似的。
首先,更新master分支,标记发布
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2.1
提醒:你可以使用 -s 或 -u 的方式对标签信息进行加密处理。
接下来,合并bug修复会开发分支
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
这里可能要产生的一个异常情况是,有可能是发布分支已经出现了,我们又产生了一个快速修复分支,要记住,现在同样需要将我们的修改合并到发布分支中,而不是开发分支。我们首先将Bug修复合并到发布分支中,之后,发布分支在进行功能修复合并的时候,会一起将修改合并到开发分支上(但是,如果开发分支上需要这部分的修改,而不能等到发布版本的合并的话,也可以进行合并到develop分支上,但是要小心哟)。
最后,移除这个临时分支
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).
总结
其实这个开发模型中,并没有什么特别新的让人振奋的新概念,只不过,图片中样式的关于开发过程的规定,确实在我们的项目中帮助到了我们,让我们的开发工作变的流程化,执行的很顺利。这有利于我们团队形成一个优雅的心智模式,很容易理解和允许团队成员开发分支和释放过程的共同理解。