Git 教程

Git Branching Model

开篇

本打算好好整理下 git 相关的教程,后来发现猴子都能懂的 GIT 入门A successful Git branching model这两个教程真是太棒了,所以打算只简单翻译一下 A successful Git branching model 这篇文章,这篇文章也是 git 命令集 git-flow 所遵循的宗旨:

About git-flow:
Git extensions to provide high-level repository operations for Vincent Driessen’s branching model.
http://nvie.com/posts/a-successful-git-branching-model/

一个成功的 git 分支模型

本文中我会阐述一种开发模型,一年前(2009年1月份)该模型就已经被我应用在所有的项目中(包括工作中的项目和私有项目),实践证明它是非常成功的。
虽然我早就想把这个模型记录下来,遗憾的是直到现在我才有时间来搞定它。我不会在文中阐述任何项目细节,仅仅谈一下分支策略和发布管理。
Git Model
全文围绕使用Git作为所有源码的版本控制工具来介绍这个模型。

为什么是 Git

要全面了解 Git 与其它集中式版本控制系统相比的优劣,可以参考 GitSvnComparison 这篇文章。这方面的争论可谓是硝烟弥漫。作为一个开发者,与其他工具相比,我更喜欢 Git 。
Git真正地改变了人们考虑合并及分支的方式。在 CVS/Subversion (我曾经使用过的版本控制工具)的世界里,合并/分支总是被认为是有点可怕的事情(“小心合并冲突,太恶心人了”),因此你只应偶尔干这种事情。

但是有了 Git 之后,合并/分支就都变得简单多了,因为他们被认为是你日常版本控制操作流程中的核心一部分。例如,在 CVS/Subversion 的书中,分支及合并往往在后面的章节才被介绍(针对高级用户),但在每一本 Git 的书中,合并、分支在前3章中就有介绍了(基础)。

简单及重复带来的好处就是,分支及合并变得不再可怕。版本控制工具本该帮助我们方便的进行分支及合并操作,这比什么都重要。

简单介绍下工具后,让我们来看开发模型。我讲介绍的模型本质上只是一组步骤,每个团队成员都必须遵循这些步骤以形成一个可靠管理的软件开发过程。

去中心化但仍保持中心化

我们使用的这个被证实工作得很好的仓库配置分支模型中,其核心是一个中心“真理”仓库。注意只有该仓库才被认为是中心库(由于 Git 是 DVCS ,在技术层面上来讲是没有中心库这一概念的)。之后我们用 origin 指代该仓库,因为大多数Git用户都熟悉这个名称。
Git Centr Decentr

每个开发者都对 originpushpull 操作。不过除了这种中心化的 push-pull 关系外,每个开发者还可以从其他小伙伴处 pull 变更,从而形成一个开发小团队。例如,当两个或更多的开发者一起开发一个大的特性时,为了避免过早的往 origin push 工作代码,这种开发者之间的 pull 就显得非常有用。在上图中, Alice 和 Bob、 Alice 和 David 、 Clair 和 David 就分别组成了一个开发小团队。

从技术上来说,无非就是 Alice 定义一个 Git remote,名字为 bob ,指向 Bob 的仓库,反过来也一样,Bob 定义一个远程仓库,叫做 alice ,指向 Alice 的仓库。

主要分支

核心的来讲,此开发模型主要受现有的模型启发。在该模型中,中心仓库包含了两个主要分支,这两个分支的生命周期是无限的:

  • master
  • develop

Git Main Branches

对于每个 Git 用户而言,都应该很熟悉 origin 上的 master 分支。与 master 分支平行存在的是另外一个名为 develop 的分支。
我们把 origin/master 分支上的 HEAD 维护在一个可用于生产环境的稳定状态,并用 origin/develop 分支上的 HEAD 反映开发过程中最新的已提交的变更。有人会称之为“集成分支”。该分支是每日自动化构建的代码源。

