# 一个成功的Git分支模型

# 反思备录(2020/3/5)

在Git诞生后不久,这个模型构思于2010年,现已过去10余年。在这10年间,git-flow(本文中提到的分支模型)在许多软件队伍里特别流行,以至于人们已经把它当成某种软件标准——但不幸的是它也被视为教条或灵丹妙药。

在这10年里,Git已经席卷了世界,而使用Git开发的最受欢迎的软件类型正在更多地转向网络应用程序,至少在我的认知(原文:过滤器泡沫-filter bubble)里是这样的。Web应用程序通常是连续交付的,而不是回滚的,并且你不必维护自然环境下运行的软件的多个版本。

这不是我10年前写博客时想的那种软件。如果你的团队正在持续交付软件,我建议你采用更简单的工作流(如GitHub流),而不是试图将git-flow塞进你的团队。

然而,如果你正在构建明确版本化的软件,或者如果你需要在自然环境下支持多个版本的软件,那么git-flow可能仍然像过去10年一样适合你的团队。在这种情况下,请继续阅读。

总之,永远记住,灵丹妙药是不存在的。充分考虑你自己的背景——不要讨厌自己决定。

# 正文

在这篇文章中,我介绍了大约一年前我为我的一些项目(包括工作和私人)引入的开发模式,结果证明它非常成功。我想写这篇文章已经有一段时间了,但直到现在,我还没有真正找到时间写得这么彻底。我不谈任何项目细节,只谈分支策略和发布管理。

# 为什么使用Git?

有关Git与集中式源代码控制系统的优缺点的详细讨论,请参阅Git-Svn对比 (opens new window),那里正在进行大量的唇枪舌战。作为一名开发人员,我更喜欢Git,而不是当下的所有其它工具。Git确实改变了开发人员对合并和分支的看法。从我所处的经典CVS/Subversion体系来看,分离(分支)/合并(分支)一直被认为有点可怕(“小心合并冲突,它们会反噬你!”),然而你只是偶尔做一次。

但是使用Git,这些操作非常低耗且简单,它们被认为是日常工作流程的核心部分之一。例如,在CVS/Subversion书籍中,分离和合并首先在后面的章节中讨论(对于高级用户),而在每一本Git书籍中,它已经在第3章(基础知识)中讨论过了。

由于其自身的简单性和重复性,分离和合并不再是令人害怕的事情。版本控制工具应该比其它任何工具更有助于分离/合并。

关于工具的内容已经足够了,让我们继续讨论开发模型。我将在这里介绍的模型基本上只不过是一系列的操作,每个团队成员都必须遵循这些操作才能进入托管软件开发过程。

# 去中心化又中心化

我们使用的存储仓库设置,它与这个分支模型很好地配合使用,是一个中央“真实”仓库。请注意,该仓库仅被(人为)视为中央仓库(由于Git是一个DVCS[Distributed Version Control System,分布式版本控制系统],因此在技术层面上不存在中央仓库)。我们将此仓库称为原始仓库origin,因为所有Git用户都熟悉这个名称。

每个开发人员都将其代码分离于且提交到原始仓库origin。但除了统一的“推送-拉取”关系之外,每个开发人员还可以从其他同行那里获取更改,以组成子团队。例如,在过早地将正在进行的工作推到原始仓库origin之前,与两个或两个以上的开发人员合作开发一个大的新特性可能会很有用。在上图中,有爱丽丝(alice)和鲍勃(bob)、爱丽丝与大卫(david)、克莱尔(clair)和大卫的子组。

从技术上讲,这意味着alice定义了一个名为bob的Git远程,指向bob的存储库,反之亦然(bob也定义了一个名为alice的远程仓库并指向)。

# 核心分支

最核心的是,开发模型受到了现有模型的极大启发。中央仓库持有两个寿命无限(伴随软件开发的整个周期)的核心分支:

master

develop

每个Git用户都应该熟悉原始仓库origin的主分支master。与主分支master并行的还有另一个分支,称为开发分支develop

我们认为origin/master是核心分支,是因为其HEAD源代码总是反映生产就绪状态。

我们认为origin/develop是核心分支,是因为其HEAD源代码总是反映下一个版本中最新交付的开发更改的状态。有人会称之为“集成分支”。这是构成任何自动夜间构建的基础。

当开发分支develop中的源代码达到稳定点并准备发布时,所有更改都应该以某种方式合并回主分支master,然后用发布号标记。这将在后面详细讨论。

因此,每次将更改合并到主分支master时,这都是一个新的生产版本。我们倾向于对此非常严格,因此理论上,我们可以使用Git钩子脚本在每次主服务器上提交代码到主分支master时自动构建软件,并将其推出到我们的生产服务器。

# 支撑分支

除了主分支master和开发分支develop之外,我们的开发模型还使用各种支持分支来帮助团队成员之间的并行开发,简化功能跟踪,为生产发布做准备,并帮助快速解决实时生产问题。与核心分支不同,这些分支的寿命总是有限的,因为它们最终会被移除(不必伴随软件开发的整个周期)。

我们可以(言外之意也可以自己定义)使用的不同类型的分支有:

Feature branches

Release branches

Hotfix branches

