Git 分支模型

我们公司的软件产品迭代采用的是scrum敏捷开发流程,代码使用git进行版本管理。在新人最初的几次开发任务中,我对于git的使用也仅限于一些基本的命令,包括:add、commit、rebase、cherry-pick、push、checkout等等。

直到有一天,我发现远程分支上存在着一些历史feature分支,这对于我这个初入职场的小白来说还是有些好奇:feature分支在本地建立不就行了吗?为何还需要推送到远程仓库?

带着这一些列的疑问,我仔细研究了一些我们基于gerrit的code review流程,终于明白了为何会有feature远程分支。这一切都与git的分支模型有关。
期间,我在一个英文博客上看到了一篇关于git分支模型的介绍,看完后觉得不错,对其进行简要地整理,以呈现给大家。

分权集中

下图中心的仓库,是我们建立并在使用的仓库,具有分支模型,其通常会被认为是“真正的中心仓库”。然而,事实上,其仅仅是被认为是中心仓库而已,因为git作为一个分布式版本控制系统(DVCS),在技术层面并不存在哪个仓库是中心仓库。而这个被认为是“中心”的仓库,我们更愿意称之为origin,这个名字对于所有git用户都是很熟悉的。

每个开发者都会从origin进行pull或向其进行push操作。但是除了与origin具有push-pull关系之外,开发者们还可能从其他同级的伙伴那里pull最新的改动,从而形成一个sub team。比如,对于两个以上的开发者,在过早地向origin推送开发进展之前,其可以开辟一个新的feature分支来共同工作。如上图所示,存在着这样几个开发小组:Alice & Bob,Alice & David,Clair & David。

从技术角度而言,这仅仅意味着Alice定义了一个远程仓库,名字是Bob,其指向了Bob的仓库,反之亦然,仅此而已。

主分支

归根到底,开发模型也受到了上述思想的影响。在中心仓库中,在其无限的生命周期中,始终存在着两条主分支:

  • master
  • develop

大家对origin上的master分支应该并不陌生。而另一个与之平行的分支,我们称之为develop分支。

origin/master:在这个分支上,源代码的HEAD指针的指向始终都是就绪/可发布的产品状态。

origin/develop:在这个分支上,源代码的HEAD指针的指向始终都是下一个版本的产品状态。

develop分支的源代码到达某一个可发布的稳定点时,所有的改动都应该合并到master分支,然后打上版本的tag标签。

因此,每次将改动合并至master分支时,意味着一个新版本的诞生。我们往往对这个过程控制得非常严格。所以每次master分支上有commit时,我们都应该使用git hook脚本来自动编译、发布软件至产品服务器上。

支持分支

与主分支masterdevelop相邻的则是各种支持分支,用于帮助团队成员之间进行平行开发,跟踪功能,准备产品发布以及帮助修复在线产品的一些Bug。与主分支不同,这些分支总是具有有限的生命周期,因为它们最终都是要被删除的。

我们可能用到的几种不同的支持分支:

  • Feature分支
  • Release分支
  • Hotfix分支

上述的每一个分支都具有特定的目的,并且必须遵守严格的规则。比如:哪些分支可以是它们的源头分支,哪些分支必须是它们的合并目标。

当然,从技术角度来说,这些分支并没有什么特殊之处。所谓的分支类型只是我们根据如何使用它们而进行分类的。

Feature分支

规则
> 可以源自develop分支
> 必须合并到develop分支
> 命名:除masterdeveloprelease-*hotfix-*之外

Feature分支(也称:topic分支)用来为即将发布的版本或更远的版本开发新的feature。当开发一个新的功能的时候,我们不知道这个功能会被纳入哪个目标版本。Feature分支的本质就是,只要该功能处于开发阶段,feature分支就会存在,并最终会被合并至develop分支(以确保将新功能添加到即将发布的版本中)或者丢弃(在实验失败的情况下)。

Feature分支通常只存在与开发者自己的仓库中,而不是origin

创建feature分支

当开始开发一个新的功能时,需要从develop主分支中开辟一个新分支。

1
$ git checkout -b myfeature develop //表示切换到一个新的分支“myfeature”

将完成的功能纳入develop

完成的功能可以合并至develop分支,以加入即将发布的版本之中。

