2023/03/21

一个成功的git分支模型

翻译http://nvie.com/posts/a-successful-git-branching-model/

介绍

在这篇文章中,我发布了一个开发模型,介绍的是我大概一年前工作中以及个人开发中的一些工程,事实证明这很成功。我很长时间以来一直想写出来,可是发现这太艰难了,直到今天我才完全写出来。我不去讲这样工程的细节,只讲讲分支策略和发布管理。

我们所有的源代码版本都是以git为工具的。(顺便说下,如果你感兴趣,我们公司GitPrime提供了一些对软件工程性能很棒的实时数据分析。)

为什么使用git?

对git利弊比较的深入讨论源代码集中控制系统,可查看网页。这里有大量的激烈的争执。作为一个开发者,今天我更喜欢Git相比其他工具。Git真正改变了开发者对合并和分支的思维。来自CVS/SVN,合并和分支总是被想的有点可怕(“谨防合并冲突,他们会吃了你!”),并且你只能每隔一段时间做一件事情。

但对Git而言,这些行为就太简单方便了,他们被认为是你日常工作流的核心部分之一,这是真的。比如:在CVS/SVN的课程中,合并和分支是最后讨论的章节(对高级人员而言),同时在每一本Git的书本中,它已经在第三章就介绍了(基础)。

由于其简单性和重复性的特性,合并和分支不在是可怕的事情了。版本控制工具比其他一切更能协助合并和分支。

足够的工具让我们开始开发模式了。我在这里一直的提及这个模式不再是一个每个组员不得不为了管理软件开发过程中都跟随的一个程序集了。

分散又集中

我们使用仓库设置可以更好的处理这个分支模型,这个仓库是一个真正的中央仓库。注意这个仓库仅仅被认为是作为一个中央仓库(因为Git是一个DVCS,在技术级别是没有这样一个中央仓库的)。我们将这个仓库称为origin,因为这个名称是所有Git使用者都熟悉的。

每个开发者都pull,push到origin。但除了集中push-pull的关系外,每个开发者也可能pull来自项目其他成员的一些changes。比如在两个或者更多开发者一起工作的大的新项目中,push到origin之前,先去pull,这可能是有用的。上图所示,有分队的Alice和Bob,Alice和David,Clair和David。

从技术上讲,这意味着没有超过Alice已经定义了一个远程Git称为Bob,指向Bob的仓库,反之亦然。

主要的分支

开发模型极大地被现有模式有启发。这个中央仓库在无限的生命周期内有两个主要的分支:master, develop。

在origin上master分支应该是被每一个Git使用者所熟识。平行于master分支,另一个已存在的是develop分支。

我们认为origin/master是所作为主分支,这里HEAD的源码总是反应着准备就绪的状态。

我们认为origin/develop是所作为主分支,这里HEAD的源码总是反应着下一个版本交付最新开发改变的的状态。一些人称之为”集成分支”。这是在自动构建。

当在develop分支的代码达到一个稳定点,准备发布,所有的改变都将会被合并到master分支,标记版本号。具体怎么实现稍后再去讨论。

因此,每次变化和合并到master分支,都是一个新的产品发布的定义。我们往往非常严格,以至于,我们使用Git脚本去自动构建并且每次都去提交到master去推广软件产品服务器。

支持分支

对于master和develop分支,我们的开发模型使用了一系列的支持分支帮助多个组员之间平行开发,轻松跟踪功能,为产品发布做准备,尽快修复产品存在的问题。不同于主分支,这些分支总是有一个有限的生命周期,因为他们最终将被删除。

我们可能会使用的不同类型的分支:功能分支, 发布分支, 热修复分支。

每个分支的都有一个特定的目的,并且被严格的规则约束,就是哪些分支可能是源分支,哪些必须是合并的目标。我们一会通过他们去实践。

从技术角度而言,这些分支并并不”特殊”。分支类型通过我们使用而确定。他们当然是一些普通的Git分支。

功能分支

可能是从develop分出的分支,必须被合并到develop, 分支命名约定:除了master,develop, release-, hotfix-以为的名称。

功能分支(或者有时也称为主题分支)是为了即将到来或一个不远的发布而被使用的开发新功能的分支。当开始开发一个功能时,目标版本中,这个功能将会被合并可能是未知的。功能分支的本质是只要功能在开发中,它就会存在,但最终会被合并到develop分支(无疑增加了新功能将被发布),或者丢掉(令人失望的开发实验)。

功能分支代表性的存在于开发者的repo中,而不是origin。

创建一个功能分支

当开始一个功能的开发时,从develop分支分出:

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

合并开发完成的功能分支

完成的功能合并到develop分支,确定增加到即将发布的版本中:

$ git checkout develop
Swith to branch "develop"
$ git merge --no--ff myfeature
updating ea1b82a..05e9557
(Summary of changes)
$ git branch -d myfeature
Deleted branch myfeature (was 059957).
$ git push origin develop

