说明及原文出处
- 原文链接
- 作者:Vincent Driessen
- 本文没有任何商业目的,也禁止任何商业目的的使用,转载等
- 如有侵权,请联系删除
反思后记(2020/3/5)
在git形成没多久时间,这个分支模型是在2010年构思出来的,现在已经过去10年了。在这10年里,git-flow(这篇文章提议的分支模型)变的越来越流行,越来越多软件团队把它视为一种标准的方式,但这也可能是一种教条或者万能药。
在这10年里,git已经席卷了整个世界,并且基于git开发的最流行的软件的类型正在向web apps类型转变--至少在我的留言版中是这样的。Web apps是典型的持续交付形式,没有回滚,也不用同时支持多个在wild环境中运行的软件版本。
这已经不是10年前我在博客里写这篇文章时我认为适合的软件类型了。如果你的团队正在做持续交付的软件开发时,我建议采用更简单一点的工作流(如 GitHubflow)而不是尝试非要在团队内用git-flow。
如果你正在构建明确的版本化软件或者你需要支持在wild环境中运行多版本软件,那么git-flow可能仍然很好适合你的团队,毕竟它已经介绍给大家有10年的时间了。这种情况下,可以接着阅读下面的文章。
总的来说,一定要记住,万能的解决方案是不存在的。根据自己的项目情况考虑,不要厌烦,为自己的项目选择合适的方案。
介绍
在这篇博文中我将展示我在一年前已经导入到我项目(工作的或者私人的)中的开发模型,这些项目的结果证明这个开发模型是成功的。我打算写一篇介绍这个开发模型的介绍有一段时间了,但是一直没有时间彻底地写出来。直到现在,我也没时间介绍项目的细节,仅仅是介绍项目的分支策略和发布管理。
branch strategy
为什么选择git
关于git相对于集中式源代码版本控制系统的优缺点的完整讨论,可以参考链接。网页上有很多关于这个的争论。作为开发者,在所有工具中,我更倾向于git。git 确实改变了开发者merging 和 branching 的方式。我曾经使用过的 CVS/Subversion 的情形,merging/branching 一直式比较可怕的(“注意合并的冲突情况,它们会很让你头疼的”)并且另外一些你不会经常用到的命令也会这样。
但是Git就不一样了,上面提到的那些操作就很简单,容易上手,并且那些操作被认为是日常工作流的核心部分之一。比如,在 CVS/Subversion 的书里,后面的章节(对于高阶用户)才会讲解merging和branching。但是在git的每一本书里,都会在第三章(基础章节)讲到merging和branching。
简化这些操作以后,branching和merging不在是可怕的事情。版本控制工具理应在branching和merging这些操作上比其他操作有更大的辅助作用。
版本管理工具的事情就说到这。让我们更多的关注开发模型的事情。我在这里展示的本质上不过是为了管理软件开发过程每个团队成员都需要遵守的一套规程。
分散但是集中
我们使用的软件仓库设置和分支模型一起工作的很好,它具有一个中心的“truth”仓库。注意这个中心仓库只是概念上的中心(因为Git是一个分布式VCS,技术层面上没有中心的仓库)。我们将此仓库作为“origin”,Git的用户对此名字都很熟悉。
repo
每个开发者 pull 和 push 提交到 origin 上。除了集中式push/pull外,每个同事还可以从其他同事那里pull changes来组成子团队。比如,这在两个或更多的开发者合作开发一个比较大的特性时,在进展中的工作还不适合push到origin时可能会很有用。在上面的图示中,有Alice和Bob,Alice和David,Clair和David子团队。
技术上讲,可以认为Alice定义了一个Git名叫bob,指向Bob的仓库的远程仓库。反之亦然。
主分支
在核心层面上,开发分支模型很大程度上受现有模型的启发。集中式仓库有两个有无限生命周期的主分支:
- master
- develop
origin仓库的master分支对Git的使用者来说都很熟悉。 平行于master分支的还有一个叫develop的分支。
main branch我们将origin/master视为HEAD指向的源代码反应的是一个已经做好产品化准备的状态的主分支。
我们将origin/develop视为HEAD指向的源代码反应的是已经包含下一次版本发布需要的修改的主分支。有时候也称之为“集成分支”。nightly builds就是从这个分支上构建出来的。
当develop分支的源代码达到了一个稳定状态并且准备好发布了,develop分支的所有提交都应该以某种方式合并到master分支,并且在合并后的master分支上打上release的版本号。具体如何操作,后面会讨论。
因此,每次有新的改动合并到master分支,都会有相应的release版本。我们倾向于严格执行这项规则,因而理论上每次master分支有commit合入,我们都可以使用Git hook脚本自动的构建并将相应的软件放到产品服务器上。
支持分支
除了master和develop的主分支以外,我们的开发模型里使用多种的支持分支来帮助团队成员之间的并行开发,简化特性开发的跟踪过程,准备版本的发布以及协助快速修复上线产品问题。和主分支不同,这些支持分支都是有限的生命周期,它们最终会被移除。
我们目前使用的不同的分支类型:
- 特性分支(Feature branches)
- 发布分支(Release branches)
- Hotfix分支(Hotfix branches)
上面的每个分支都有特定的用途和严格的规则。至于哪个分支会作为源分支拉出新的分支,那个分支会作为目标分支合入其他分支的改动。下面会有具体的说明。
从技术角度来看,这些分支决不是“特殊的”。这些分支的类型是根据我们的使用方式分类的。它们也是普通的Git分支。
特性分支(Feature branches)
特性分支:
- 从 develop 拉取出来
- 一定要合并回到 develop 分支
- 分支命名规则:除了 master,develop,release-*,或者 hotfix-* 之外的名字
特性分支(有时也称为主题分支)主要用来开发为即将发布的或者一段时间以后发布的版本准备的新特性。当特性还在开发阶段时,它会被包含到哪一个发布版本中可能还处于不确定的状态。特性分支的本质是在特性开发过程中会一直存在,但最后被合并回develop分支(在即将发布的版本中确定添加该新特性)或者被丢弃(特性实验结果不理想)。
特性分支只存在在开发者的仓库中,在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
使合并操作会创建一个新的commit object,即使合并操作可以执行 fast-forward
。这样可以避免丢失关于特性分支的历史存在性的信息并将添加特性的所有提交进行分组。--no-ff
与 fast-forward
对比如下:
pic:git merge --no-ff
在后面的例子中,从Git历史中是不可能看出哪些commits一起实现了一个特性,你只能手动阅读所有log信息。在后面的情形中,回退一整个特性(即一堆commit)是非常头疼的,然而使用 --no-ff
标识合并分支以后就很容易实现。
--no-ff
标识它可能会创建更多的(空)commit,但是带来的收益明显大于其弊端。
发布分支
- 可能从 develop 分支拉出
- 一定要合并回 develop 和 master 分支
- 分支命名规范:release-*
发布分支为新产品发布准备提供支持。它允许做版本的最终额一些细微修改(They allow for last-minute dotting of i’s and crossing t’s),此外,它允许小错误的修复,为发布准备基本数据(版本号,构建日期,等)。通过拉出发布分布,在发布分支上做发布准备工作,develop分支可以继续接收为下次发布开发的特性的提交。
当开发的状态已经基本上反应出新发布版本所期望的状态时是从develop分支拉取发布分支的关键时点。至少在拉出分布分支时要在分布版本中出现的所有特性都已经及时合入到develop分支。所有未来发布(不是此次发布的内容)的特性都不要合入develop,这些特性必须等待此次发布分支拉出完以后在合入。
正好在发布分支的拉出以后,即将发布的版本才会被分配一个版本号——而不是发布分支拉出之前就分配版本号。在此之前,develop分支反映了“下一个版本”的修改,但是此时还不清楚“下一个版本”最终是0.3还是1.0,直到发布分支拉出以后才能确认。版本号是在发布分支的拉出时决定并且由项目的版本号规则产生的。
创建发布分支
发布分支是从develop分支创建的。例如,假设版本1.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修复可能应用于此分支(而不是develop分支)。向发布分支添加大的新特性是严格禁止的。它们必须合并到develop分支中,等待下一个大的发布版本。
结束发布分支
当发布分支的状态已经达到可以发布的要求时,需要执行一系列的操作。
- 首先,发布分支被合并到master分支中(记住,规则要求master分支上的每一次提交都是一个新版本)。
- 其次,合并到master分支的的提交必须打tag,以便将来轻松的引用这个历史版本。
- 最后,在发布分支上所做的更改需要合并回develop分支中,以便将来的发布版本也包含这些错误修复。
前两步在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
该发布版本现在已经完成,并已打上tag以备将来的引用。
编注:也可以使用
-s
或-u <key>
参数以密码方式对tag签名。
为了保持发布分支中所做的更改,我们需要将它们合并回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).
Hotfix分支
- 从master分支拉出
- 必须合并回master和develop分支
- 分支命名规范:hotfix-*
hotfix分支与发布分支非常相似,因为它们也是为新的产品发布做准备,尽管是计划外的。它们因为live产品版本的不符合预期的状态必须立即采取修复的必要性而产生。当产品版本中的严重错误必须立即解决时,从master分支中相对应的发布版本的tag处拉出hotfix分支。
hotfix分支的实质是当另一个成员正在准备产品的快速修复方案时,团队成员(在develop分支上)的工作可以继续。
创建hotfix分支
hotfix分支是从主分支创建的。例如,假设版本1.2是当前发布并在运行的产品版本,由于一个严重的bug而造成了问题。但develop分支上的提交还不稳定,然后我们可以拉出hotfix分支,并在上面修复问题:
$ 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并提交一个或多个单独的commit修复问题。
$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)
结束hotfix分支
当hotfix问题修复完成,修复的提交需要合并回master分支,当也要同时合并到develop分支,以确保下一个发布版本中也包含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 <key>
参数以密码方式对tag签名。
接下来,bug修复也合并到develop分支中:
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
这里规则的一个例外是,当一个发布分支当前存在时,需要将hotfix分支的修改合并到该发布分支中,而不是合并到develop分支。当发布分支结束时,合并到发布分支的hotfix分支的修复,最终这些修复也被合并到develop分支中。(如果develop分支不能等待发布分支完成,需要立即修复bug,那么也可以将bug修复合并到现在的develop分支中)
最后,删除临时分支:
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).
总结
虽然这个分支模型并没有什么令人惊奇的新东西,这篇文章开头的“大图”在我们的项目中非常有用。它形成了一个优雅的脑力模型,易于理解,并允许团队成员开发对一个共同理解的分支和发布流程。
网友评论