本文翻译自Vincent Driessen的:A successful Git branching model
本文所讲的开发模型是我在Git
诞生后不久的2010年构思的,距今已有十余年。在这10年里,git-flow
(本文中列出的分支模型)在许多软件团队中非常流行,以至于人们开始将其视为某种标准,但不幸的是,它也被视为一种教条或万灵药。
在这10年间,Git
本身已经风靡全球,而且web应用程序越来越成为最流行的软件应用类型,至少在我的认知中是这样的。Web应用程序通常是连续交付的,而不是回滚的,而且你不必支持多个版本的软件。
这不是我十年前写博客时想的那种软件。如果您的团队正在持续交付软件,我建议采用更简单的工作流(如GitHub流),而不是试图将 git-flow
硬塞进您的团队。
然而,如果您正在构建显式版本化的软件,或者如果您需要在野外支持软件的多个版本,那么git-flow可能仍然像过去10年一样适合您的团队。如果是这样,请继续阅读。
总之,永远记住,万灵药是不存在的,你要结合自己的需求背景做决定。
在这篇文章中,我介绍了大约一年前我为我的一些项目(包括工作和私人项目)引入的开发模型,结果证明它非常成功。我想写这篇文章已经有一段时间了,直到今天我才腾出时间来。在这篇文章中我不会谈论任何项目的细节,只谈论分支策略和发布管理。
有关Git
相对于集中式源码控制系统的优缺点,网络上讨论得如火如荼,我在这里就不再赘述。作为一名开发人员,我本人是更喜欢Git
。Git
确实改变了开发人员对合并和分支的看法。在Git
之前,合并分支一直被认为是一件让人担惊受怕的事情。
但有了Git
之后,这些操作变得非常简单,并且它们也成为日常工作流程的核心部分之一。例如,在CVS/Subversion书籍中,分支和合并通常在属于面向高级用户的知识点而被放在靠后的章节中讨论,而在Git
相关的书籍中,这些被视为基础知识而放置在第三章进行讨论。
简单和重复的特性带来的结果是:分支与合并不再是什么值得害怕的东西。分支/合并被认为对于版本管理工具比其他功能更重要。
工具已备,让我们直接看开发模型吧。我将在这里介绍的模型基本上只不过是一组程序,每个团队成员都必须遵循这些程序才能形成托管软件开发过程。
Git
属于一种分布式版本控制系统(Distributed Version Control System
),因此 Git
中并没有一个真正意义上的中央仓库。但我们通常把 每个Git
用户所熟知的 origin
视作中央仓库,这种设定非常契合我的分支模型。
每个人都可以从 origin
拉取代码或向 origin
推入代码。但除了这种中心化的推-拉关系之外,每个开发人员还可以和其他人组成子团队,子团队成员之间互相拉取对方的代码。这种模式对多个开发者协作开发一个大型的需求更加有利。例如上图中,有Alice和Bob、Alice和David、Clair和David的子团队。
从技术上来说,这只意味着Alice定义了一个名为bob的Git
远程,指向bob的仓库,反之亦然。
中央仓库拥有两个永久的主要分支:
master
develop
其中,每个 Git
用户都耳熟能详的 origin/master
分支,承载着通过了测试并准备发布到生产环境上的最新代码。
而与之对应的 origin/develop
分支,也就是开发分支,这个分支上的代码一般是由各个开发人员针对下个版本开发合并而来,因此也有些人将其称为“集成分支”。这个分支是构建和测试的基础。
当开发分支中的代码经过测试达到可上线状态,那么开发分支的所有更改都应该以某种方式合并回master
分支,并用发布号标记。后文中,我们将进一步详细讨论如何做到这一点。
每次将更改合并回 master
时,理论上都将诞生一个新的生产版本,因此向 master
合并代码往往是非常严谨的一件事情。从而,从理论上来说,每当master分支有一个提交操作,我们就可以使用Git
钩子脚本来自动构建并且发布软件到生产服务器。
除了 master
和 develop
这两个主要分支之外,我们的开发模型还会使用多个次要分支来支持多人并行开发模式。与主要分支不同的是,这些次要分支生命周期是有限的,我们通常是会为某个需求而创建一个分支并在需求完成之后删除它。
我们可以使用的辅助分支类型有:
Feature branches
Release branches
Hotfix branches
这些分支都有其特定用途,并受到严格约束,例如哪些分支是原始分支,哪些分支是代码合并的目标分支等。后文我将简短地展开论述一下这个问题。
Feature branches
,可以称之为特性分支,特性分支(有时被称为主题分支)用于为近期或远期的未来版本开发新特性(或新功能)。当一个新特性开始开发时,它将被纳入的目标版本可能在那时还不清楚。特性分支的本质是,只要新特性还在开发中,它就会一直存在,但最终会被合并回 develop
分支中(以明确地将新特性添加到即将发布的版本中)或被丢弃。
Feature
分支基于 develop
分支,合并到 develop
分支,命名上比较自由,但不得使用master
、develop
、release-*
、hotfix-*
等。
创建 Feature 分支
当开始开发一个新功能时,请从 develop
分支初始化一个特性分支:
# Switched to a new branch "myfeature"
$ git checkout -b myfeature develop
合并 Feature 分支
开发完成的功能可能会合并到 develop
分支中,以明确将其添加到即将发布的版本中:
# 切换到 develop 分支
$ git checkout develop
# 将 myfeature 分支合并到 develop 分支
$ git merge --no-ff myfeature
# 删除 myfeature 分支
$ git branch -d myfeature
#
$ git push origin develop
--no-ff
标志会导致合并始终创建一个新的提交对象,即使合并可以快速执行。这样可以避免丢失有关功能分支历史存在的信息,并将所有添加功能的提交组合在一起。对比情形如下:
在后一种情况下,无法从Git
历史中看到哪些提交对象一起实现了一个特性,你必须手动读取所有日志消息,而且这种情况下还原整个特性(即一组提交)确实是一个令人头痛的问题,而如果使用 --no-ff
标志,这个问题就容易解决多了。
尽管使用 --no-ff
标志将创建更多的提交对象,但收益远大于成本。
Release branches
,被称之为发布分支,这个分支用来支持新产品或新功能的准备工作。发布分支允许进行小的错误修复和元数据(版本号、构建日期等)准备工作。当 Release
分支创建后,develop
分支就可以腾出来去承接新的需求了。
从 develop
分支中分离出一个新的 Release
分支的关键前提是 develop
分支达到了新发行版本的期望状态,也就是所有针对这次即将发布的版本而开发的代码都必须合并进来,至于为后续版本开发的功能则要等 Release
分支创建后再合并进 develop
分支。
随着发布分支的创建,即将发布的版本会被分配一个版本号,这个版本号会与之前的版本号做区分。在这之前,develop
分支反映了“下一个版本”的变化,但在Release
分支开始之前,“下一版本”最终会变成0.3还是1.0还不清楚。这个版本号命名是在Release
分支创建时做出的,并严格遵守项目关于版本号的延续命名习惯。
Release
分支是从develop
分支创建的。例如,假设版本1.1.5是当前的生产版本,我们即将推出一个大型版本。开发状态已经为“下一个版本”做好了准备,我们已经决定这将成为1.2版(而不是1.1.6或2.0版)。因此,我们创建相应的Release
分支 ,并为Release
分支机构指定一个反映新版本号的名称:
# 切换到一个新分支 release-1.2
$ git checkout -b release-1.2 develop
# 进行版本号碰撞--提示:Files modified successfully, version bumped to 1.2.
$ ./bump-version.sh 1.2
# [release-1.2 74d9424] Bumped version number to 1.2
# 1 files changed, 1 insertions(+), 1 deletions(-)
$ git commit -a -m "Bumped version number to 1.2"
至此,Release
分支创建完成,并打了标记以备将来参考。
您还可以使用
-s
或-u <key>
标志对标记进行加密签名。
不过,为了保存在Release
分支中所做的更改,我们需要将这些更改合并回develop
分支中:
# 切换到 develop 分支
$ git checkout develop
# 将 release-1.2 分支合并到 develop 分支
$ git merge --no-ff release-1.2
这一步很可能会导致合并冲突,如果遇到冲突,解决冲突后再重新提交即可。
以上工作全部完成后,Release
分支的使命就完成了,我们可以将其删除:
$ git branch -d release-1.2
Hotfix branches
,热修复分支,与Release
分支非常相似,也旨在为新的生产版本做准备,尽管是计划外的。它们通常是为了解决线上问题而创建的临时分支。
Hotfix
分支基于 master
分支,合并到 master
分支和 develop
分支,命名上一般采用 hotfix-*
形式。
在实际工作中,通常由特定人员在 Hotfix
分支上快速修复线上问题,而其他人员的正常开发工作继续进行。
创建Hotfix
分支
Hotfix
分支是从master
分支创建的。例如,假设1.2版是当前正在运行的生产版本,并且由于严重的错误而导致问题。与此同时,随着新需求的开发,develop分支仍在不断更新中。因此,我们可以分支出一个Hotfix
分支并开始修复问题:
# 切换到 hotfix-1.2.1 分支
$ git checkout -b hotfix-1.2.1 master
# 更新版本号--Files modified successfully, version bumped to 1.2.1.
$ ./bump-version.sh 1.2.1
# 初始提交
$ git commit -a -m "Bumped version number to 1.2.1"
分支创建后,记得更新版本号。
接下来,修复问题并提交你的代码吧。
$ git commit -m "Fixed severe production problem"
# [hotfix-1.2.1 abbe5d6] Fixed severe production problem
# 5 files changed, 32 insertions(+), 17 deletions(-)
终结Hotfix
分支
问题修复完成后,bugfix
分支需要合并回master
分支,也需要合并回develop
分支,以确保这些代码也包含在下一版本中。
首先,更新 master
分支并标记特性
# 切换到 master 分支
$ git checkout master
# 将 hotfix-1.2.1 合并到 master 分支
$ git merge --no-ff hotfix-1.2.1
# 打标记
$ git tag -a 1.2.1
您还可以使用
-s
或-u <key>
标志对标记进行加密签名。
接下来,将bugfix
分支合并到develop
分支:
# 切换到 develop 分支
$ git checkout develop
# 将 hotfix-1.2.1 合并到 develop 分支
$ git merge --no-ff hotfix-1.2.1
此处规则的一个例外是: 如果存在一个release
分支,那么应该把hotfix
合并到这个release
分支,而不是合并到develop
分支。当release
分支完成后, 将bugfix
分支合并回release
分支同样会使得bugfix
被合并到develop
分支。(如果在develop
分支的工作急需这个bugfix
,等不到release
分支的完成,那你也可以把bugfix
合并到develop
分支)
最后,删除 bugfix
分支:
$ git branch -d hotfix-1.2.1
虽然这个分支模型并没有什么新的令人震惊的东西,但这篇文章开头的图表在我们的项目中已经被证明是非常有用的。它形成了一个优雅的思维模型,易于理解,并引领团队成员达成对分支和发布过程的共识。