原文链接:https://nvie.com/posts/a-successful-git-branching-model/
反思笔记 (2020 年 3 月 5 日)
此模型源于2010年,距今逾十载,其诞生仅在Git问世之后不久。在这十年间,git-flow(本文所述的分支模型)在众多软件团队中广受欢迎,几乎成为某种标准——但遗憾的是,它也被当作教条或万能药。
在这同样的十年,Git以翻天覆地之势席卷全球,而用Git开发的最流行的软件类型正逐渐偏向网络应用——至少在我的认知范围内是这样。网络应用通常是连续交付的,不会被回滚,你不必支持在外部运行的多个版本的软件。
这并非我十年前撰写博文时所想象的软件类别。如果您的团队正在进行软件的持续交付,我建议采纳一个更为简化的工作流(比如GitHub流程),而不是强行将git-flow融入您的团队。
然而,如果你正在构建的软件明确需要版本控制,或者您需要在外部支持软件的多个版本,那么git-flow或许仍然适合您的团队,如同过去十年对许多人一样。在这种情况下,请继续阅读。
最后,切记,万能药不存在。考虑自身所处之境。无需盲目跟随。一切,由您决定。
在这篇文章中,我将介绍大约一年前我为我的一些项目(包括工作中和私人项目)引入的开发模型,这种模型已经被证明是非常成功的。我一直想要就此写文章,但直到现在,我都没有真正找到时间来彻底地做这件事。我不会讨论任何项目的细节,仅仅关于分支策略和发布管理。
为什么选择Git?
关于 Git 与集中式源代码控制系统的优缺点的详尽讨论,请参考网络上的相关内容。那里有着大量关于这个话题的激烈讨论。作为一名开发人员,我更喜欢 Git,它胜过当今所有其他工具。Git真正改变了开发者对于合并和分支的思考方式。在我所熟悉的经典CVS/Subversion世界中,创建分支/分支一直被认为有点可怕(“小心合并冲突,它们会咬人!”)而且只是偶尔才会做的事情。
但在Git的世界里,这些操作不仅成本低廉、操作简便,而且成为了日常工作流程中的核心部分。例如,在CVS/Subversion的书籍中,分支和合并通常在后面的章节(面向高级用户)才讨论,而在每本Git书籍中,这些内容已经在第3章(基础知识)就涵盖了。
由于其简易性和重复性质,创建分支和合并不再是令人害怕的东西。版本控制工具的首要任务,无疑是协助进行创建分支与合并。
说完工具,让我们来谈谈开发模式。我在这里介绍的模式本质上不过是一套程序,每个团队成员都必须遵循这套程序才能实现可管理的软件开发流程。
集中亦去中心化
我们采纳的仓库架构,它与分支模型相得益彰,存在一个被公认的“权威”仓库。值得注意的是,这个仓库之所以被视作中心,仅是在概念上如此(Git作为一个分布式版本控制系统,在技术层面并不真正有“中心”仓库的概念)。我们将这个仓库称作origin
,这一命名对所有Git使用者而言,都如雷贯耳。
每位开发者都会向origin
仓库进行拉取和推送。但除了这种集中式的拉取-推送关系外,每位开发者也可以从其他同伴那里拉取更改,以形成小型团队。例如,这在与两位或更多开发者一起开发一个大型新功能时特别有用,这样可以避免过早地将正在进行的工作推送到origin仓库。在上图中,有Alice和Bob、Alice和David,以及Clair和David的小型团队。
从技术上讲,这无非是Alice定义了一个名为bob
的Git远程仓库,指向Bob的仓库,反之亦然。
主要分支
在核心层面,该开发模型极大地受到了现存模型的启发。中央仓库维护着两个具有无限生命周期的主分支:
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
分支命名规范: 除了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标志,这会容易得多。
是的,这将创建一些更多的(空的)提交对象,但收获远大于成本。
发布分支(Release branches)
可从以下分支切出: develop
必须合并回以下分支: develop
和 master
分支命名约定: release-*
发布分支支持新生产版本的准备工作。它们允许进行最后一刻的点缀和修正。此外,它们允许进行小的错误修复和为发布准备元数据(版本号、构建日期等)。通过在发布分支上进行所有这些工作,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脚本,它更改工作副本中的一些文件以反映新版本。(当然,这也可以是手动更改——重点是一些文件会发生变化。)然后,提交更改后的版本号。
这个新分支可能会存在一段时间,直到发布最终可能被推出。在此期间,bug修复可能会在这个分支上应用(而不是在develop分支上)。在这里添加大的新功能是严格禁止的。它们必须被合并到develop,因此,等待下一个大的版本发布。
完成发布分支
当发布分支的状态准备变成一个真正的发布时,需要执行一些操作。首先,发布分支被合并到master
(因为按定义,master上的每次提交都是一个新发布,记住了吗?)。接下来,master上的那次提交必须被打上标签,以便于将来轻松地参考这个历史版本。最后,发布分支上做出的更改需要被合并回develop
,以便未来的版本也包含这些bug修复。
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
发布现在完成了,并为将来的参考打上了标签。
Edit:您可能还想使用
-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 branches)
可从以下分支拉出: master
必须合并回以下分支: develop
和master
分支命名规约: hotfix-*
紧急修复分支在本质上非常类似于发布分支,因为它们同样意在准备一个新的生产版本发布,尽管这是非计划的。它们源于立即对生产版本中出现的不期望状态采取行动的必要性。当生产版本中出现一个必须立即解决的关键性错误时,可以从标记着生产版本的master分支上的对应标签拉出一个紧急修复分支。
其要点是,团队成员(在develop
分支上的)可以继续工作,而另一个人则在准备一个快速的生产修复。
创建紧急修复分支
紧急修复分支是从master
分支创建的。例如,假设版本1.2是当前正在运行并因为一个严重的bug造成麻烦的生产版本。但develop
上的更改还不稳定。我们可能会从master
拉出一个紧急修复分支并开始解决问题:
$ 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(-)
在从master分支拉出后不要忘记提升版本号!
然后,修复bug并在一个或多个单独的提交中提交修复。
$ 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并标记该版本。
$ 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
Edit:您可能还想使用
-s
或-u <key>
标志来对您的标签进行加密签名。
接下来,也要将bug修复包含进develop中:
$ 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
了。)
最后,移除临时分支:
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).
总结
虽然这种分支模型并没有什么真正令人震惊的新元素,但这篇文章开头所展示的“大局观”图示在我们的项目中证明极其有用。它形成了一个优雅的心智模型,易于理解,允许团队成员发展出对分支和发布流程的共同认识。
评论区