最近有同事问到git的分支到底该如何管理,就想起很早看过的一篇文章,这篇文章讲到的方法受到很多人的推崇并运用到实践中。今天回顾了一下,大致总结一下这个模型的分支策略和发布管理的方法。

这个模型将分支分为主要分支(masterdevelop)和辅助分支(特性分支、发布分支、热修复分支)。

先上图:

git-model@2x

去中心化与中心化

这个模型中,我们需要一个唯一的中心仓库(从技术上说,git是去中心化的,并没有中心仓库的概念),我们命名为origin,这个我们都很熟悉。

centr-decentr@2x

每一个开发人员都可以从origin中拉取和推送代码,也允许开发人员相互之间进行代码的拉取和推送,形成小项目组来开发一个新功能。在新功能开发完成之前,项目小组不必将代码提交到origin。在上图中,alice和bob,alice和david,clair和david分别组成三个小组。

技术上,alice需要定一个git远程仓库,命名为bob,并连接到bob的本地代码仓库,其他小组成员也一样。

主要分支

main-branches@2x

这个和我们平时使用的简单开发模型类似,在中心仓库中包含两个主要分支,这些分支在项目生命周期一直存在:

  • master分支
  • develop分支

master主分支是git用户常用的名称,与master主分支同时存在的分支,称之为develop开发分支。

origin/master作为主要分支之一,因为这个分支上的最新代码总是代表着生产环境的最新发布版本。

origin/develop分支作为另外一个主要分支,这是因为这个分支上的最新代码代表着开发人员为下一次发布提交的最新代码。有时我们会把它称为“集成分支”,我们可以用自动集成工具来发布每日构建。

当develop分支功能被开发完成,并准备发布的时候,所有的变更都应该被合并到master分支,同时用tag来标记一个版本号。

也就是说,当在master分支上出现新的代码提交或者合并的时候,就代表一个新的生产发布需求产生。在实际开发工作中,我们可以通过git的webhook定义脚本,自动检测master分支的变更,以达到我们自动编译和自动发布的需求。

辅助分支

辅助分支主要是用于团队成员的并行开发,包括新功能开发、生产发布准备、生产环境问题修复等。与主要分支不同的是,辅助分支一般在需要时被建立,生命周期结束后就被释放删除。

辅助分支分以下几类:

  • Feature branches(特性分支)
  • Release branches(发布分支)
  • Hotfix branches(热修复分支)

每一种分支都有特殊用途,对分支的创建和合并进行严格定义,它们只能从指定的源分支建立,然后合并到指定的目标分支。

特性分支

  • 分支来源: develop
  • 合并目标: develop
  • 命名规范: 可以是任何名字,但是不可以用 master, develop, release-* 或 hotfix-*

特性分支被用于即将开发或更长期的功能开发。新特性分支建立时,我们无法准确预知它的完成时间,所以新特性分支在该特性的开发过程中一直存在,当开发完毕后,就会被合并到develop分支,生命周期结束。

需要注意的是,特性分支一般只在开发人员的仓库中存在,不需要在origin中被创建。

1. 创建特性分支

当我们需要开始开发一个新功能时,我们要基于develop分支创建一个新的特性分支。

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

2. 将开发完成的新功能合并到develop

当新功能开发完成后,我们将特性分支合并到develop分支,准备下一次的发布。

1
2
3
4
5
6
7
8
$ 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)”方式进行。这样就可以避免在合并的时候丢失特性分支存在的历史信息。

merge-without-ff@2x

在右面的途中,特性分支的提交都被嵌入到了develop分支中,除了仔细阅读提交日志,我们没法把他们分离出来。当我们想要回顾这个特性分支做了哪些变更的时候,会是一件很麻烦的事情。但是如果我们加上了--no-ff标记,就可以很容易的解决这个问题。

这个会导致新创建一个合并的提交,但是带来的好处却是大大的。

发布分支

  • 分支来源: develop
  • 合并目标: develop和master
  • 命名规范: release-*

发布分支用于新版本发布前的准备工作,它允许我们在发布前,做一些必要的变动,比如少量bug的修改,元数据(如版本信息、编译参数等)的修改等。当所有工作完成之后,develop分支再将这些修改都合并过来,开始下一个大版本的开发工作。

分布分支只在我们决定发布新版本的时候被创建,从develop分支中拉取并标记新版本的发布状态信息。此时,所有准备上线的功能代码,都必须先合并到develop分支。针对未来的功能变动不得提交到此分支,它们必须等到下个发布计划再提交。

发布分支决定了新版本。在此之前,develop分支并不决定下一个版本的版本号,不清楚下一个版本应该是“0.3”还是“1.0”,直到下一个发布分支建立时,才能知道。版本号命名交由将由项目版本管理计划决定。

1. 创建一个发布分支

发布分支是基于develop分支创建。假设我们现在已经发布的版本是1.1.5,开发人员的开发工作已经完成,经过评估,我们认为这次变动较大,决定发布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脚本中,我们修改了一些版本变更要修改的文件,以便新版本生效,然后提交了代码。

新的发布分支会在这次发布完成之前一直存在,这个期间,可能会有一些bug需要修复。这里不允许提交大功能的改进代码,他们必须被合并到develop分支,然后等待下一个发布工作。

2. 完成发布分支

当一个发布分支已经准备发布时,需要进行如下操作。首先,该发布分支需要被合并至master分支(注意:所有master分支的提交都代表一个新的发布工作);然后,对master增加一个tag,以便未来对历史进行跟踪;最后,所有发布分支上的提交都需要合并回develop分支,确保下一个发布工作中包括了所有的bug修复。

前两项的git操作如下:

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

一次发布工作就这样完成了,并标记了tag,以供未来参考之用。

提醒: 在tag操作中,你可能需要增加-s 或 -u 参数,用来对tag代表的代码进行签名,防止纂改。

为确保下一次的发布中包括了本次发布分支中所有的变更,我们还需要在develop中进行合并工作,git命令行如下:

1
2
3
4
5
$ 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
  • 合并目标: develop和master
  • 命名规范: hotfix-*

热修复分支和发布分支相似,但是线上的bug修复并不是计划中的工作。所以热修复分支是基于线上运行的tag号创建分支,并以此为基础进行修改。

这样,这次bug修复就不会影响develop分支当前的开发工作。

1. 创建一个热修复分支

热修复分支基于master创建,比如我们目前线上运行的是1.2版本,不幸的是,现在线上出现了一个严重的bug。同时,我们的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(-)

创建分支的时候不要忘记声明新的版本号。

然后我们修复bug,并将代码提交:

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(-)

2. 完成热修复分支

当修复工作完成之后,代码重新被合并到master分支进行发布。同时要合并到develop分支,以确保下次发布时包含了本次修复。合并工作和发布分支操作类似。

首先,切换到master分支,合并,然后打tag标签。

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

提醒: 在tag操作中,你可能需要增加-s 或 -u 参数,用来对tag代表的代码进行签名,防止纂改。

接下来,在develop分支中合并该分支。

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

最后,移除这个临时的分支。

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

总结

这个也只是一个分支管理模型的案例,没有最好的,只有最合适的,上面介绍的这个适合多人开发,经常有并行开发需求,团队有各种统一标准的。

也许你的项目只有你一个人在开发并且不需要维护多版本,那你可能develop和master分支就足够满足需求。

也许你是为开源软件贡献代码,fork和pull request就能满足你的需求。

所以看你什么场景,什么规模,什么开发标准了。在常规的企业开发中,我想应该还是本文介绍的模型更适合。

参考