posts

May 09, 2019

一个成功的Git分支模型

Estimated Reading Time: 2 minutes (353 words)

翻译自A successful Git branching model

这篇文章介绍了一种开发模型,适用于工作或私人项目,只讨论分支策略和发布管理,不会讨论任何项目的细节。

05a5b5adeab51cb58a4141ff.png

为何是Git?

你可以在这篇文章上查看关于Git与集中式源代码控制系统(centralized source code control systems)相比较的优缺点,讨论比较激烈。作为一个开发者,我更喜欢Git,而不是其他的所有工具。Git改变了开发者对于合并与分支的思考方式。从我之前所在的经典的CVS/Subversion世界来看,合并/分支一直被视为可怕的事情,每隔一段时间就会做一次,要小心合并冲突,它们会吃了你。

但在Git的世界里,这些动作极其廉价和简单,而且它们被视为日常工作流的核心部分。在CVS/Subversion中,首次介绍分支与合并是在最后一章(对于高级用户),然而Git的中是在第三章(基础)。

由于其天生的简单性重复性,分支和合并不再是一件令人害怕的事情。版本控制工具应该比其他任何东西更有助于分支/合并。

关于工具已经谈得足够了,让我们进入开发模型的讨论。我将介绍的模型基本上只是每个团队开发成员必须遵循的一组程序才能进入被管理的软件开发过程。

分散但集中

一个中心的真实的仓库,是我们用来与当前分支模型协作的。注:这个仓库仅仅被视为中心的一个,我们所用用户将其称作origin

330294da97c60462ed6d3e94.png

每个开发者从origin拉取(pull)和推送(push)。但是除了集中地推送拉取的关系,每个开发中也会从其他同事拉取变更来组成子团队,例如这在将工作进度永久推送到origin之前,两个或更多开发者协作开发一个新的功能很有用。在上图中,有三个子团队,分别是alice和bob、alice和david、david和clair。

从技术上讲,这意味着Alice定义了一个名为bob远程(remote),指向Bob的仓库,反之亦然。

主要分支

在核心部分,这个开发模型极大程度上受下面的模型启发。中央仓库含有了两个具有无限生命时间的分支:

origin的master分支应该对于所有Git用户都很熟悉。与master平行地,还存在一个称为develop的分支。

我们称origin/master是主要的分支,因为其HEAD的源代码总是在生产就绪状态

我们称origin/develop是主要的分支,因为其HEAD的源代码总是在下一个发布版本的最新交付的开发变更。有时也被称作集成分支,是所有自动夜间构建的来源。

develop分支的源代码达到稳定并且发布就绪的状态时,所有的变更应该被合并回到master,并被标记上一个发布编号。后面我们会讨论实现细节。

因此。每次将变更合并回master时,根据定义,这是i一个新的生产版本,对此应该非常严格。可以使用Git钩子脚本在每次有master提交时自动构建和推出软件到我们的生产服务器。

支持分支

在主分支masterdevelop之后,我们的开发模型还会用到一系列支持分支来用于团队成员之间的平行开发,使跟踪功能、准备生产发布和快速修复生产环境问题等。与主分支不同,这些分支通常的生命周期的有限的,因为它们最后会被删除。

我们用到的不同种类的分支是:

这些分支的没一个都有特定的目的,并且必须遵守关于哪些分支必须是它们的合并目标的严格规则。我们下面开始讨论它们。

从技术角度来看,这些分支不是特殊的,分支类型取决与我们如何使用它们,它们是普通的Git分支。

Feature分支

可以从develop分支创建,可以合并到develop,分支命名惯例:除了masterdeveloprelease-*或者hotfix-*之外的名称。

b86efa2a58a9872bf2584e55.png

Feature分支(或者有时被称作Topic分支)通常备用做开发近期或者远期新的特征。当开始开发一个特征时,可能还不知道此特征将会合并到哪一个发布。一个feature分支的本质是在其对应特征开发过程中存在,但最终会被合并回develop(确定地将该特征添加到近期的发布中)或者被忽略(比如一个令人失望的实验的情况)。

创建一个feature分支

当开始一个新特征时,从develop分支创建:

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

将一个完成的feature加入到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-forward)执行合并。这样避免了丢失有关feature分支历史存在的信息,并将所有一起添加功能的提交组合在一起。对比:

64339443c52c18e965148a98.png

