A successful Git branching model

原文: A successful Git branching model
img

Why git?

有关Git与集中式源代码控制系统相比的利弊的详细讨论,请参阅网站。那里战火纷飞。作为开发人员,我喜欢Git超过所有的其他工具。 Git 真的改变了开发人员对合并和分支的看法。在 古老的的 CVS / Subversion 世界里,合并/分支一直被认为有点可怕(“小心合并冲突,它们会咬你”),但是你每隔一段时间都要做。

但是使用Git,这些操作非常容易和简单,并且它们被认为是你日常工作流程的核心部分之一。例如,在 CVS / Subversion 书籍中,分支和合并通常在最后的章节才第一次谈论(对于高级用户),而在每本Git书中,它已经在第3章(基础知识)中介绍过了。

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

准备好工具,让我们进入开发模型。我将在这里介绍的模型基本上只是每个团队成员必须遵循的一组步骤以便来管理软件开发过程。

Decentralized but centralized

我们使用的仓库设置与该分支模型配合良好,具有一个“真实”中央仓库。请注意,这个仓库只被认为是中央仓库(因为Git是DVCS,在技术层面没有中央仓库)。我们将此repo称为origin,所有Git用户应该都熟悉此名称。
img

每个开发人员都会pull并puch 到 origin。但除了集中式 push-pull 关系之外,每个开发人员还可以从其他同行中获取更改以形成子团队。例如,在将正在进行的工作过早地推送到 origin 中之前,这对于与两个或更多开发人员一起处理大型新功能可能是有用的。在上图中,有 Alice 和 Bob,Alice 和 David 以及 Clair和David 的子团队。

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

The main branches

img
事实上,这个开发模型受到现有模型的极大启发。中央仓库拥有两个主要分支,具有无限的生命周期:

  • master
  • develop

origin 上的 master 分支每个Git用户应该都熟悉。与 master(本地) 分支并行,另一个分支称为develop。

我们认为 origin/master 是主分支,其中HEAD的源代码总是反映生产就绪状态。

我们将 origin/develop 视为主要分支,而它的 HEAD 指针始终指向为下一发版而做的最新开发改动。有些人称之为“整合分支”(integration branch)。这是建立任何自动夜间构建的地方。

当 develop 分支中的源代码达到稳定点并准备好发布时,所有更改都应以某种方式合并回master,然后使用版本号进行标记。具体如何做我们将进一步讨论。

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

Supporting branches

主干分支masterdevelop之外,我们的开发模型使用了很多辅助分支来帮助团队成员间的并行开发,减轻功能跟踪的成本,预备生产环境的发布,协助快速修复已发布版本的问题。和主干分支相反,这些辅助分支只有有限的生命期,最终会被移除。

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

  • Feature branches
  • Release branches
  • Hotfix branches

上面的每个辅助分支都有一个特定的目标,且严格限制哪些是起源分支,哪些是合并的目标分支。我们一会儿会详细解释。

从技术角度来看,这些分支绝不是”特殊的”。分支的类型是按照我们如何使用它们而划分的,也就是普通的 Git 分支而已。

Feature branches

img

  • 可以从 develop 派生。
  • 必须合并到 develop 分支。
  • 分支命名公约: 除 master 、develop、 release- 和 hotfix- 以外的任意命名。

Feature 分支(有时称为 topic 分支),是用来为未来的发版而开发新 features 的分支。当开始 feature 的开发时,。Feature 分支的本质它会在 feature 的开发期内存在,但最终会合并回 develop 分支(一定会在即将发布的版本中添加新功能),或者被直接丢弃掉(在令人失望的试验的情况下)。

Feature 分支通常仅存在于开发人员的 repos 中,而不在 origin 中。

Creating a release branch

要创建一个新的 Feature 分支,从 develop 分支上派生:

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

Incorporating a finished feature on 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 标识使得 merge 操作总是创建一个新的 commit,即使 merge 可以使用 fast-forward。这避免了 feature 分支历史信息的丢失,而且把所有 commit 归并在一起。比较一下:

