<译>一个成功的分支模型


我是个俗气至顶的人,见山是山,见海是海,见花便是花。唯独见了你,云海开始翻涌,江潮开始澎湃,昆虫的小触须挠着全世界的痒。你无需开口,我和天地万物便通通奔向你。
——王小波《爱你就像爱生命》

前言

正文

  • 在这篇文章中,我将展示我一年前曾经介绍过的用于我部分项目(无论是在工作还是私人)的开发模式,并且已经证明是非常成功的。我有写这篇文章的打算已经很久了,但直到今天我才有时间彻底的完成它。我不会谈论关于项目的任何细节,仅仅讨论关于分支策略和发布管理。
    git-model

为什么git?

  • 关于专业人士的全面讨论以及Git和集中的源代码控制系统的利弊比较,请这里。那是一个充满了硝烟的战场。作为开发人员,如今我喜欢Git要远超其他工具。Git真正的改变了开发人员想要合并和构建分支的方式。我从经典的CVS / Subversion世界一路走来,合并/分支一直被认为是一个可怕的(“合并冲突要小心,他们会咬你!”)而且这些事你仅仅偶尔做一次。
  • 但是使用Git,这些操作非常简洁和高效,它们也被称作日常工作流程的核心部分之一。例如,在CVS / Subversion 书籍中,分支和合并将在后面的章节(高级用户)中优先讨论,而在 每一本 Git 书中,它已经在第3章(基础知识)中讨论过。
  • 由于其简单性和重复性,分支和合并不再是令人害怕的事情。版本控制工具可以帮助快速的进行分支/合并。
  • 工具到此为止,让我们来看看开发模型。我在这里展示的模型本质上只是一系列的步骤,每个团队成员都必须遵循这些过程来实现可管理的软件开发过程。

分散但集中

  • 我们使用的,且与这个分支模型配合良好的仓库,他有一个“真正”的中央仓库。注意,这个库只是被认为是中央仓库(因为Git是一个分布式的版本控制工具,在技术层面没有所谓的中央仓库)。我们将会为这个仓库起名为origin,因为所有的Git用户对这个名字都比较熟悉。
    centr-decentr
  • 每个开发者从origin拉取和推送代码。除了集中式的推送拉取外,每个开发者也可以从别的开发者处拉取代码,组成一个子团队。例如当与两个或者更多的人开发一个大的功能时,在将代码推送到origin之前,这种代码管理模式将非常有用。在上图中,存在Alice和Bob,Alice和David,Clair和David三个子团队。

  • 技术上而言,这只不过意味着Alice定义了一个远程Git仓库,起名为bob,实际上指向Bob的版本库,反之亦然(Bob定义了一个远程Git仓库,起名为alice,实际上指向Alice的版本库)。

主分支

  • 在核心地方,当前开发模型受到了已存在模型的很大启发。集中式的版本库有两个永久存在的主分支:
    • master分支
    • develop分支

main-branches

  • origin的master分支每个Git用户都很熟悉。平行的另外一个分支叫做develop分支。

  • 我们认为origin/master这个分支上HEAD引用所指向的代码都是可发布的。

  • 我们认为origin/develop这个分支上HEAD引用所指向的代码总是反应了下一个版本所要交付功能的最新的代码变更。一些人管它叫“整合分支”。它也是自动构建系统执行构建命令的分支。

  • 当develop分支上的代码达到了一个稳定状态,并且准备发布时,所有的代码变更都应该合并到master分支,然后打上发布版本号的tag。具体如何进行这些操作,我们将会讨论

  • 因此,每次代码合并到master分支时,它就是一个人为定义的新的发布产品。理论上来讲,在这里我们应该非常严格,当master分支有新的提交时,我们应该使用Git的钩子脚本执行自动构建命令,然后将软件推送到生产环境的服务器中进行发布。

辅助性分支

  • 紧邻master和develop分支,我们的开发模型采用了另外一种辅助性的分支,以帮助团队成员间的并行开发,特性的简单跟踪,产品的发布准备事宜,以及快速的解决线上问题。不同于主分支,这些辅助性分支往往只要有限的生命周期,因为他们最终会被删除。

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

    • 特性分支
    • Release分支
    • Hotfix 分支
  • 上述的每一个分支都有其特殊目的,也绑定了严格的规则:哪些分支是自己的拉取分支,哪些分支是自己的目标合并分支。
  • 从技术角度看,这些分支的特殊性没有更多的含义。只是按照我们的使用方式对这些分支进行了归类。他们依旧是原Git分支的样子。

功能分支

  • 功能分支可以从develop分支拉取建立,最终必须合并会develop分支。特性分支的命名,除了 master, develop, release-*,或hotfix-*以外,可以随便起名。
    Feature branches

  • 功能分支(有时候也成主题分支)用于开发未来某个版本新的特性。当开始一个新特性的开发时,这个特性未来将发布于哪个目标版本,此刻我们是不得而知的。功能分支的本质特征就是只要特性还在开发,他就应该存在,但最终这些功能分支会被合并到develop分支(目的是在新版本中添加新的功能)或者被丢弃(它只是一个令人失望的试验)

  • 功能分支只存在开发者本地版本库,不在远程版本库。

创建功能分支

  • 当开始开发一个新功能时,从develop分支中创建功能分支
1
2
$ git checkout -b myfeature develop
Switched to a new branch "myfeature"

