摘要

介绍 Git 的主要分支、功能分支、发布分支和热修复分支,以及它们的协作与合并流程。

关键词

Git、版本控制、分支管理、发布流程

为什么使用 Git?

讨论 Git 与集中式版本控制系统的优缺点,可以从开发者的实际体验出发。对我来说,Git 是当下最喜欢的版本控制工具,它改变了开发者看待分支与合并的方式。在过去使用 CVS、Subversion 时,分支和合并往往让人担心,也只是偶尔才会进行的操作。

Git 让分支与合并变得简单而日常,不再令人畏惧。版本控制工具本就应该帮助开发者方便地管理这些操作。

分布式协作与中心仓库

这种分支模型采用一个约定上的中心“事实”仓库。由于 Git 是分布式版本控制系统,技术上并不存在真正的中心仓库;这里只是在协作中赋予它中心地位。我们沿用 Git 用户熟悉的名字,将其称为 origin

每位开发者都从 origin 拉取并向其推送修改。除了这种集中式同步关系,开发者也可以彼此拉取变更,组成小组协作。

例如,两位或多位开发者共同开发大型新功能时,可以先在小组内协作,避免过早把未完成的工作推到 origin。上图展示了 Alice 与 Bob、Alice 与 David,以及 Clair 与 David 的协作关系。

从技术上说,这只是 Alice 添加了名为 bob、指向 Bob 仓库的 Git 远程地址,Bob 也进行了对应设置。

两条主要分支

该开发模型借鉴了既有实践。中心仓库包含两条长期存在的主要分支:

  • master
  • develop

Git 用户通常都熟悉 origin 上的 master 分支。与它并行的另一条长期分支是 develop

origin/masterHEAD 应始终保持可用于生产发布的状态。

origin/developHEAD 包含为下一次发布准备的最新开发变更,因此也称“集成分支”。自动化的每夜构建通常从这里产生。

develop 的代码稳定且准备发布时,应将变更合并回 master,并打上版本标签。具体操作将在后文说明。

按照这一约定,每次合并到 master 都意味着一次新的生产发布。如果严格遵守,就可以通过 Git hook 在 master 出现新提交时自动构建并部署到生产服务器。

辅助分支

除了 masterdevelop,模型还使用辅助分支,支持团队并行开发、功能追踪、发布准备以及线上紧急修复。这些分支都有有限的生命周期,完成使命后会被删除。

辅助分支包括:

  • 功能分支。
  • 发布分支。
  • 热修复分支。

每类分支都有明确用途,并严格约定从哪里创建、最终合并到哪里。

这些分支在技术上并无特殊之处,仍是普通 Git 分支,区别仅在于使用方式。

功能分支

可以从以下分支创建:

  • develop

必须合并回:

  • develop

命名规则:除 masterdeveloprelease-*hotfix-* 之外的名称。

功能分支有时也称主题分支,用于开发近期或未来版本中的新功能。开始开发时,未必已经确定它属于哪次发布。功能开发期间保留该分支,完成后合并到 develop,纳入后续版本;如果实验结果不理想,也可以将其丢弃。

功能分支通常只存在于开发者自己的仓库中,不放在 origin

创建功能分支

开始开发新功能时,从 develop 创建分支:

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

将完成的功能合并到 develop

完成的功能可以合并进 develop,纳入下一次发布:

1
2
3
4
5
6
7
$ 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 则容易得多。

这种做法会增加少量合并提交,但其收益明显大于成本。

发布分支

可以从以下分支创建:

  • develop

必须合并回:

  • developmaster

命名规则:

  • release-*

发布分支用于为新的生产版本做最后准备,包括收尾、小范围缺陷修复,以及版本号、构建日期等元数据更新。这些工作独立在发布分支完成后,develop 就可以继续接收下一个大版本的功能。

当 develop 已经基本达到本次发布目标时,就适合创建发布分支。至少,本次计划发布的全部功能应已合并进 develop

版本号在创建发布分支时才正式确定。在此之前,develop 代表的是“下一个版本”的变化,但这个版本最终叫 0.3 还是 1.0,要到创建发布分支时才明确。

创建发布分支

发布分支从 develop 创建。例如,当前线上版本为 1.1.5,develop 已经准备好下一次较大的发布,并决定版本号为 1.2,就创建对应名称的分支:

1
2
3
4
5
6
7
$ 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 是一个示例脚本,用来修改工作副本中的文件,反映新的版本号。也可以手工修改,关键是将版本号变更提交。

发布分支可以保留一段时间,直到正式上线。这期间在该分支修复缺陷,而不是在 develop 上进行。严禁在这里加入大型新功能;新功能应进入 develop,等待下一个大版本。

完成发布分支

发布分支准备就绪后,先将其合并进 master,因为按约定 master 的每次提交都对应发布;再为 master 上的提交打标签,方便以后查找该历史版本;最后将发布分支的变更合并回 develop,确保后续版本也包含这些修复。

前两步的 Git 操作:

1
2
3
4
5
$ 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

1
2
3
4
$ git checkout develop
# Switched to branch 'develop'
$ git merge --no-ff release-1.2
# Merge made by recursive. (Summary of changes)

这一步可能出现合并冲突,尤其是已经修改了版本号时。如有冲突,应解决后提交。

至此发布流程完成,不再需要的发布分支可以删除:

1
2
$ git branch -d release-1.2
# Deleted branch release-1.2 (was ff452fe).

热修复分支

可以从以下分支创建:

  • master

必须合并回:

  • developmaster

命名规则:

  • hotfix-*

热修复分支与发布分支相似,也用于准备生产发布,但这种发布并非事先计划。当线上版本出现必须立即修复的严重问题时,可以从 master 上对应生产版本的标签创建热修复分支。

它的关键作用是:一位成员准备紧急修复时,其他成员仍可在 develop 上继续正常开发。

创建热修复分支

热修复分支从 master 创建。例如,线上运行的 1.2 版本出现严重缺陷,而 develop 的改动尚不稳定,就可以这样创建分支并修复:

1
2
3
4
5
6
7
$ 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(-)

创建分支后,不要忘记更新版本号。

然后修复问题,通过一个或多个独立提交保存修改。

1
2
3
$ git commit -m "Fixed severe production problem"
# [hotfix-1.2.1 abbe5d6] Fixed severe production problem
# 5 files changed, 32 insertions(+), 17 deletions(-)

完成热修复分支

修复完成后,既要合并回 master,也要合并回 develop,确保下一次发布同样包含该修复。这与完成发布分支的流程基本一致。

首先更新 master,并为新版本打标签:

1
2
3
4
5
$ 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

1
2
3
4
$ 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 的工作立即需要这项修复、无法等待发布分支结束,也可以提前将修复安全地合并到 develop。

最后删除临时热修复分支:

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

总结

这种分支模型并没有惊人的新概念,但文章开头的全景图在项目中非常有用。它提供了清晰、易理解的模型,让团队成员对分支与发布流程形成共同认识。

这里提供该图的高清 PDF:gitflow-model.pdf。可以打印出来放在手边,随时参考。