–no–ff标签导致合并总会创建一个新的提交对象,尽管合并可以快进执行。这避免了功能分支的历史记录和组内一起合作提交增加功能的信息丢失。比较如下:

在后一种情况下,不能看到从提交对象一起增加的功能的Git历史记录,但不得不对手动阅读所有的日志信息。回退整个功能分支(即一个组的提交),在后者的情况下这是一个很头痛的问题,然而如果使用–no–ff这是很容易做到的.

是的,它将创建一些更多(空的)提交对象,但获得的远远大于成本。

release版本

可能是从develop分出的分支,必须被合并到developmaster, 分支命名约定release-*。

发布版本为新产品发布的准备提供了支持。它们允许贯穿开始到最后一分钟。将来,它们也允许小bug的修复和准备发布的元数据(版本号,创建日期等等)。在release分支做的所有工作下,develop分支可以为下一个大的发布接收功能。

从develop分出的release分支的关键时刻是当develop(almost)反应了新的发布的想要的状态。至少所有为发布而创建的分支都应该在这个时间点上被合并到develop。在将来发布的所有的功能必须等待到release分支被分出。

实际上在即将发布的release分支的开始被分配一个版本号,而不是任何更早的时候。直到那一刻,develop分支反应了下一个发布的改变,到那时不清楚最终下一个发布会变成0.3还是1.0,直到这个release分支被创建。这一决定是由release分支的开始和进行项目的版本号而决定的。

创建release分支

release分支由develop分支创建而来。比如当前产品发布的版本是1.1.5,即将有一个大的发布。develop的状态就是准备到下一个发布,我们决定把它变成1.2(而不是1.1.6或2.0).我们我们分出的release分支的名称反应了这个版本号。

$ git checkout -b release-1.0 develop
Switch to a new branch "release-1.2"
$ ./bump-version .sh 1.2
Files moditied successfully,version bumped to 1.2
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d94224] Bumped version number to 1.2
1 files changed, 1 insertions(+), 1 deletions(-)

创建并切换到一个新分支后,我们bump一个版本号。这儿,bump-version.sh 是一个虚构的shell脚本,它改变一些文件的工作副本以反应新版本。(当然也可能是一些手动的变化,重点是一些文件的改变。)然后,这个bumped版本就会被提交。

新分支可能会存在一段时间,直到可以肯定地推出发布。这段期间,bug修复可能被应用在这个分支(而不是在develop分支)。严厉禁止增加一些新功能。它们必须合并到develop,然后等待下一个大的发布。

完成release分支

当release分支的状态准备真正发布时,需要执行一些行为。首先,release分支合并到mater(因为在maser上的每个提交都肯定是新发布中的,记住)。其次,master提交必须被标识以方便参考这一历史版本。最后,release分支的这个改变需要合并到develop,以至于竟来的发布可以包含这个bug的修复。

在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 标记去贴上你的标签。

为了保持release分支的变化,我们需要合并到develop,在Git中:

$ 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, 分支命名约定:hotfix-*。

Hotfix分支很像release分支,他们都是为新产品的发布做准备,尽管也有一些意外情况。它们源自于在一个不想要的产品版本状态中立即执行的必要性。当在产品版本中出现一个致命的bug时需立即解决,hotfix分支可能是从一个已标记的master分支其标记产品版本号中分出去的分支。

组员的基本工作(在develop分支)可能还在继续,同时另一个人正在准备快速修复产品bug。

创建hotfix分支

hotfix分支从master分支创建出来。比如,当前线上产品发布版本是1.2,一些问题导致了严重的bug.但develop不再是稳定的。我们需要分出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(-)

不要忘记在创建分支后bump版本号。

然后,修复bug,提交一个或多个修复。

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

完成hotfix分支

完成后,修复bug的需要合并到master分支,但也需要合并到develop分支,为了安全,最好在下一个发布中也包含这个修复。这个release分支如何完成是完全相似的。

首先,更新master,标识release:

$ git chekout 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也包括这个修复:

$ git chekout develop
Switched to branch "develop"
$ git merge --no--ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)

规则的一个例外是当release分支存在时,hotfix的改变需要合并到release分支,代替develop。bugfix合并到release分支后,当release完成后,最终仍需合并到develop。(如果develop工作直接需要bugfix,则不用等release完成,你就该安全地把bugfix合并到develop。)

最后,删除临时分支:

$ git branch -d hotfix-1.2.1

总结

虽然没有真正令人震惊的新分支模型,但这篇文章的”大局”开始已经被证明在我们项目中非常有用。它形成了一个优雅的心理模型很容易理解,允许团队成员开发一个共享的分支和发布流程。

这里提供高质量的PDF。继续把它放到桌面在任何时间快速参考。

更新: 对于任何有需要的人:这里是gitflow-model.src.key

如果你想取得联系,在Twitter上@navie

文内导航