一个成功的 Git 分支模型
反思笔记(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 章(基础)中进行了介绍。
由于其简单性和重复性,分支和合并不再是可怕的事情。版本控制工具应该比其他任何东西都更能帮助分支/合并。
关于工具已经足够了,让我们进入开发模型。我将在这里展示的模型本质上只不过是每个团队成员为了进入托管软件开发过程而必须遵循的一组过程。
去中心化但中心化
我们使用的并且与此分支模型配合良好的存储库设置是具有中央“真实”存储库的存储库设置。请注意,此 repo 仅 被视为 中央仓库(由于 Git 是 DVCS,因此在技术层面上没有中央仓库这样的东西)。我们将此存储库称为origin,因为所有 Git 用户都熟悉此名称。

每个开发人员都拉动并推送到原点。但除了中心化的推拉关系,每个开发者也可能会从其他同行那里拉动变化,形成子团队。例如,在将正在进行的工作origin提前进行之前,这可能有助于与两个或更多开发人员一起开发一个重要的新功能 。在上图中,有 Alice 和 Bob、Alice 和 David、Clair 和 David 的子团队。
从技术上讲,这意味着 Alice 定义了一个 Git 远程,名为bob,指向 Bob 的存储库,反之亦然。
主要分支

在核心,开发模型受到现有模型的极大启发。中央仓库拥有两个具有无限生命周期的主要分支:
masterdevelop
将master在分支origin应该熟悉到每一个用户的Git。与master分支平行,存在另一个分支,称为develop。
我们认为origin/master是源代码HEAD始终反映生产就绪状态的主要分支 。
我们认为origin/develop是主分支,其中的源代码 HEAD始终反映下一个版本最新交付的开发更改的状态。有些人将其称为“集成分支”。这是构建任何自动夜间构建的地方。
当develop分支中的源代码达到稳定点并准备发布时,所有更改都应以master 某种方式合并回,然后用发布号标记。将进一步讨论如何详细完成此操作。
因此,每次将更改合并回 时master,根据定义,这是一个新的生产版本。我们在这方面往往非常严格,因此理论上,我们可以使用 Git 钩子脚本在每次提交 master.
支持分支
在主要分支master和旁边develop,我们的开发模型使用各种支持分支来帮助团队成员之间的并行开发、轻松跟踪功能、准备生产版本并帮助快速修复实时生产问题。与主分支不同,这些分支的生命周期总是有限的,因为它们最终会被移除。
我们可能使用的不同类型的分支是:
- 功能分支
- 发布分支
- 修补程序分支
这些分支中的每一个都有特定的目的,并且在哪些分支可以是它们的原始分支以及哪些分支必须是它们的合并目标方面受到严格的规则约束。我们将在一分钟内穿过它们。
从技术角度来看,这些分支绝不是“特殊的”。分支类型根据我们如何使用它们进行分类。它们当然是普通的旧 Git 分支。
功能分支

- 可以从以下分支:
develop- 必须合并回:
develop- 分支命名约定:
- 除了
master,develop,release-*, 或hotfix-*
功能分支(或有时称为主题分支)用于为即将发布或遥远的未来版本开发新功能。在开始开发某个功能时,将包含该功能的目标版本在那时很可能是未知的。功能分支的本质在于,只要该功能处于开发阶段,它就存在,但最终会被合并回develop(明确将新功能添加到即将发布的版本中)或丢弃(如果实验令人失望)。
功能分支通常只存在于开发者仓库中,而不存在于origin.
创建功能分支
当开始一个新特性的工作时,从这个develop分支分支出来。
$ git checkout -b myfeature develop
切换到新分支“myfeature”
在 develop 上合并一个完成的特性
完成的功能可能会合并到develop分支中,以明确地将它们添加到即将发布的版本中:
$ git checkout develop
切换到分支'develop'
$ git merge --no-ff myfeature
更新ea1b82a..05e9557
(更改摘要)
$ git branch -d myfeature
删除了分支myfeature(原为05e9557)。
$ git push origin 开发
该--no-ff标志导致合并总是创建一个新的提交对象,即使合并可以用快进执行。这避免了丢失有关功能分支的历史存在的信息,并将所有一起添加功能的提交组合在一起。比较:

在后一种情况下,无法从 Git 历史记录中看出哪些提交对象一起实现了某个功能——您必须手动读取所有日志消息。在后一种情况下,恢复整个特性(即一组提交)是一个真正令人头疼的问题,而如果使用了--no-ff标志,则很容易做到 。
是的,它会创建更多(空)提交对象,但收益远大于成本。
发布分支
- 可以从以下分支:
develop- 必须合并回:
develop和master- 分支命名约定:
release-*
发布分支支持准备新的生产版本。它们允许在最后一刻打点 i 和交叉 t。此外,它们允许修复小错误并为发布准备元数据(版本号、构建日期等)。通过在发布分支上完成所有这些工作,该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
切换到新分支“release-1.2”
$ ./bump-version.sh 1.2
文件修改成功,版本撞到1.2。
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d9424] Bumped version number to 1.2
1 个文件已更改,1 个插入(+),1 个删除(-)
在创建一个新分支并切换到它之后,我们增加了版本号。这 bump-version.sh是一个虚构的 shell 脚本,它更改了工作副本中的一些文件以反映新版本。(这当然可以是手动更改——关键是某些文件发生了更改。)然后,提交了增加的版本号。
这个新分支可能会存在一段时间,直到正式发布。在此期间,可能会在此分支(而不是在develop分支上)应用错误修复。严禁在此处添加大型新功能。它们必须合并到 中develop,因此,等待下一个大版本。
完成一个发布分支
当发布分支的状态准备好成为真正的发布时,需要执行一些操作。首先,将发布分支合并到 master(因为根据定义,每次提交master都是一个新版本,请记住)。接下来,必须标记该提交,以便将来轻松引用此历史版本。最后,在发布分支上所做的更改需要合并回,以便未来的版本也包含这些错误修复。masterdevelop
Git的前两步:
$ git checkout master
切换到分支'master'
$ git merge --no-ff release-1.2
通过递归进行合并。
(改动总结)
$ git tag -a 1.2
发布现已完成,并标记以供将来参考。
编辑:您可能还想使用
-s或-u <key>标志以加密方式签署您的标签。
不过,为了保留在发布分支中所做的更改,我们需要将它们合并回develop. 在 Git 中:
$ git checkout develop
切换到分支'develop'
$ git merge --no-ff release-1.2
通过递归进行合并。
(变化概要)
这一步很可能会导致合并冲突(甚至可能,因为我们已经更改了版本号)。如果是这样,请修复它并提交。
现在我们真的完成了,发布分支可能会被删除,因为我们不再需要它了:
$ git branch -d release-1.2
删除了分支 release-1.2(原为 ff452fe)。
修补程序分支

