A successful Git branching model是一篇有名的分支管理策略(Git-Flow)的学习,下面文章以这篇文章为基础,介绍Git-Flow工作流,顺便练一下英语。这篇不是简单的英文翻译,里面杂合了我的理解与外部参考。

参考:

前言

这篇文章诞生于10年前,那时Git刚诞生不久。在这10年里,许多团队都把它当成了“准则”和“灵丹妙药”。现在,越来越多的软件类型转向了web应用,而这些应用一般是持续交付的,而不是回滚式的,因此不需要支持多版本管理。如果软件开发团队正在做持续交付的软件,那么可以参考更简单的工作流(如GitHub 流)而不是非得使用git-flow。

当然了,学习前需要记住,世界上没有灵丹妙药。要考虑自己的情况选择自己的工作流。

为什么选择Git

Git改变了程序员对“合并/分支”的看法。以前(CVS/Subversion时代),“合并/分支”有点可怕:小心合并冲突,不然它会咬你!但有了Git,这些操作将变得成本低廉和简单。由于其简单和可重复性的特性,“合并/分支”不再可怕。

“去中心化”了但是还要“中心化”

因为Git是DVCS(分布式管理系统),在技术层面上就没有诸如”中心化“仓库的东西。但是要让我们的模型工作得好,首先就得有一个“中心”的“真实”的仓库。这个中心化的仓库我们称为origin(这个名字对Git用户来说比较熟悉)

每一个开发人员都从origin中拉取和推送,与此同时成员之间也互相拉取形成子团队。这适用于2到3个开发人员在对一个软件的重要特性的进行开发的场景。

上图中,alice和bob、alice和david、david和clair组成了子团队,这意味着Alice定义了一个远程Git仓库名为bob,反之亦然。

主分支

中心仓库掌握两个主要的分支,它们有着无限的生命周期:

  • master分支:主要分支,HEAD总是指向一个“可以作为产品发布”的状态。
  • develop分支:主要分支,HEAD总是指向为下一个版本做准备的最新交付开发版本,它包含了一些针对下一个版本的更改。
img

当develop中的源代码趋向稳定且做好发布准备时,所有的改变都应以某种方法“合并”回master,并打上版本标签。因此,每一次把所有改变合并回master,根据定义,这将是一个全新的发行版本。对于这一点,我们应严格遵守。所以从理论上来讲,每一次master分支存在提交,我们编写的Git Hook脚本会自动构建并导出我们的软件产品到产品服务器。

辅助分支

次于master和develop两个主分支,我们的开发模型使用许多辅助分支以帮助团队成员之间的平行开发、简化特性的追踪、为版本发行做好准备以及对产品临时出现的问题进行修复。与主分支不同,这些分支总是有着有限的生存周期,它们最终将会被永久地移除。

我们可能会用到的不同辅助分支:

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

这些分支有不同的目的,且各自有严格规则约束,这些规则规定了它们原分支和目的分支。从技术上来讲,这些分支并不十分“特殊”,它们都是一般的Git分支,只不过我们以“使用它的方式”进行分类罢了。

特性分支

可能从哪个分支分出来 必须合并回哪个分支去 习惯命名
develop develop 除了叫master、develop、release-*或hotfix-*,其他都可以

特性分支(有时也称主题分支)是用来开发即将到来或远期发行版本新特性的分支。当我们开始开发一个特性时,包含这个特性的发行版本是未知的。特性分支的本质是,只要这个特性还在开发中,它就一直存在,但最终会合并回develop(这就决定了新特性将加入即将发行的版本之中)或丢弃(以防最终的结果令人失望)。

特性分支一般只存在于开发中的仓库,而不在origin

img

创建特性分支

当我们开始在新特性上进行开发时,我们从develop中分出新分支

1
2
3
git checkout -b myfeature develop
# git checkout [-b <new-branch>] [<start-point>]
# Create a new branch named <new-branch>, start it at <start-point>, and check the resulting branch out

在develop分支中合并完成的特性

开发完成的特性将合并到develop分支中,这就决定了即将发行的版本将包含这个特性。

1
2
3
4
5
6
7
8
# 合并新特性
git checkout develop # Switched to branch 'develop'
git merge --no-ff myfeature
# 删除特性分支
git branch -d myfeature
# git branch -d <branchname>
# Delete a branch.
git push origin develop

–no-ff参数解释:

默认情况下,Git执行"快进式合并"(fast-farward merge,参考fast_forward),会直接将Master分支指向Develop分支。使用–no-ff参数后,会执行正常合并,在Master分支上生成一个新节点。为了保证版本演进的清晰,我们希望采用这种做法(即使这样做会产生空的commit结点,但利大于弊)。

img

预发行分支

可能从哪个分支分出来 必须合并回哪个分支去 习惯命名
develop develop,master release-*

预发布分支,它是指发布正式版本之前(即合并到Master分支之前),我们可能需要有一个预发布的版本进行测试。

预发行分支辅助新产品发行的准备工作,They allow for last-minute dotting of i’s and crossing t’s. 此外,它允许小错误的修复且为一个发行版本准备元数据(版本号、构建日期等)。通过所有在预发行分支所做的工作,develop分支将被清除以接收下一个大版本的发行。