HEAD 指向的是现在使用中的分支的最后一次更新。通常默认指向 master 分支的最后一次更新。通过移动 HEAD ,就可以变更使用的分支。

当 develop 分支上的源码到达一个稳定的状态时,就可以发布版本。所有 develop 上的变更都应该设法合并回 master 分支,并且使用发布版本号打上标签。稍后我们会讨论具体操作细节。

因此,每次有变化被合并到 master 分支时,根据定义这就是一次新的产品版本发布。我们趋向于严格遵守该规范,所以理论上来说,每次 master 有提交时,我们都可以使用一个 Git 钩子 (hook) 脚本来自动构建并部署软件至产品环境服务器。

辅助分支

除了主要分支 master 和 develop 外,我们的开发模型使用了多种辅助分支来帮助团队成员间实现并行开发、追踪产品特性、准备产品版本发布、以及快速修复产品问题。与主要分支不同的是,这些分支的生命周期是有限的,它们最终都会被删除。

我们会用到的辅助分支主要有以下种类:

  • 特性分支 (feature branch)
  • 发布分支 (release branch)
  • 热补丁分支 (hotfix branch)

Git Feature Branches

上述每种分支都有特定的用途,它们各自关于源自什么分支、合并回什么分支,都有严格的规定。稍后我们逐个进行介绍。

从技术角度来说,这些分支一点都不“特殊”。分支按照我们对其的使用方式进行分类。技术角度它们都一样是平常的 Git 分支。

特性(feature)分支

  • 可能的分支来源:develop
  • 必须合并回:develop
  • 分支命令约定:任何除 master, develop, release- 或 hotfix- 以外的名称

特性分支(有时也被称作 topic 分支)是用来为下一发布版本开发新特性。当开始开发一个特性的时候,该特性会成为哪个发布版本的一部分,往往还不知道。特性分支的重点是,只要特性还在开发,该分支就会一直存在,不过它最终会被合并回 develop 分支(将该特性加入到发布版本中),或者被丢弃(如果试验的结果令人失望)。

特性分支往往只存在于开发者的仓库中,而不会出现在 origin。

创建一个特性分支

开始开发新特性的时候,从 develop 分支创建特性分支。

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

把完成的 feature 合并到 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 标记能使合并总是创建一个新的 commit 对象,即使该合并能以 fast-forward 的方式进行。这么做可以避免丢失特性分支存在的历史信息,同时也能清晰的展现一组 commit 构成一个特性。比较下面的图:
Git Merge Fast-forward

在右边这个图中,已经无法一眼从 Git 历史中看到哪些 commit 对象构成了一个特性——你需要阅读日志以获得该信息。在这种情况下,回退 (revert) 整个特性(一组 commit )就会比较麻烦,而如果使用了 –-no-ff 就会简单很多。

是的,这么做会造成一些(空的) commit 对象,但这么做是利大于弊的。

可惜的是,我没能找到方法让 –-no-ff 成为 git merge 的默认行为参数,但其实应该这么做。

发布(release)分支

  • 可能的分支来源:develop
  • 必须合并回:develop 和 master
  • 分支命名约定:release-*

release 分支是为发布新的产品版本而设计的。在这个分支上的代码允许做小的缺陷修正、准备发布版本所需的各项说明信息(版本号、发布时间、编译时间等等)。通过在 release 分支上进行这些工作可以让 develop 分支空闲出来以接受新的 feature 分支上的代码提交,进入新的软件开发迭代周期。

当 develop 分支上的代码已经包含了所有即将发布的版本中所计划包含的软件功能,并且已通过所有测试时,我们就可以考虑准备创建 release 分支了。而所有在当前即将发布的版本之外的业务需求一定要确保不能混到 release 分支之内(避免由此引入一些不可控的系统缺陷)。

成功的派生了 release 分支,并被赋予版本号之后, develop 分支就可以为“下一个版本”服务了。所谓的“下一个版本”是在当前即将发布的版本之后发布的版本。版本号的命名可以依据项目定义的版本号命名规则进行。

