Git 分支模型
16 Feb 2015我会在这篇文章中为大家阐述一个开发模型, 大约一年前, 我就已经在我所有的项目中引入了它, 并且已经被证明这是一个很成功的模型. 其实我很久之前就想写这篇文章了, 但是始终没找到合适的时间来完成它, 现在终于可以了. 我不会提及任何项目的细节, 而仅仅似乎关于分支策略与发布管理.
首先, 所有的源码的版本管理都是基于Git的.
为什么选择Git?
Git较之于集中化的源码控制系统的优缺点, 详见此处, 以及这儿. 那里有着大量的口水战. 作为一个开发人员, 此时我更倾向于Git. Git真正的改变了开发人员对于代码合并与分支的认识. 在传统的CVS/SVN世界里, 合并/分支总是有点可怕(“小心合并冲突, 他们会咬你!”) 并且是偶然才会去做的事情.
但是在Git中, 这些操作则相当的廉价且简便, 并且会是每日工作中核心的部分. 例如, 在 CVS/SVN的书籍中, 首次提到分支与合并是在靠后的章节(针对高级用户), 然而在每本Git书籍内, 通常在第三节(基础部分)它就已经被提及了.
由于其简单性及可重复性, 分支与合并将不再是令你惧怕的事情. 版本控制工具应优先的协助分支/合并.
关于工具的介绍以及差不多了, 让我们把注意力转到开发模型上吧. 我将要呈现的这个模型本质上只是一系列的过程, 这些过程每个团队成员都应遵守以保证软件开发流程受控.
分散化却又集中
有利于分支模型的代码仓库的设置, 会包含一个中心的仓库. 注意, 这个仓库只是被认定为是中心仓库(由于Git是一个分布式版本控制软件, 在技术层面上是不存在中心仓库的). 我们通常以origin作为这个仓库的名字, 因为所有的Git用户都对它很熟悉.
每个开发人原都会在origin上拉取和推送代码. 但是除了集中化的推送-拉取关系, 开发人员也可以从其他同事那拉取变更, 从而形成子团队, 在不应过早的给origin推送还在开发中的产品之前, 这点对于多人的团队协作是很有帮助的. 在上面的图中, 子团队有: Alice 和 Bob, Alice 和 David, 还有 Clair 和 David.
技术上来说, 无非就是Alice定义了一个名为Bob的Git的远程仓库, 指向Bob的仓库, 反之亦然.
主干分支
核心层面, 这个开发模型是深受已经存在的模型启发的. 中央仓库持有2个拥有无限寿命的主干分支:
- master
- develop
对于orign上的master分支, 每个Git用于都应该是很熟悉的. 与之并存的是一个名为develop的分支.
orgin/master是一个源码HEAD标识总是对应生产环境状态主干分支.
orgin/develop则是源码HEAD标识总是对应这即将发布版本的最新变更状态的主干分支. 一些人也称之为”集成分支”. 它就是每日自动构建的代码源.
当develop分支的源码达到一个稳定点并且已经可发布, 所有的变更都应被合并到master分支并且添加发布序号的标记. 关于这个步骤, 会在之后详细讨论.
因此, 每当变更需要合并回master时, 都是生产环境上一个新的的发布. 对此我们往往是十分严格的, 所以理论上, 在每次在master上的提交, 都可以使用一个Git钩子脚本来自动的构建并向生产环境转出软件.
辅助分支
紧接着master和develop分支, 开发模型使用了多种辅助分支来帮助团队成员间的并行开发, 减轻功能的跟踪, 生产环境发布的准备以及协助快速修复生产环境的问题. 与主干分支不同的是, 这些分支的生命时长总是有限的, 最终它们都会被移除.
不同类型的分支包括:
- 功能分支
- 发布分支
- 修复分支
每个分支都有一个特定的目的并且有严格的规则来确定他们的源分支以及合并的目标分支. 我们会简单的了解一下它们.
从技术层面上来说, 这些”特殊”的分支并不是特殊的. 分支的类型只是按我们如何使用它们来划分的. 它们都只是简单的Git分支.
功能分支
分支可能基于:
develop
分支必须合并到:
develop
分支命名规则:
除了master, develop, release-或hotfix-以外的任何字符
功能分支(有时也称为主题分支)被用于开发新的功能, 针对于即将到来的或一次独立的功能的版本发布. 当开始开发新功能时候, 它要被整合进的目标发布版本可能是位置的. 功能分支的本质是它会在功能开发过程中一直存在, 但是最终会被合并到develop(确定在下一个版本发布中添加这个新功能)或者废弃.
功能分支通常只存在于开发人员的仓库中, 而不在origin内.
创建功能分支
当开始开发新功能时, 从develop建立分支
$ git checkout -b myfeature develop
Switched to a new branch "myfeature"
合并已完成的功能
已完成的功能被合并到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标记使合并总会创建一个新的提交, 即使合并可以以快速前进实现. 这避免了功能分支历史记录的丢失并且将所有添加这个功能的提交组合在一起. 对比:
在后面的情况里, 从Git的提交历史记录里想要看到实现的功能是不可能的, 你只能人工阅读所有的日志信息. 对整个功能(比如, 一组提交)的回退, 在后面的情况里真的很头疼的一件事, 然而如果使用–no-ff标记则变的相当简单.
是的, 它会创建更多的(空的)提交, 但是收获却远大于成本.
不幸的是, 我还没找到一种方法能使得–no-ff变成git merge默认的行为, 但是它确实应该是那样的.
发布分支
分支可能基于:
develop
分支必须合并到:
develop 和 master
分支命名规则:
release-*
发布分支旨在为准备新的生产环境版本发布. 它们必须考虑到最后时刻的细节. 此外, 它们还需要考虑到小型bug的修复以及版本发布中元数据(版本号, 构建日期等)的准备. 在一个发布分支上完成这些工作, 则develop分支可只关注下次大规模版本发布的功能.
当develop(几乎)反映了新版本发布所需的状态时, 就是基于develop分支建立发布分支的关键时刻. 至少在那时, 所有需要发布的目标功能必须合并到develop分支. 所有未来版本发布中的目标功能则不是这样, 它们必须等到发布分支被创建之后.
发布分支开始时 即将到来的版本获取分配到的版本号-不会更早.
即将发布正是在发布分支的开始阶段才被分配一个版本号-不会更早. 直到这一时刻, develop分支才反映”下次发布”的变更, 但是”下次发布”是否将最终成为0.3还是1.0是不明确的, 直到发布分支开始. 这点会在发布分支开始时候确定, 并且依据项目中版本号提升的规则进行实施.
创建发布分支
发布分支是基于develop分支创建的. 举个例子, 1.1.5是当前生产环境的版本号, 我们即将有一次大规模的版本发布, develop分支的已经是”新版本发布”就绪状态, 我们决定版本会变为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的修复可能被应用到这个分支上(而不是develop分支). 添加大型的新功能是被严格禁止的. 它们必须被合并到develop上, 因此, 还是等下个版本发布吧.
完成发布分支
当发布分支变为真正的发布就绪状态时, 有些动作则需要进行实现. 首先, 发布分支被合并到master(因为每次master上的提交都是一次新的版本发布, 切记). 接下来, master上的提交必须进行标记, 以便于将来参考历史版本. 最终, 发布分支上的变更需要合并到develop上, 以便于将来的功能分支也能包含这些bug的修复.
Git中的前两个步骤:
$ 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
标记来加密记录你的标记.
为了保留发布分支中的变更, 需要把变更合并到develop中. 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).
修复分支
分支可能基于:
master
分支必须合并到:
develop 和 master
分支命名规则:
hotfix-*
修复分支与发布分支是很相似的, 它也是为了新的生产环境版本发布而准备的. 由于需要立即解决生产环境版本上的异常状态, 修复分支应运而生. 当生产环境有一个严重BUG必须立即解决时, 修复分支就会基于生产环境对应的master分支上的标记进行创建.
其本质其实就是团队成员的工作(develop分支上)可以持续, 而另一人则可同时准备一个快速的生产环境的修复.
创建修复分支
修复分支创建于master分支. 举个例子, 1.2是当前运行的生产环境的版本, 一个严重的bug造成了一些麻烦. 但是develop分支上的变更还不稳定. 因此我们或许可以创建一个修复分支, 然后开始修复这个问题:
$ 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分支, 为了保证本次修复也包含在下次发布中, 同时也需要合并到develop分支. 这与发布分支是完全一样的.
首先, 更新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
标记来加密记录你的标记.
接下来, 在develop分支中包含上此次修复:
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
这个步骤唯一的例外是: 当前发布分支已经存在了, 修复的变更需要合并到这个发布分支, 而不是develop分支. bug的修复合并到发布分支, 在发布分支完成时, 也会导致这次修复最终合并到develop中. (如果修复需要在develop内立即生效并且等不及到发布分支完成, 也可以现在就合并修复到develop分支.)
最终, 移除临时分支:
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).
总结
这个分支模型其实并没有什么令人震撼的新知识, 本文开始的”高清无码大图”已经被证明在我们的项目中是非常有用的了. 它形成了优雅的模型, 它易于理解且使得团队成员在分支和发布流程上达成了共识.
注:原文出处A successful Git branching model
comments powered by Disqus