从develop分支分叉处新的预发行分支的时刻,就是develop(几乎)反应新发行版本的理想状态的时刻,至少此时所有指向正在构建的发行版本的特性已经合并到develop中(但是指向未来发行版本的特性也许还没有合并进去,它们必须等到那个版本被分出来后,才考虑进行合并)。

正是在预发行分支存在开始,预发布的版本将被指定一个版本号(不会更早),一直到develop分支反映下一个发行版本变动的那一刻。但是,预发行版本的版本号最终是0.3还是1.0我们是不清楚的。版本号的决定是从预发行分支开始,并由项目的版本号制定规则决定。

创建一个预发行分支

预发行分支从develop分支分流出来的。例如,现在的产品版本为1.1.5,我们将有一个大的发行版本出现,develop分支也已经为下一个发行版本准备好了,而且我们决定这个版本将为1.2(而不是1.1.6或2.0)。所以我们分出一条预发行版本分支,其名字反映了新的版本号:

1
2
3
4
5
git checkout -b release-1.2 develop # 切换到新分支 "release-1.2"
./bump-version.sh 1.2 # 文件修改成功,版本升级至1.2
git commit -a -m "Bumped version number to 1.2"
# --all: Tell the command to automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected.
# -m <msg>: Use the given <msg> as the commit message.

在创建新分支并切换到它之后,我们提升了版本号。./bump-version.sh在这里是一个虚构的shell脚本,他将改变当前工作中的文件(副本)以反映新的版本(当然,你可以自己手动更改,就是一些文件的更改)。最后,提升的版本号将被提交。

新的分支也许会存在一段时间,直到这个发行版被推出。在这段时间中,release分支会有很多bug修复(而不是在develop分支中),但严格禁止在这里增加大的特性。大的特性应该合并到develop分支中,并等待下一个发行版本。

完成一个预发行分支

当预发行分支准备好发行时,我们必须进行一些操作。首先,预发行分支合并到master分支中(别忘了,master中的每次提交代表的是一个新的发行版本)。其次,master中的提交必须被贴上标签以便未来对这个历史版本的参考。最后,预发行分支中的改变需要合并回develop,这样未来的发行版本中将会包含这些bug修复。

在Git中提交的步骤:

1
2
3
4
5
6
7
git checkout master
git merge --no-ff release-1.2
git tag -a 1.2
# -a: Make an unsigned, annotated tag object
# 你还可以使用以下标签
# -s: Make a GPG-signed tag, using the default e-mail address’s key.
# -u <key-id>: Make a GPG-signed tag, using the given key.

预发行已经完成,且进行了标签标注以便未来的参考。

为了保证预发行分支中的改变得到保留,我们需要将其合并到develop分支。在Git中这样做:

1
2
git checkout develop # Switched to branch 'develop'
git merge --no-ff release-1.2

这个步骤可能会导致合并冲突(或者说是必定冲突,因为我们改变了版本号),如果真的这样那就修复它然后提交。

现在我们真的要完事了,这个分支我们已经不需要了,把它删掉:

1
git branch -d release-1.2

热修复分支

可能从哪个分支分出来 必须合并回哪个分支去 习惯命名
master develop,master hotfix-*
img

热修复分支非常像预发行分支,因为它也是为新产品发行做准备。它的出现是为了应对当前版本中出现的错误情况。当现行发行的产品出现一个紧急的Bug,它必须立即修复,这时热修复分支会从master分支中对应版本的tag分流出来。

它的本质就是要在develop进行开发的工作团队继续它们的工作,要另一个人去准备好修复这个Bug。

创建热修复分支

热修复分支可以从master分支中创建。例如,版本1.2是当前产品发行版本,然而出现了严重Bug。但是develop分支中的改变还是不稳定的,我们需要分支出热修复分支并着手开始解决问题。

1
2
3
git checkout -b hotfix-1.2.1 master # Switched to a new branch "hotfix-1.2.1"
./bump-version.sh 1.2.1 # 文件修改成功,版本升级至1.2.1
git commit -a -m "Bumped version number to 1.2.1"

别忘了分支出热修复分支后提升版本号。

大约提交几次commit后,修复好Bug。

1
git commit -m "Fixed severe production problem"

完成热修复分支

完成Bug修复后,需要把热修复分支合并到master中以及develop中,这是为了保证修复后的内容也被下一个版本包含。这就很像预发布分支完成后的操作。

首先,更新master并标注发行版本:

1
2
3
4
5
6
git checkout master
git merge --no-ff hotfix-1.2.1
git tag -a 1.2.1
# 你还可以使用以下标签
# -s: Make a GPG-signed tag, using the default e-mail address’s key.
# -u <key-id>: Make a GPG-signed tag, using the given key.

然后,把Bug的修复也合并到develop中:

1
2
git checkout develop # Switched to branch 'develop'
git merge --no-ff hotfix-1.2.1

有一个例外规则是:当预发布分支存在时,热修复中的改变需要合并到这个预发布分支,而不是develop。合并的预发布分支的修复,会在预发布分支完成后最终合并到develop中。当然,如果develop分支急需这个修复,你也可以安全的合并到develop中。

最后,移除热修复分支这个临时分支。

1
git branch -d hotfix-1.2.1

总结

如果这个分支模型没有什么重大改变,下面这张图将对你的项目非常有用。img

Author: Vincent Driessen

Original blog post: http://nvie.com/posts/a-succesful-git-branching-model

License: Creative Commons BY-SA

推荐阅读

约定式提交 (conventionalcommits.org)