在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方式的合并。这就避免了丢失功能分支的历史记录信息以及提交记录信息。比较一下

  • 在后面的例子中,是不可能从Git历史记录中看到一个已经实现了的功能的所有提交对象-除非你去查看所有的日志信息。要想获取整个功能分支信息,在右面的例子中的确是一个头疼的问题,但是如果使用--no-ff参数就没有这个问题。

  • 使用这个参数后,的确创建了一些新的提交对象(那怕是空提交对象),但是很值得。
  • 不幸的是,我还没有找到一种方法使Git默认的merge操作带着--no-ff参数,但的确应该这样。

发布分支

  • 从develop分支去建立Release分支,Release分支必须合并到develop分支和master分支,Release分支名可以这样起名:release-*

  • Release分支用于支持一个新版本的发布。他们允许在最后时刻进行一些小修小改。甚至允许进行一些小bug的修改,为新版本的发布准要一些元数据(版本号,构建时间等)。通过在release分支完成这些工作,develop分支将会合并这些特性以备下一个大版本的发布。

  • 从develop分支拉取新的release分支的时间点是当开发工作已经达到了新版本的期望值。至少在这个时间点,下一版本准备发布的所有目标特性必须已经合并到了develop分支。更远版本的目标特性不必合并会develop分支。这些特性必须等到个性分支创建后,才能合并回develop分支

  • 在release分支创建好后,就会获取到一个分配好即将发布的版本号,不能更早,就在这个时间点。在此之前,develop分支代码反应出了下一版本的代码变更,但是到底下一版本是 0.3 还是 1.0,不是很明确,直到release分支被建立后一切都确定了。这些决定在release分支开始建立,项目版本号等项目规则出来后就会做出。

创建release分支

  • 从develop分支创建release分支。例如1.1.5版本是当前产品的发布版本,我们即将发布一个更大的版本。develop分支此时已经为下一版本准备好了,我们决定下一版的版本号是1.2(1.1.6或者2.0也可以)。所以我们创建release分支,并给分支赋予新的版本号:
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是一个虚构的shell脚本,它更改了工作空间的某些文件来反映新版本特征。(当然也可以手动改变这些文件),然后版本就被提交了。
  • 新的分支会存在一段时间,直到新版本最终发布。在这段时间里,bug的解决可以在这个分支进行(不要在develop分支进行)。此时是严禁添加新的大特性。这些修改必须合并回develop分支,之后就等待新版本的发布。

完成一个release分支

  • 当release分支的准备成为一个真正的发布版本时,一些操作必须需要执行。首先,将release分支合并回master分支(因为master分支的每一次提交都是预先定义好的一个新版本,谨记)。然后为这次提交打tag,为将来去查看历史版本。最后在release分支做的更改也合并到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
  • 这样release分支已经完成工作,tag也已经打了。
  • 备注:你可以使用-s or -u 参数为你的tag设置标签签名。
  • 为了保存这些在release分支所做的变更,我们需要将这些变更合并回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)
  • 这步有可能会有合并冲突(极有可能,因为我们已经改变了版本号)。如果有冲突,解决掉他,然后提交。
  • 现在我们已经完成了工作,release分支可以删除了,因为我们不在需要他:
1
2
$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).

Hotfix分支

  • Hotfix分支从master分支建立,必须合并回develop分支和master分支,为Hotfix分支可以这样起名:hotfix-*
  • Hotfix分支在某种程度上非常像release分支,他们都意味着为某个新版本发布做准备,并且都是预先不可知的。Hotfix分支是基于当前生产环境的产品的一个bug急需解决而必须创建的。当某个版本的产品有一个严重bug需要立即解决,Hotfix分支需要从master分支上该版本对应的tag上进行建立,因为这个tag标记了产品版本
    Hotfix分支

创建hotfix分支

  • Hotfix分支从master分支进行创建。例如当前线上1.2版本产品因为server端的一个Bug导致系统有问题。但是在develop分支进行更改是不靠谱的,所以我们需要建立hotfix分支,然后开始解决问题:
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(-)

结束hotfix分支

  • 完成工作后,解决掉的bug代码需要合并回master分支,但同时也需要合并到develop分支,目的是保证在下一版中该bug已经被解决。这多么像release分支啊。
  • 首先,对master分支进行合并更新,然后打tag
    • 备注:你可以使用-s or -u 参数为你的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
  • 紧接着,在develop分支合并bugfix代码
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)
  • 这里可能会有一些异常情况,当一个release分支存在时,hotfix 分支需要合并到release 分支,而不是develop分支。当release分支的使命完成后,合并回release分支的bugfix代码最终也会被合并到develop分支。(当develop分支急需解决这些bug,而等不到release分支的结束,你可以安全的将这些bugfix代码合并到develop分支,这样做也是可以的)。
  • 最后删除这些临时分支
1
2
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).

总结

  • 这个分支模型其实没有什么震撼人心的新东西,这篇文章开始的那个“最大图片”已经证明了他在我们工程项目中的巨大作用。它会形成一种优雅的理想模型,而且很容易理解,该模型也允许团队成员形成一个关于分支和版本发布过程的相同理念。
  • 这里有提供一个高质量的分支模型图的PDF版本。去吧,把它挂在墙上随时快速参考。

    pdf

    Git-分支-模型.pdf
  • 更新:任何需要的小伙伴,这里有一个原图的gitflow-model.src.key文件。

参考

要不要鼓励一下😘