img

后者无法从 Git 历史中找到是哪些 commit 实现了 feature,你必须要手工的去阅读所有的 log 信息。撤销一个 feature 在后一种情况下真的很头疼,如果使用了--no-ff标志,很容易做到。

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

Release branches

  • 可以 develop 分支派生。
  • 必须合并回 developmaster 分支。
  • 分支命名公约: release-*

Release 分支为新的生产版本做预备。最后的画龙点睛。此外,Release 分支也可以用来修复小 bug 和准备发布版本的元数据(版本号,构建日期等)。通过在 Release 分支上完成所有这些工作,develop 分支将清晰明了,以便接收下一个大版本的发布。

develop 分支新发布分支的关键时刻是开发(几乎)反映新版本的期望状态。。至少所有针对要构建的版本的功能在此时必须已经合并进 develop。针对未来版本的所有功能可能不会 – 他们必须等到release分支派生后。

确切的说,release 分支始于即将发布的一个版本分配了一个版本号–不能提早。直到那一刻之前,develop 分支反映出“next release” 的改动。但还不清楚“下一次发布”最终是否会变成0.3或1.0,对 release 版本的决定是在 release 分支开始时,由项目的版本号规则来拟定。

Creating a release branch

Release 分支是从develop分支创建的。例如,当前的生产版本是 1.15,且即将有一个大迭代。develop 分支的状态已经为 next release 做好准备,我们也已经决定好即将到来的版本是 1.2(而不是 1.1.6或者2.0)。因此我们从当前的 develop 分支上派生并给他起一个反应了新版本号的名字:

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 脚本,用来修改一些文件,使其能体现新版本。之后,遇到的版本号被 commit 上去。

该新分支可能会存在一段时间,直到 release 可以确定被发布出来。在此期间,分支上可能会修复一些 bug(而不是在 develop 分支上)。严禁在该分支上添加大的 feature 进来。release 分支最后必须被 merge 回 develop 分支上,然后继续等待下一个大版本的发布。

Finishing a release branch

当 release 分支的开发状态到了真正可以发布的时候,就需要采取一些行动了。首先, release 分支得 merge 到 master 分支(记住,master 上的每个 commit 都是一个定义的新的 release)。然后,提交到 master 上的 commit 必须被标记上 Tag,以便将来参考该历史版本。最后,release 分支上的改动还需要 merge 回 develop 分支,以便以后的 release 也包含这些 bug fixes。

两步 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 或 -u 来对 Tag 进行加密)

为了保留 release 分支上的改动,继续将他们 merge 回 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 branches

img
master 分支派生;
必须 merge 回 develop 分支和 master 分支;
分支命名公约:hotfix-*

Hotfix 分支和 release 分支很类似,都是为发布新的生产版本而准备,虽然是计划外的。其存在的必要性是由于当前生产版本处于需要紧急修复的状态。当生产版本发生严重 bug 必须立即修复时,hotfix 分支可以从生产版本的 master 分支上的相应 Tag 派生出来。

这样做的本质是在 develop 分支上开发的人员可以继续,另一边,其他人员则可以快速的进行生产版本的修复工作。

Creating the hotfix branch

Hotfix 分支从 master 派生。比如,当前线上运行的生产版本是 1.2 版,遇到严重 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(-)

Finishing a hotfix branch

完成后,bug修复需要合并到 master 分支上,同时还得 合并到 develop 分支,以保证 bugfix 也包含在下一个版本中 ,这和 release 分支的结束过程是非常类似的。

首先,更新master ,然后为新的 release 标记 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

(你可能还想使用 -s 或 -u 来对 Tag 进行加密)

然后, develop 分支也包含这些 bugfixs:

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 分支上的修复代码最终还是会合并到 develop 上。

最后,移除这个临时分支:

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

Summary

虽然这个分支模型并没有什么令人耳目一新的新功能,但这篇文章开头的“大图”在我们的项目中非常有用。它构造了易于理解并允许团队成员开发公共分支和发布流程的优雅模型。