在右边的情况下,不能从Git历史中看到那些提交对象一起实现了一个功能——只能读取日志消息。回滚整个feature(也就是一组提交)则是令人头疼的,但若使用了--no-ff标志,一切将会变得简单。

是的,它会创建一些(空白的)提交对象,但是收益远大于成本。

Release分支

develop分支创建,必须合并回developmaster,分支命名惯例release-*

Release分支支持了一个新的生产发布的准备。它们允许最后一刻的修改。此外,它们允许次要的BUG修复和为一次发布的元数据准备工作(版本号、构建日期等等)。通过在release分支上完成所有这些工作,develop分支将会是干净的以接受下一个大发布的feature。

从develop分支创建一个新的release分支的关键时刻是develop分支(很大程度上)是新的发布的期望状态。至少所有针对要构建按的发布的功能此时已经被合并到develop分支。针对未来发布的所有feature可能不会,它们必须等到它们对应的release被创建。

正是在一个release分支开始时,即将发布的发布将被分配一个版本号,而不是之前任何时间。直到release分支开始时,develop分支是下一个发布的变更,但是其版本号是0.3或是1.0才能确定。

创建一个release分支

Release分支从develop分支创建。例如,假设版本1.1.5是当前生产发布,而且我们有一个即将到来的大发布。develop分支的状态已经为下一个发布准备好了,我们已经确定这将是版本1.2(而不是1.1.6或2.0)。所以我们创建这个release分支,并且名为反映这个新版本号:

$ 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脚本,它可以更改工作副本中的某些文件以反映新版本。 (这当然可以是手动更改 - 关键是某些文件会发生变化。)然后,提交了有问题的版本号。

这个新的分支可能存在一段时间,直到发布可能肯定推出(rolled out)。在此期间,可以在此分支中应用错误修复(而不是在develop分支上)。严禁在此处添加大型新功能。它们必须合并到develop,因此,等待下一个大的发布。

完成一个release分支

当发布分支的状态准备好成为真正的发布时,需要执行一些操作。首先,将release合并到master(因为master上的每个提交都是定义的新版本,请记住)。接下来,必须标记master上的提交以便将来参考此历史版本。最后,在release分支上进行的更改需要合并回到develop中,以便将来的版本也包含这些错误修复。

前两个步骤:

$ 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现已完成,并标记以供将来参考。

更新:您可能还想使用-s-u <key>标志以加密方式对标记进行签名。

为了保持release分支中所做的更改,我们需要将这些更改合并到develop中:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)

这一步很可能导致合并冲突(可能因为我们已经更改了版本号)。如果是这样,请修复并提交。

现在我们已经完成了,并且可能会删除release分支,因为我们不再需要它了:

$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).

Hotfix分支

master分支创建,必须合并回developmaster,分支命名惯例htofix-*

67b0f4395a863dd90376e217.png

Hotfix分支非常像release分支,因为它们也是为了准备新的生产版本,尽管是计划外的。它们源于必须立即采取实际生产版本的不良状态。当必须立即解决生产版本中的严重错误时,可以从标记生产版本的master分支上的相应标记分支修补程序分支。

实质是团队成员(在develop分支)的工作可以继续,而另一个人正在准备修复生产环境问题。

创建hotfix分支

master分支创建hotfix分支。例如,假设版本1.2是当前正在运行的生产版本,并且由于严重的错误而导致麻烦。但是发展的变化还不稳定。然后我们可以分支hotfix分支并开始修复问题:

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

分支后不要忘记增加版本号!

然后,修复错误并在一个或多个单独的提交中提交修复:

$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)

结束一个hotfix分支

完成后,hotfix需要合并回master,但也需要合并回develop,以保证对错误的修复也包含在下一个版本中。这与发布分支的完成方式完全相似。

首先,更新master并标记发布:

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

此处规则的一个例外是,当release分支当前存在时,需要将hitfix更改合并到该release分支中,而不是develop。在release分支完成时,将hotfix反向合并到release分支中最终将导致hotfix合并到develop中。(如果立即开发工作需要这个错误修复,并且不能等待发布分支完成,您可以安全地将hotfix合并到hotfix中。)

最后,删除这个分支:

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

总结

虽然这个分支模型并没有什么令人震惊的新功能,但这篇文章开头的分支图在我们的项目中非常有用。它形成了一个易于理解的优雅心理模型,并帮助团队成员形成对分支和发布过程的共同理解。此处提供了该图的高质量PDF版本,您可以把它挂在墙上,以便随时参考。