- 可以从以下分支:
master- 必须合并回:
develop和master- 分支命名约定:
hotfix-*
Hotfix 分支与发布分支非常相似,因为它们也旨在为新的生产版本做准备,尽管是计划外的。它们产生于需要立即对现场制作版本的不良状态采取行动。当必须立即解决生产版本中的关键错误时,可以从标记生产版本的主分支上的相应标记中分支出一个修补程序分支。
本质是团队成员(在develop分支上)的工作可以继续,而另一个人正在准备快速生产修复。
创建修补程序分支
修补程序分支是从master分支创建的 。例如,假设版本 1.2 是当前正在运行的生产版本,并且由于严重的错误而导致问题。但变化develop尚不稳定。然后我们可以从一个修补程序分支分支并开始修复问题:
$ git checkout -b hotfix-1.2.1 master
切换到新分支“hotfix-1.2.1”
$ ./bump-version.sh 1.2.1
文件修改成功,版本撞到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 个文件更改,1 个插入(+),1 个删除(-)
分支后不要忘记修改版本号!
然后,修复错误并在一个或多个单独的提交中提交修复。
$ git commit -m "修复了严重的生产问题"
[hotfix-1.2.1 abbe5d6] 修复了严重的生产问题
5 个文件更改,32 次插入(+),17 次删除(-)
完成一个修补程序分支
完成后,错误修正需要合并回master,但也需要合并回develop,以确保错误修正也包含在下一个版本中。这与发布分支的完成方式完全相似。
首先,更新master并标记版本。
$ git checkout master
切换到分支'master'
$ git merge --no-ff hotfix-1.2.1
通过递归进行合并。
(改动总结)
$ git tag -a 1.2.1
编辑:您可能还想使用
-s或-u <key>标志以加密方式签署您的标签。
接下来,也包括在 中的错误修正develop:
$ git checkout develop
切换到分支'develop'
$ git merge --no-ff hotfix-1.2.1
通过递归进行合并。
(变化概要)
此处规则的一个例外是, 当当前存在发布分支时,修补程序更改需要合并到该发布分支中,而不是develop. develop当发布分支完成时,将错误修复回合并到发布分支最终将导致错误修复也被合并。(如果develop立即工作需要此错误修复并且不能等待发布分支完成,您可以安全地将错误修复合并到developnow 中。)
最后,删除临时分支:
$ git branch -d hotfix-1.2.1
删除了分支 hotfix-1.2.1 (是 abbe5d6)。
总结
虽然这个分支模型没有什么真正令人震惊的新东西,但本文开头的“大图”图在我们的项目中非常有用。它形成了一个优雅的思维模型,易于理解,并允许团队成员对分支和发布过程形成共同的理解。
此处提供了该图的高质量 PDF 版本。来吧,把它挂在墙上,随时快速参考。
更新:对于任何请求它的人:这是主图表图像(Apple Keynote)的 gitflow-model.src.key。

浙公网安备 33010602011771号