合理使用Git分支

Git分支使用策略

分类:

Git


反思

这个模型诞生于2010年,距今已有十余年,当时Git问世也才不久。在这十年间,git-flow(即本文阐述的分支模型)已在众多软件团队中广泛流行,甚至被奉为某种标准——但不幸的是,有时也被当作教条或万能解决方案。 过去十年间,Git本身已席卷全球。根据我的观察筛选,使用Git开发的最主流软件类型正日益转向Web应用——这类应用通常采用持续交付模式,无需回滚操作,也不必维护多个正在运行的软件版本。 这与我十年前撰写博客时设想的软件类型并不相同。如果您的团队正实施持续交付,我建议采用更简洁的工作流程(例如GitHub flow),而非生搬硬套git-flow。 但如果您正在开发明确需要版本标识的软件,或需要维护多个线上版本,那么git-flow可能依然适合您的团队,正如过去十年间它对许多团队的帮助一样。若是这种情况,请继续阅读本文。 最后请谨记:世间并无万能药。要结合自身环境具体考量,避免盲目否定,独立做出决策。

在这篇文章中,我将介绍大约一年前我为某些项目(包括工作和私人项目)引入的开发模式,该模式已被证明非常成功。我一直想就此撰写文章,但直到现在才真正抽出时间进行详细阐述。我不会讨论任何项目的具体细节,仅涉及分支策略和发布管理。 git-flow

为何选择Git?

关于Git相较于集中式源代码控制系统的优势与不足,网络上已有详尽探讨,其中不乏激烈争论。作为一名开发者,我个人认为Git远超当今其他工具。Git彻底改变了开发者对合并与分支的认知。在我曾经使用的经典CVS/Subversion体系中,合并/分支始终被视为需要谨慎对待的操作(“当心合并冲突,它们会带来麻烦!”),通常只会偶尔进行。

而Git使这些操作变得极其轻量和简单,它们事实上已成为日常工作流的核心组成部分。例如在CVS/Subversion的教程中,分支与合并通常出现在高级章节,但在所有Git教程中,第三章(基础篇)就会涉及这些内容。

正是由于其简洁性和可重复性,分支与合并不再令人望而生畏。版本控制工具最重要的功能,恰恰就是协助完成分支/合并操作。

关于工具就谈到这里,现在让我们转向开发模式。接下来要介绍的模型,本质上是一套需要全体团队成员遵循的规范化软件开发流程。

去中心化的集中式管理

我们采用的代码库架构与这个分支模型完美契合:建立一个中央”权威”仓库。需要注意的是,这个仓库之所以被称为中央仓库(鉴于Git是分布式版本控制系统,在技术层面并不存在真正的中央仓库),仅仅是因为所有Git用户都熟悉origin这个默认名称。 TeamWork

所有开发者都向origin执行推送和拉取操作。但除了这种集中式的推送-拉取关系外,每位开发者也可以从其他同事那里拉取变更,以组建子团队。例如,当两三位开发者共同开发一个重要新功能时,这种模式就能避免未完成的代码过早流入origin。在上图中,存在Alice与Bob、Alice与David、Clair与David的子团队。

从技术层面而言,这仅意味着Alice定义了一个名为bob的Git远程节点指向Bob的代码库,反之亦然。

主干分支体系

该开发模式的核心思想深受现有模型启发。中央代码库维护着两个具有无限生命周期的核心分支:

每个Git用户都应熟悉origin/master分支。与master分支并行存在的另一分支称为develop

我们将origin/master视作核心分支,其HEAD所指的源代码始终为可并入生产环境的状态。

我们将origin/develop视作主要开发分支,其HEAD所指的源代码始终包含为下一版本准备的最新开发变更。有些人将其称为”集成分支”,夜间自动构建版本均基于此分支生成。

develop分支中的源代码达到稳定状态并准备发布时,所有变更都应以某种方式合并回master分支,并标记版本号。具体操作方式将在后续详细说明。

因此根据定义,每次将变更合并回master分支即意味着生成新的生产版本。我们对此要求极为严格,理论上甚至可以通过Git fork脚本实现:每当master分支有提交时,自动构建并将软件部署至生产服务器。

辅助分支体系

masterdevelop主干分支外,我们的开发模型还采用多种辅助分支来促进团队成员的并行开发、简化功能追踪、准备生产发布以及快速修复线上问题。与主干分支不同,这些分支都具有有限的生命周期,最终会被移除。

我们使用的分支类型包括:

每种分支都有特定用途,并严格限定其源分支和必须合并的目标分支。这些分支遵循严谨的规则体系来确保开发流程的规范性。稍后我们将逐一详述这些分支。

