一个成功的 Git 分支模型(适用于商业应用开发)
在这篇文章中,我将推广一下大约一年前我介绍过的一些项目(公私皆有)中使用的开发模型,它们的结果都非常成功。有段时间我非常想写出来分享一下,但是我至今才抽出时间来。我不会言及任何项目细节,仅讨论分支策略和发布管理。
为何使用 git?
关于 Git 和集中式源码版本控制系统的优缺点对比讨论, 见 此 web。这里有很多精彩激烈的论战。作为一名开发者,现在我更偏好使用 Git 。Git 真的改变了开发者关于合并和分支的认知。我来自传统的 CVS/Subversion 世界,合并/分支是件恐怖的事情 (「小心合并冲突,它们会反噬你!」),而且大家偶尔才会做这件事。
但是使用 Git,这些操作就显得轻而易举,它们会成为你 日常 工作流的核心部分。例如,在 CVS/Subversion 手册中,分支和合并直到最后章节才首次出现(仅供高级用户参考),但是在 每个 Git 手册中,第三章就覆盖到了(基本上)。
因为具有简单和可重复的基因,分支和合并不再是什么值得担心的问题了。版本控制工具应该专注于分支/合并,而非其他事情。
关于工具的了解已经够了,那么我们就开始进入开发模型了。这个我要介绍的开发模型,不过是每个团队成员进入软件开发流程之前必须遵循的规范。
去中心化和中心化
基于一个「真正」中心库的这种分支模型可以让我们很好的协同工作。注意这个库仅仅是被认为一个中心库(因为 GIT 在技术角度并没有中心库这一说法)。我们将此仓库命名为 origin
,因为这名字被 Git 用户熟知。
!
所有开发者都从 origin
库拉取或向其推送代码。在中心化的推送-拉取关系之外,开发者也可以从其他的开发者库拉取代码更改。例如,为了开发一个大型功能,有多位开发者组成一个开发小组,在功能完成之前,开发过程不必推送至 origin
库。在上面的图中就有 Alice 和 Bob, Alice 和 David,还有 Clair 和 David 之间组成的小组。
技术层面,Alice 要在本地库加一个远程 Git 分支并命名为 bob
,指向 Bob 的仓库。其他人也一样。
主分支
git 的核心在开发模式上受到了现有模式的极大启发,中心仓库在整个生命周期保持了两个主要的分支:
master
develop
每个 Git 用户都对在 origin
的 master
分支很熟悉。 跟 master
分支并行的是另一个称为 develop
的分支。
我们称 origin/master
为主分支,这个分支源码的 HEAD
一直指向 可用于生产环境 的状态。
我们称 origin/develop
为主分支,这个分支源码的 HEAD
总是反映下一个版本的最新开发状况。有些人称这个分支为 "整合分支" 。所有的每日自动构建都是从这儿构建的。
当 develop
分支上的源代码达到一个稳定点并准备发布时, 所有的更改都应该以某种方式合并回 master
分支, 然后使用发行版本进行标注。 接下来将从细节上讨论这是如何完成的。
因此, 每次将变更合并回 master
分支时, 这是一个 根据定义 的新产品发布。 我们趋向于对此非常严格,因此理论上来讲, 我们可以在每次提交到 master
分支时, 使用一个 Git 钩子脚本来自动完成构建和发行我们的软件到生产服务器。
辅助分支
在讨论完 master
分支和 develop
分支后,将要讨论的多样化的辅助分支,支持成员间并行开发, 轻松跟踪功能开发、生产版本发布、还能快速修复生产环境中产生的 Bug 。和主分支不同的是,辅助分支只有有限的生命周期,通常在完成使命后会被删除。
可能用到的辅助分支分类有:
- 功能分支
- 发布分支
- 修复 Bug 分支
每个分支都有特殊的用途。这些分支的来源分支和他们要合并回的分支都有严格的定义。在后面我们再具体讨论。
技术上,辅助分支都没有特殊含义。我们就是根据具体使用功能对辅助分支进行分类。辅助分支和普通的 Git 分支没有区别。
功能分支
!
功能分支可能源自于:
develop
分支
功能分支必须合并回:
develop
分支
功能分支命名惯例:
任何名字都可以,但不能包含 master
, develop
, release-*
, 或者 hotfix-*
。
功能分支(或称为特性分支)是被用来开发新功能的,这些新功能是要即将上线或更长时间后发布的。功能分支创建后开始开发时,之后将要合并的时间点是不知道的。功能分支的精髓是伴随开发过程一直存在,但是肯定会被合并回 develop
(在下一个预期的发布版本中清晰的添加新功能 ) 或被丢弃 (万一实验不尽如人意)。
功能分支通常存在开发人员的仓库中,不会出现在 origin
仓库。
创建一个功能分支
当我们开始写一个新的功能时,请从 develop
分支中切换出来
$ git checkout -b myfeature develop
Switched to a new branch "myfeature"
在开发中加入已经完成的功能
完成的功能可能被合并进入 develop
分支中,以确保他们会被添加到即将发布的版本中去
$ 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-forwark方法进行提交。这就可以避免丢失功能分支的历史信息并且可以把所有的功能在叠加在一起提交上去,请看图片对比:
!
在后一种情况中,从 Git 历史中是无法查看到是哪几个提交对象在一起实现了一个功能 -——您必须手动读取所有日志消息。还原整个功能(即一组完整的提交)在后一种情况下是真的让人很头疼的事情,但是如果使用使用 --no-ff
标志,就可以很容易完成这个任务的。
当然啦,它虽然创作更多的空的提交对象,但是增益却远远大于成本。
发布分支
该分支可能源自于:
develop
分支
必须合并到:
develop
和 master
分支
分支命名习惯:
release-*
发布分支(Release branches) 支持新产品发布的准备。 它们允许在最后一刻追求细节。此外,它们允许小错误修复以及为发布准备元数据(版本号,构建日期等)。通过在发布分支上做的这些工作, develop
分支被清除以接收下一个大版本的功能特性。
从 develop
分支检出一个新发布分支的重要时刻就是当开发(基本上)反映了新版预期状态的时候。 至少,在那时,所有以『即将构建的发布版』( release-to-be-built )为目标的功能特性必须合并回 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
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
分支)。 严禁在此分支添加大的新功能特性。 这些分支必须合并回 develop
分支,然后,等待下一个大版本的到来。
完成并发布你的分支
当你真的准备好要发布分支的时候,还需要执行一些别的操作。首先,发行版必须合并进 master
分支中(一定确保每次提交到 master
分支的都是最新的版本)。接下来,请一定标记对 master
分支的更新记录,用于以后查看该版本时进行参考。最后, 发布新分支所做的更改需要重新合并为 develop
分支,以确保以后的版本也修复了这些错误。
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 <key>
来加密签名。
为了保持发布分支所做的更改一致,我们需要将这些更改合并到 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
分支命名惯例:
hotfix-*
热修复(hotfix)分支和发布(release)分支很像,因为它们都意味着即将有新的生产版本发布,尽管不是意料之中的。它们产生的原因是现有的生产版本出现了不受欢迎的情况,从而必须立即起作用。当生产版本中的一个严重的 bug 必须马上被修复,热修复分支可能从 master 分支上用于标记生产版本的对应标签上创建分支。
其本质是当有人准备对生产版本进行快速修复时,团队成员(在 develop
分支)可以继续工作。
创建修复 bug 分支
修复 bug 分支创建于 master
分支。 例如,1.2版本是当前生产环境的版本并且有 bug 。但是 develop
分支上的修改还不够稳定。这时我们可以创建一个修复 bug 分支来解决这个问题:
$ 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 ,一次提交或多次分开提交。
$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)
完成一个修复 bug 分支
当完成一个修复 bug 分支之后,bug 分支需要合并到 master
和 develop
分支上,以保证在下一版本中也包含该 bug 修复。 这与完成发布分支完全相似。
首先,更新 master
并对这次发布打上 tag 。
$ 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
分支里包含 bug 修复分支的改动:
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
对于上面的规则,有一点是例外的: 当发布分支已经存在时, bug 修复分支的改动应该合并到该发布分支,而不是 develop
分支。当发布分支完成的时候, 把 bug 修复分支反向合并到发布分支中,最终也会导致 bug 修复被合并到 develop
分支中去。(如果 develop
分支中的工作马上就要这个 bug 修复的改动并且不能等待发布分支完成,那么现在你也可以安全地将 bug 修复合并到 develop
分支中去。)
最后,移除临时分支:
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).
小结
虽然这种分支模式目前看来已经不是什么新鲜事了,但这篇文章开头的 「大图」已经证明,这种模式对我们的项目实实在在的是非常有用。它形成了一个优雅并且更加容易理解的模型,并且能加强团队中成员们对于分支和其释放过程的理解。
这里提供给大家上面大图更加清晰的 PDF 版本,建议把它打印出来挂在墙上膜拜并且以便开发过程中快速查看。
更新: 如果有人需要这张图片: 这里附上下载文件 gitflow-model.src.key
更多现代化 PHP 知识,请前往 Laravel / PHP 知识社区