原文地址:https://nvie.com/posts/a-successful-git-branching-model/。
反思笔记(2020 年 3 月 5 日)
该模型构思于 2010 年,现已有 10 多年时间,也就是在 Git 本身出现不久之后。在这 10 年里,git-flow(本文介绍的分支模型)在许多软件团队中非常流行,以至于人们开始将其视为某种标准 — 但不幸的是,它也被视为教条或灵丹妙药。
在这 10 年中,Git 本身已经席卷全世界。用 Git 开发的最流行的软件类型正在更多地转向 Web 应用 — 至少在我看来如此。Web 应用通常被持续交付,不回滚,无需支持软件的多个版本。
这不是我 10 年前写这篇博文时,所想到的软件类别。如果你的团队正在做软件的持续交付,我建议采用更简单的工作流程(比如 GitHub flow),而不是试图将 git-flow 硬塞进你的团队。
然而,如果构建版本化的软件,或者需要支持软件的多个版本,那么 git-flow 可能仍然适合你的团队,就像它在过去 10 年里一直适合人们一样。在这种情况下,请继续阅读。
总之,永远记住,不存在灵丹妙药。结合你自己的背景。不要憎恨。自己决定。
在这篇文章中,我将介绍大约一年前我为我的一些项目(包括工作中的和私人的)引入的开发模型,结果证明它非常成功。我一直想把它写下来,但我从未真正地找时间这样做,直到现在。我不讨论任何项目的细节,只讨论分支策略和发布管理。
关于与集中式的源代码控制系统相比,Git 优缺点的深入讨论,请查看 web。那里有许多激烈的争论。作为开发人员,我喜欢 Git 胜过目前所有其它工具。Git 真正地改变了开发者对合并和分支的看法。在经典的 CVS/Subversion 世界中,合并/分支一直被认为有点可怕(“小心合并冲突,它们会咬你!”),而且是偶尔才会做的事情。
但是使用 Git,这些操作代价很小,并且非常简单,实际上,它们被认为是日常工作流的核心部分之一。比如,在 CVS/Subversion 书籍中,在后面的章节(针对高级用户)中,才讨论分支和合并;而在每本 Git 书籍中,在第 3 章(基础)中,就已经介绍过。
由于其简单和重复的性质,分支和合并不再是令人害怕的事情。版本控制工具应该比其它任何工具更有助于分支/合并。关于工具的讨论已足够多,接下来转向开发模型。我在这里将要介绍的模型本质上不过是一组过程,每个团队成员都必须遵循这些过程,才能进入受管理的软件开发流程。
我们使用的、与该分支模型配合良好的存储库设置具有中心“真实”存储库。我们将该存储库称为 origin
,因为所有 Git 用户都熟悉该名字。
每个开发者都去 origin 拉取和推送。但是除集中式的推-拉关系外,每个开发人员也可以从其他同事处拉取变更,以形成子团队。比如,在将正在进行的工作过早地推送到 origin
之前,与两个或多个开发人员一起开发大型新功能时,这可能很有用。在上图中,有 Alice 和 Bob、Alice 和 David,以及 Clair 和 David 子团队。
从技术上讲,这仅意味着 Alice 定义一个名为 bob
的 Git 远程,指向 Bob 的存储库,反之亦然。
从本质上讲,该开发模型受到现有模型的极大启发。中央存储库拥有两个具有无限生命周期的主分支:
master
develop
每个 Git 用户都应该熟悉 origin
上的 master
分支。与 master
分支并行存在的另一个分支被称为 develop
。
我们将 origin/master
视为主分支,其 HEAD
的源代码总是反映生产就绪(production-ready)状态。
我们将 origin/develop
视为主分支,其 HEAD
的源代码总是反映具有最新交付的开发变更的状态,用于下一版本。有人称之为“集成分支”。任何自动的夜间构建都从这里构建。
当 develop
分支中的源代码达到稳定点,并且准备好发布时,所有变更应该以某种方式合并回 master
,然后使用版本号打标签(tag),实现细节将在后面讨论。
因此,每当变更被合并回 master
时,根据定义,这是一个新生产版本。在这一点上,我们往往非常严格,因此理论上,每当 master
上有提交时,我们应该使用 Git 钩子脚本,自动地构建,并且将软件部署到生产服务器上。
除主分支 master
和 develop
外,我们的开发模型使用多种支撑分支,帮助团队成员之间并行开发,简化特性的追踪,为生产发布做准备,协助快速修复实时的生产问题。与主分支不同,这些分支的生命周期有限,因为它们最终会被移除。
我们可能使用的不同类型的分支是:
这些分支中的每一个都有特定的用途,并且被绑定到严格的规则中,即哪些分支可以是它们的原始分支,哪些分支必须是它们的合并目标。我们马上就会讲到。
从技术角度来看,这些分支绝不是“特殊的”。根据我们的使用方式,对分支类型进行分类。它们是普通 Git 分支。
可以分支于:
develop
必须合并到:
develop
分支命名约定:
除 master
、develop
、release-*
或 hotfix-*
外的任意名称
特性分支(有时被称为主题分支)用于为即将到来的或遥远的未来版本开发新特性。当开始特性的开发时,该特性将被合并到的目标版本在那时很可能是未知的。特性分支的本质是,只要特性处于开发阶段,它就一直存在,但最终将被合并回 develop
(明确地将新特性添加到即将到来的版本中)或丢弃(在实验令人失望的情况下)。
特性分支通常仅存在于开发者存储库中, 而不是在 origin
中。
当开始开发新特性时,从 develop
分支创建特性分支:
xxxxxxxxxx
$ git checkout -b myfeature develop
Switched to a new branch "myfeature"
develop
上合并已完成特性已完成特性可被合并进 develop
分支,以明确地将它们添加到即将到来的版本中:
xxxxxxxxxx
$ 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)。因此,我们创建并给发布分支起一个反映新版本号的名称:
xxxxxxxxxx
$ 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 的前两步:
xxxxxxxxxx
$ 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 中:
xxxxxxxxxx
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)
这一步可能导致合并冲突(很可能,因为我们已经更改版本号)。如果确实这样,那么修复它,然后提交。
现在我们已真正地完成发布,可以移除发布分支,因为我们不再需要它:
xxxxxxxxxx
$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).
可以分支于:
master
必须合并到:
develop
master
分支命名约定:
hotfix-*
热修复分支与发布分支非常像,因为它们也意味着为新生产版本做准备,尽管是计划之外的。它们产生于对生产版本的不期望状态立即采取行动的需要。当必须立即解决生产版本中的关键错误时,可以从标记生产版本的主分支上的相应标签中分支出一个热修复分支。
其本质是,团队成员(在开发分支上)的工作可以继续,而其他人准备快速的生产修复。
热修复分支是从 master
分支创建的。比如,假设版本 1.2 是当前正在运行的生产版本,由于严重的 Bug,导致故障。但是 develop
上的变更尚未稳定。我们可以创建一个热修复分支,开始修复该问题:
xxxxxxxxxx
$ 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,在一个或多个独立的提交中提交修复。
xxxxxxxxxx
$ 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
。以保证 Bug 修复也被包含在下一版本中。这与发布分支的完成方式完全相似。
首先,更新 master
,并且为发布打标签。
xxxxxxxxxx
$ 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>
标记,给标签加密。
接下来,也在 develop
中包含该 Bug 修复:
xxxxxxxxxx
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
该规则的一个例外是,当发布分支当前存在时,热修复变更需要被合并到该发布分支中,而非 develop
。将该 Bug 修复反向合并到发布分支中最终将导致 Bug 修复也被合并到 develop
中,当发布分支完成时。(如果 develop
中的工作立即需要该 Bug 修复,并且不能等到发布分支完成,那么也可以安全地将该 Bug 修复合并到 develop
中。)
最后,移除临时分支:
xxxxxxxxxx
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).
虽然该分支模型没有什么真正令人震惊的新东西,但这篇博文开头的“大图”在我们的项目中非常有用。它形成一个优雅的思维模型,该模型易于理解,使团队成员对分支和发布过程形成共同的理解。
这里提供该图的高质量 PDF 版本。将它挂在墙上,以便随时查阅。
更新:对于任何需要它的人:这是主图图像的 gitflow-model.src.key(Apple Keynote)。