这些分支每个都有特定的目的,并受严格的规则约束,即哪些分支可以是其原始分支,哪些分支必须是其合并目标。我们将在一分钟内彻底了解它们。

从技术角度来看,这些分支绝非“特殊”。分支类型根据我们使用它们的方式进行分类。它们当然是普通的旧Git分支。

# 特性分支Feature branches

可以来自于(特定分支):

develop

必须合并到(特定分支):

develop

分支命名约定:

除了masterdeveloprelease-*,或hotfix-*之外的所有命名均可。

特性分支Feature branches(或有时称为主题分支)用于为即将发布或将来发布的版本开发新功能。当开始开发一个特性时,这个特性将被纳入的目标版本在那个时候可能是未知的。特性分支的本质是,只要特性处于开发阶段,它就一直存在,但最终会被合并回开发分支develop(以明确地将新特性添加到即将发布的版本中)或被丢弃(以防出现令人失望的经历)。

特性分支Feature branches通常只存在于开发者存储库中,而不存在于原始仓库origin中。

创建功能分支Creating a feature branch

在开始新特性的工作时,从开发分支develop拉出分支。

$ git checkout -b myfeature develop
Switched to a new branch "myfeature"

在开发过程中合并已完成的功能Incorporating a finished feature on 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标志使合并始终创建一个新的提交对象,哪怕(不使用的话)合并可以快速执行。这避免了丢失关于特性分支的历史存在的信息,并将一起添加特性的所有提交分组在一起。比较:

在后一种情况下,(合并以后)不可能从Git历史中看到哪些提交对象一起实现了一个特性,你必须手动读取所有日志消息。恢复整个特性(例如一组提交)是一个真正令人头痛的问题,而如果使用了--no ff标志,则很容易完成。

是的,它将创建更多(空)提交对象,但收益远大于成本。

# 发布分支Release branches

可以来自于(特定分支):

develop

必须合并到(特定分支):

developmaster

分支命名约定:

release-*

发布分支为新的生产发布提供支持。它们允许在最后一刻打点和提交……(原文是:They allow for last-minute dotting of i’s and crossing t’s,此句不明?)。此外,它们还允许小的错误修复和为发布准备元数据(版本号、构建日期等)。通过在发布分支上完成所有这些工作,开发分支develop可以接收下一个大型发布的特性。

从开发分支develop拉出新发布分支的关键时刻是开发分支(几乎)反映了新发布的期望状态。至少此刻所有针对要构建的版本的特性都必须合并在开发分支develop中。所有面向未来版本的功能(可能都不是),它们必须等到发布分支被分离后。

正是从发布分支出现开始,即将发布的版本被分配了一个版本号——而不是更早的时候。直到那一刻(指发布分支出现),开发分支develop反映了“下一次发布”的变化,但在发布分支开始之前,尚不清楚“下一版本”最终会变成0.3还是1.0。这个决定是在发布分支开始时做出的,并由项目关于版本号变更的规则执行。

创建发布分支Creating a release branch

发布分支是从开发分支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中,并且,等待下一个大型版本。

完成发布分支Finishing a release branch

当发布分支的状态准备成为真正的发布时,需要执行一些操作。首先,发布分支被合并到主分支master中(因为主分支master上的每个提交根据定义都是一个新的发布,请记住这点)。接下来,必须标记在主分支master上的提交,以便于将来参考此历史版本。最后,需要将发布分支上所做的更改合并回开发分支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

该版本现已完成,并已标记以供将来参考。

你还可以使用-s或-u<key>标志对标记进行加密签名。

为了保持发布分支中所做的更改,我们需要将这些更改合并回开发分支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

必须合并到(特定分支):

developmaster

分支命名约定:

hotfix-*

修补程序分支与发布分支非常相似,因为它们也旨在为新的生产发布做准备,尽管是计划外的。它们产生于必须立即对现场制作版本的不期望状态采取行动。当必须立即解决生产版本中的关键bug时,可以从标记生产版本的主分支上的相应标记分出一个修补程序分支。

本质是团队成员(在开发分支develop上)的工作可以继续,而另一个人正在准备快速的生产修复。

创建修补程序分支Creating the hotfix branch

修补程序分支是从主分支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(-)

完成修补程序分支Finishing a hotfix branch

完成后,hotfix需要合并回主分支master,但也需要合并回开发分支develop,以确保hotfix也包含在下一个版本中。这完全类似于发布分支的完成方式。

首先,更新主分支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>标志对标记进行加密签名。

接下来,在开发分支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中。(如果开发分支develop中的工作立即需要此错误修复,并且无法等待发布分支完成,那么你也可以安全地将错误修复合并到开发分支develop中。)

最后,删除临时分支:

$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).

# 总结

虽然这个分支模型并没有什么真正令人震惊的新东西,但这篇文章开头的“大图”表现出在我们的项目中非常有用。它形成了一个优雅的心理模型,易于理解,并允许团队成员对分支和发布过程形成共同的理解。

本文翻译自网络:https://nvie.com/posts/a-successful-git-branching-model (opens new window)

本文由【白鸽子中文网「baigezi.com」】翻译



微信公众号

QQ交流群
原创网站开发,偏差难以避免。

如若发现错误,诚心感谢反馈。

愿你倾心相念,愿你学有所成。

愿你朝华相顾,愿你前程似锦。