创建一个 release 分支

release 分支从 develop 分支创建。例如,假设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 脚本,它修改一些本地工作区的文件以体现新的版本号。(当然这也可以手动完成——这里只是说要有一些文件变更)接着,提交新版本号。

新的 release 分支可能存在一段时间,直到该版本明确对外交付。这段时间内,该分支上可能会有一些 bug 的修复(而不是在 develop 分支上进行修复)。在该分支上添加新特性是严格禁止的。新特性必须合并到 develop 分支,然后等待下一个版本发布。

结束一个 release 分支

当 release 分支达到一个可以正式发布的状态时,我们就需要执行一些操作。首先,将 release 分支合并至 master (记住,我们之前定义 master 分支上的每一个 commit 都对应一个新版本)。接着, master 分支上的 commit 必须被打上标签 (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

现在版本发布完成了,而且为未来的查阅提供了标签。

提醒:你可能同时也会想要用 -s 或者 -u <key> 来对标签进行签名。

为了能保留 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)分支

  • 可能的分支来源: master
  • 必须合并回: develop 和 master
  • 分支命名约定: hotfix-*

Git Hotfix Branches

hotfix 分支和 release 分支十分类似,它的目的也是发布一个新的产品版本,尽管是不在计划中的版本发布。当生产环境中的软件遇到了异常情况或者发现了严重到必须立即修复的软件缺陷的时候,就需要从 master 分支上指定的 TAG 版本派生 hotfix 分支来组织代码的紧急修复工作。

使用 hotfix 分支的主要作用是(develop 分支上的)团队成员可以继续工作,而另外的人可以在 hotfix 分支上进行快速的产品 bug 修复。

创建一个 hotfix 分支

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

不要忘了在创建 hotfix 分之后设定一个新的版本号!

然后,修复 bug 并提交更改,可以是一组 commit(只提交一次) 或者多个单独的 commit 。

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 分支

修复完成后,hotfix 分支需要合并回 master ,但同时它还需要被合并回 develop ,这样相关的修复代码才会同时被包含在下个版本中。这与我们完成 release 分支很类似。

首先,更新 master 分支并打上标签。

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 <key> 来对标签进行签名。

接着,将修复的代码合并到 develop :

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 。将 hotfix 合并到 release 分支,也意味着当 release 分支结束的时候,变更最终会被合并到 develop 。(如果 develop 上的开发工作急需热补丁并无法等待 release 分支完成,这时你已经可以安全地将 bugfix 从 release 分支合并到 develop 分支。)

最后,删除临时的 hotfix 分支:

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

总结

虽然这个分支模型中没有什么特别新鲜的东西,但事实上,本文起始处的“全景图”在我们的项目中起到了非常大的作用。它帮助建立了优雅的,易理解的概念模型,使得团队成员能够快速建立并理解一个公用的分支和发布过程。

我同时也提供了一个该图对应的高质量PDF版本。你可以打印出来并挂在墙上,随时参考。

Update: And for anyone who requested it: here’s the gitflow-model.src.key of the main diagram image (Apple Keynote).

更进一步

Git Flow 开发模型从源代码管理角度对通常意义上的软件开发活动进行了约束。应该说,为我们的软件开发提供了一个可供参考的管理模型。
Git Flow开发模型让 nvie 的开发代码仓库保持整洁,让小组各个成员之间的开发相互隔离,能够有效避免处于开发状态中的代码相互影响而导致的效率低下和混乱。

所谓模型,在不同的开发团队,不同的文化,不同的项目背景情况下都有可能需要进行适当的裁剪或扩充。

PS:为了简化使用 Git Flow 模型时 Git 指令的复杂性, nvie 开发出了一套 git 增强指令集。可以运行于 Windows、Linux、Unix 和 Mac 操作系统之下。有兴趣的同学可以去看看(https://github.com/nvie/gitflow)。

参考文献