1
2
3
4
$ git checkout develop  //切换至develop分支
$ git merge --no-ff myfeature //
$ git branch -d myfeature //删除myfeature分支
$ git push origin develop

--no-ff选项会在合并分支时创建一个新的commit对象,即使合并可以是一次fast-forward操作。这可以避免丢失feature分支的历史存在信息,并将该feature分支上的所有commit放在在一起。

如上图所示,相比较而言,后面一种情况是不可能从git历史记录中看到哪些提交对象一起实现了一个功能,你必须手动读取所有日志信息。在后一种情况下,恢复整个功能(即一组提交)是非常困难的,而如果使用了--no-ff选项,则很容易实现。

当然,这会创建一些空的commit对象,但收益远大于成本。

Release分支

规则 > 可以源自develop分支
> 必须合并到develop分支和master分支
> 命名:release-*

Release分支用于支持准备新的产品版本,它们允许对版本进行小错误修复和元数据准备(版本号,构建日期等)。通过在Release分支上进行所有这些工作,develop分支会被清理以接收下一个大版本的功能。

develop分支达到了新版本的期望状态时,即可从develop分支开辟新的release分支。当然,必须要等到所有待发布的功能合并至develop分支之后才可以。

正是在release分支的开始,即将发布的版本会被分配版本号。直到这一刻起,develop分支“下一个版本”的改动。但是“下一个版本”会变为0.3还是1.0,需要等到release分支开始才知道。这是在release分支开始时,由版本规则决定的。

创release分支

Release分支从develop分支创建而来。例如,1.1.5版本是当前的产品版本,我们即将有一个大的版本。develop已经为“下一个版本”准备就绪了,并且我们已经决定这将成为1.2版本(而不是1.1.6或2.0)。所以我们开辟一个分支,并予以相应的版本号。

1
2
3
$ git checkout -b release-1.2 develop å
$ ./bump-version.sh 1.2
$ git commit -a -m "Bumped version number to 1.2"

结束release分支

当release分支的状态已经准备好成为y一个真正的版本,需要完成一些操作。首先,将release分支合并至master分支(master上的每一个commit都是一个新的版本)。然后,master上的commit必须被打上标签,以便参考历史版本。最后,将在release分支上作出的改动合并到develop分支上,以便未来的版本还包含这些错误修复。

1
2
3
4
5
6
7
8
9
10
// step 1
$ git checkout master å
$ git merge --no-ff release-1.2
// step 2
$ git tag -a 1.2
// step 3
$ git checkout develop
$ git merge --no-ff release-1.2
// 完成后,我们可以将release分支删除
$ git branch -d release-1.2

Hotfix分支

规则 > 可以源自develop分支
> 必须合并到develop分支和master分支
> 命名:hotfix-*

Hotfix分支与release分支非常相似,其也是用于为新的产品版本做准备,尽管是计划之外的。它们是对发布版本的不良状态作出的回应。当产品版本中的一个关键bug必须要被修复时,可以从master分支上相应的tag标签中开辟一个hotfix分支。

Hotfix分支的本质是可以使develop分支上的工作得以继续,而另外有人进行bug修复。

创建hotfix分支

Hotfix分支从master分支创建而来。例如,1.2版本是当前产品的版本号,正在在线运行,由于服务器bug出现了一些问题。但是develop分支上的改动还不稳定。我们需要开辟一个hotfix分支来进行bug修复。

1
2
3
4
5
$ git checkout -b hotfix-1.2.1 master
$ ./bump-version.sh
$ git commit -a m "Bumped version number to 1.2.1"
// 修复bug并commit
$ git commit -m "Fixed server production problem"

结束hotfix分支

当完成bug修复之后,hotfix分支需要合并到master分支,当然也需要合并至develop分支,这与release分支是非常相似的。

1
2
3
4
5
6
7
8
9
// step 1: 合并至master,打上版本标签
$ git checkout master
$ git merge --no-ff hotfix-1.2.1
$ git tag -a 1.2.1
// step 2: 合并至develop
$ git checkout develop
$ git merge --no-ff hotfix-1.2.1
// step 3: 删除hotfix分支
$ git branch -d hotfix-1.2.1

这里有一个例外需要注意,当一个release分支当前还存在时,hotfix分支的修改应该合并至release分支,而不是develop分支。因为release分支完成后,需要合并至develop分支。