译自:A successful Git branching model
作者:Vincent Driessen
首发:2010-01-05
反思笔记
更新:2020-03-05
本文中提出的分支模型(git-flow)是在 2010 年构思的,那时 Git 才诞生不久,如今 10 年已过。在这 10 年中,该模型开始在众多软件团队中大放异彩,以至于人们开始将其视为某种标准 —— 但遗憾的是,其甚至被视为了教条或者所谓万能药。
在这 10 年中,Git 席卷全球,而使用 Git 开发的那些最流行的软件,其类型则更多的转向 Web 应用。而 Web 应用通常是持续交付的,不会回滚,你无需支持市场中同时运行的多个版本。
这并非我 10 年前写下这篇博文时想到的那种软件。如果你的团队正在进行软件的持续交付,我建议采用更简单的工作流程(如 GitHub flow),而不是试图将 git-flow 硬塞进你的团队。
但是,如果你正在构建明确版本控制的软件,或者需要在市场中支持多个版本的软件,那么 git-flow 可能仍然适合你的团队,就像大家在过去 10 年中使用的那样。在这种情况下,请继续阅读吧。
总而言之,请永远记住,万能药并不存在。请因需取求,对症下药。
介绍
在本文中,我将介绍一种我从大约一年前开始为一些工作或私人项目引入的开发模型,事实证明其非常成功。我筹备这篇文章已经有一阵子了,但在此之前一直没有找到时间好好儿地写。我不会在此谈论任何项目的细节,仅专注于分支策略和发布管理。
为何选用 Git
有关 Git 与集中式源代码控制系统相比的优缺点的详尽讨论,请参见此页面,那里有很多火爆的唇枪舌战。作为一名开发者,如今我喜欢 Git 胜过所有其他工具。Git 切实改变了开发者对合并和分支的理解。在传统的 CVS 或 Subversion 中,合并或分支是一种会让人担惊受怕的,偶尔才做一次的操作。
但对于 Git 来说,这些操作是非常简单的,实际上这些操作是日常工作流程的核心部分之一。例如,在 CVS 或 Subversion 的书籍中,分支和合并操作总会在后面的那些针对进阶用户的章节才会进行讨论,而在 Git 书籍中,在第 3 章介绍基础知识时便已涉及。
由于其很简单且会高频使用,分支和合并操作便不再让人恐惧。版本控制工具应该比其他工具更适合辅助分支和合并操作才对。
对工具的讨论到此为止,让我们进入开发模型。我在此介绍的模型,本质上不过是为了使开发过程可管理,每一个团队成员需要遵循的一套流程。
去中心化但却集中
我们所使用的且与本分支模型配合良好的仓库配置,需要有一个「真实的」中央仓库。请注意,此仓库仅仅被视为 中央仓库(由于 Git 是 DCVS,因此在技术层面上没有中央仓库之类的概念)。我们将此仓库称为 origin,因为所有的 Git 用户都熟悉这个名字。
每个开发者都从 origin 拉取(pull)并向其推送(push)。但是除了这种集中式的推拉之外,每个开发者也可以从其他同伴那里拉取代码变更,以组成一个子团队。例如,两个或多个开发者协同开发一个重要的新功能时,可避免将代码过早的推送到 origin。在上图中,便有 Alice 与 Bob、Alice 与 David、以及 Clair 与 David 的子团队。
从技术角度讲,这仅表示 Alice 定义了一个名为 bob 的 Git remote,指向了 Bob 的仓库,反之亦然。
主分支
本质上,该开发模型很大程度上是从现有模型获得的灵感。中央仓库包含两个具有无限生命周期的主分支:
每个 Git 用户都应该熟悉 origin 中的 master 分支。与 master 分支还并行存在一个 develop 分支。
我们认为 origin/master 是一个其 HEAD 源码总保持产品就绪 (production-ready)状态的主分支。
我们认为 origin/develop 是一个其 HEAD 源码总反映下一个发行版最新交付的开发变更的主分支。有人将其称为「整合分支」。这就是自动每日构建版(nightly builds)的代码来源。
当 develop 分支中的源码达到一个稳定的状态并准备发布时,所有的代码变更都应该以某种方式合并回 master,然后标记一个发行版本号。具体细节将在后面进一步讨论。
因此,每当代码变更被合并回 master,都将定义一个新的生产版本。此操作应当被严格的限制,所以理论上每当 master 有提交时,可以使用 Git hook 脚本自动构建软件并发布到生产服务器。
辅助分支
除了主分支 master 和 develop 之外,我们的开发模型还使用各种辅助分支来协助团队成员之间的并行开发,简化功能的追踪,为生产发布做准备,并协助快速解决线上版本的问题。与主分支不同,这些分支的生命周期总是有限的,因为他们终将被删除。
这些特殊的分支类型包括:
功能(Feature)分支
发行版(Release)分支
热修复(Hotfix)分支
这里面每一个分支都有明确的目的,并受严格的规则约束,即哪些分支可能是其原始分支,以及哪些分支必须是其合并的目标分支。我们马上就将深入了解。
从技术角度看,这些分支绝不是「特殊的」。这些分支类型实际是按我们的使用方式进行分类的。他当然依旧只是普通的 Git 分支。
功能分支
可能的原始分支:
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 确保合并操作总是创建一个新的提交对象,即使该合并操作可以 fast-forward。这样可以避免丢失这个功能分支的历史信息,将该功能的所有提交组合在一起。比较一下:
后一种情况中,无法从 Git 历史记录中查看到哪些提交一起实现了一个功能 —— 你必须亲自阅读所有日志信息。若想对整个功能(一组提交)进行还原,后一种情况实在令人头疼,而若使用 --no-ff 参数则很容易做到。
没错,这虽然会创建一些(空的)提交对象,但收益远大于成本。
发行版分支
可能的原始分支:
develop
最终必须合并到的分支:
develop 和 master
分支命名规范:
release-*
发行版分支用来为新的生产版本做准备。它允许在最后一刻做一些细节修改。此外,它允许进行细微的 bug 修复并为该发行版准备元数据(版本号、构建日期等)。通过在发行版分支上完成这些工作,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 脚本,它会更改工作环境中的某些文件以反映新版本号。(当然也可以手动更改 —— 这里仅仅是说明某些 文件会被更改)然后修改了版本号的文件被提交。
这个新分支可能会存在一段时间,直到可以确定发布该版本为止。在此期间,可能会在此分支(而不是 develop 分支)进行一些 bug 修复。严禁在此添加大型的新功能,其必须合并到 develop 分支中,因此需要等待下一个大版本。
完成一个发行版分支
当发行版分支的状态准备好成为一个真正的发行版时,需要执行一些操作。首先,将发行版分支合并到 master 中(记住这点,因为 master 上的每个新提交都是我们所定义 的新发行版)。接下来,必须打一个标签,方便以后引用这个历史版本。最后,需要将发行版分支上所做的更改合并到 develop 中,以便将来的发行版中也包含这些 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 <key> 参数来对标签进行加密签名。
为了保留在发行版分支中所做的更改,我们需要将这些更改重新合并到 develop 中:$ 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-*
热修复分支与发行版分支非常相似,尽管其是计划外的,但其目的都是为新的生产版本做准备。它们是对生产环境的不良状态所采取的行动。当生产环境的缺陷必须马上修复时,热修复分支可以基于 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(-)
完成一个热修复分支
完成后,该热修复需要合并回 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 <key> 参数来对 tag 进行加密签名。
然后,将此修复也合并到 develop 中:$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
该规则有一个例外,当存在一个发行版分支时,需要将该热修复的改动合并到该发行版分支中,而非 develop 中 。将热修复合并到发行版分支中时,当发行版分支完成时,最终也会将热修复合并到 develop 中。(如果立即从事的开发工作需要修复此 bug,而又不能等待发行版分支完成,也可以安全的将热修复合并到 develop 中。)
最后,删除临时的分支:$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).
总结
尽管此分支模型没有什么真正令人震惊的新东西,但本文开头的「大图片」在我们的项目中被证明非常有用。它形成了一个易于理解的优雅思维模型,并使团队成员可以对分支和发布过程形成共识。
这里也提供了该图的高质量 PDF 版本。可将其挂在墙上以随时快速参考。
更新 :如果有人需要,这里有份主图的 Keynote 源文件。