从技术角度而言,这些分支绝非“特殊”存在。分支类型是根据我们的使用方式进行划分的,它们本质上都是最基础的Git分支。

Feature branches (功能分支)

功能分支(有时称为主题分支)用于为即将发布或远期规划版本开发新功能。当启动某个功能的开发时,该功能最终将融入哪个目标版本可能尚未确定。功能分支的本质在于,其生命周期与功能开发周期同步:只要功能处于开发阶段,该分支就会持续存在,并最终被合并回develop分支(以确保新功能被纳入即将发布的版本),或在实验未达预期时予以废弃。

功能分支通常仅存在于开发者个人的代码库中,而非origin内。

创建功能分支

在着手开发新功能时,请从开发分支创建新分支。

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

将已完成的功能合并到开发分支

已完成的功能可以合并到开发分支,以明确将其纳入即将发布的版本中:

  $ 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 参数会强制合并操作始终创建新的提交对象,即使该合并可以通过快进方式完成。这种做法既保留了特性分支在历史中的存在痕迹,也将实现某个特性的所有提交归整到同一组。对比分析如下: withoutff

在后一种情况下(未使用—no-ff),我们无法从Git历史中直观看出哪些提交对象共同实现了某个特性 —— 必须手动阅读所有日志信息。当需要回滚整个特性(即一组提交)时,后一种情况会带来巨大麻烦,而使用 —no-ff 标记后此操作将变得简单易行。

虽然这会创建更多(空)提交对象,但其收益远大于代价。

发布分支

发布分支用于支持新生产版本的准备工作。它们允许进行最后的细节调整,同时支持小规模错误修复及版本元数据准备(版本号、构建日期等)。通过在发布分支上完成这些工作,develop分支可保持清洁状态以接收下一个重大版本的新特性。

从开发分支(develop)创建新发布分支(release branch)的关键时间点,是当开发分支(几乎)达到新发布的理想状态时。此时至少所有计划纳入该版本的功能都必须已完成合并。而针对未来版本的功能则不可并入——这些功能必须等待发布分支创建完成后才能合并。

正是在发布分支创建的这一刻,即将发布的版本才会被分配版本号——绝不会提前分配。在此之前,开发分支始终反映的是”下一个版本”的变更内容,但该”下一个版本”最终会成为0.3还是1.0,直到发布分支创建时才会明确。这个决策将在发布分支创建时根据项目的版本号变更规则确定并执行。

创建发布分支

发布分支通常从开发分支(develop)中创建。例如,假设当前生产环境的版本是1.1.5,而我们即将进行一次重大发布。此时开发分支的状态已经为”下一次发布”做好准备,且团队决定新版本将升级为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

发布现已完成,并已打上标签以便将来参考。

提示:你可能还想使用 -s 或 -u <密钥> 标志来对您的标签进行加密签名。

不过,为了保留发布分支中所做的更改,我们需要将其合并回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).

热修复分支

热修复分支与发布分支极为相似,它们都是为新的生产版本发布做准备,尽管热修复通常源于计划外的紧急情况。这类分支的出现是由于线上生产版本出现意外问题需要立即处理。当生产环境中发现必须立刻解决的关键性缺陷时,可以从标记生产版本的master分支对应标签处创建热修复分支。

其核心价值在于:当某位成员正在紧急修复生产环境问题时,其他团队成员可以继续在develop分支上正常开展工作。

创建热修复分支

热修复分支基于主分支(master)创建。例如,假设当前线上运行的1.2版本生产环境因严重缺陷引发故障,而开发分支(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(-)

在分支创建后,请记得更新版本号!

随后修复该错误,并通过一次或多次独立提交来完成修复。

$ 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分支,以确保修复内容能被包含在下一版发布中。此流程与完成发布分支的处理方式完全一致。

首先,更新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 <密钥> 标志来对您的标签进行加密签名。

接下来,请将修复也合并到develop分支中:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)

此规则的一个例外情况是,**当目前存在发布分支时,热修复变更需合并至该发布分支而非开发分支。**将错误修正反向合并至发布分支,最终会在该发布分支完成时使修正内容同步至开发分支。(若开发分支中的工作急需此错误修正且无法等待发布分支完成,您也可以立即安全地将修正内容合并至开发分支。)

最后,请删除临时分支:

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

总结

尽管这一分支模型并未带来真正惊人的革新,但本文开篇所展示的”全景图”在我们的项目中已被证明极具价值。它构建了一个易于理解的优雅模型,让团队成员对分支与发布流程形成统一认识。