一个成功的 Git 分支模型
回顾
这个模型构思于 2010 年,距今已超过十年——而 Git 本身也才刚刚诞生不久。在这十年间,git-flow(本文所描述的分支模型)在众多软件团队中变得极为流行,以至于人们开始将其视为某种“标准”——但不幸的是,有时也被当作教条或万能药。
与此同时,Git 本身席卷全球,而使用 Git 开发的最主流软件类型也逐渐转向 Web 应用(至少在我的信息圈内是如此)。Web 应用通常采用持续交付(Continuous Delivery),几乎不会回滚,也不需要同时支持多个线上版本。
这并非我十年前撰写这篇博客时所设想的软件类型。如果你的团队正在做持续交付,我建议采用更简单的工作流(例如 GitHub Flow),而不是强行套用 git-flow。
然而,如果你开发的是明确带有版本号的软件,或者你需要在线上同时维护多个版本,那么 git-flow 对你的团队可能依然非常适用——就像过去十年对许多团队那样。在这种情况下,请继续阅读。
最后要强调:世上没有万能药。请结合你自己的上下文来判断。不要盲目排斥,也不要盲目崇拜。自己做出明智的选择。
在本文中,我将介绍一种我在大约一年前为一些项目(包括工作和个人项目)引入的开发模型,实践证明它非常成功。我一直想详细写一写这个模型,但始终没找到合适的时间,直到现在。本文不会涉及任何具体项目的细节,只聚焦于分支策略和发布管理。
为啥选用Git
关于 Git 与集中式版本控制系统(如 CVS、Subversion)的优劣对比,网上已有大量讨论(甚至争论)。作为开发者,我今天更偏爱 Git。Git 真正改变了开发者对合并(merge)和分支(branch)的看法。
从我过去使用的 CVS/Subversion 世界里,合并/分支一直被视为有点“可怕”(“小心合并冲突,它们会咬你!”),通常只在必要时才偶尔使用。
但在 Git 中,这些操作极其轻量且简单,已成为日常开发流程的核心部分。例如,在 CVS/Subversion 的书籍中,分支与合并通常出现在靠后的章节(面向高级用户);而在每一本 Git 书籍中,它早在第 3 章(基础部分)就已出现。
正因为其简单性和重复性,分支与合并不再令人畏惧。版本控制工具本就应该在分支与合并方面提供最大支持。
好了,工具的话题到此为止。让我们进入开发模型本身。
去中心化但集中化
我们所使用的、并被证明与此分支模型配合良好的仓库设置,是包含一个中央“权威”仓库(central “truth” repo)的结构。请注意,从技术层面讲,Git 是分布式版本控制系统(DVCS),并不存在真正的“中央仓库”;但我们仍将其称为 origin,因为这是所有 Git 用户都熟悉的名字。
每位开发者都会从 origin 拉取(pull)和推送(push)。但除了这种中心化的推拉关系外,每位开发者也可以从其他同伴那里拉取变更,从而形成子团队。例如,当两位或更多开发者需要协作开发一个大型新功能时,在将未完成的工作过早推送到 origin 之前,他们可以先在彼此之间同步代码。在下图中,存在 Alice 和 Bob、Alice 和 David、Clair 和 David 组成的子团队。
从技术上讲,这意味着 Alice 已定义了一个名为 bob 的 Git 远程(remote),指向 Bob 的仓库,反之亦然。
开发模型中主要分支
在核心层面,该开发模型深受现有模型的启发。中央仓库包含两个永久存在的主干分支:
master/maindevelop
origin/master 分支对所有 Git 用户来说都很熟悉。我们认为 origin/master 的 HEAD 始终反映生产就绪状态(production-ready state)的源代码。
我们认为 origin/develop 的 HEAD 始终反映下一个版本的最新开发进展的状态。有些人称之为“集成分支”(integration branch)。所有自动化的 nightly 构建(每晚构建)都基于此分支。
当 develop 分支的代码达到稳定状态并准备发布时,所有变更应以某种方式合并回 master,并打上版本标签(tag)。具体如何操作将在后文详述。
因此,每次向 master 合并变更,就意味着一次新的生产发布。我们对此非常严格,理论上甚至可以配置一个 Git 钩子(hook),在每次 master 有提交时自动构建并部署到生产服务器。
辅助支持分支
除了上述两个主干分支,我们的开发模型还使用多种辅助分支,以支持团队成员并行开发、便于追踪功能、准备正式发布,以及快速修复线上问题。与主干分支不同,这些辅助分支都有有限的生命周期,最终会被删除。
我们可能使用的分支类型包括:
- 功能分支(Feature branches)
- 发布分支(Release branches)
- 热修复分支(Hotfix branches)
这些分支在技术上并无特殊之处——它们只是普通的 Git 分支。其“类型”完全由我们的使用方式决定。
每种分支都有明确的用途,并严格规定了:
- 从哪个分支创建(originating branch)
- 必须合并回哪些分支(merge targets)
- 命名规范
下面我们逐一说明。
新功能(Feature)分支
- 可能从:
develop - 必须合并回:
develop - 命名规范:除
master、develop、release-*、hotfix-*外的任意名称 (可推荐以feat-*开头)
功能分支(或有时称为主题分支)用于开发即将发布或远期版本中的新功能。在开始开发某个功能时,该功能将被纳入哪个目标版本可能尚不明确。功能分支的本质在于:只要功能还在开发中,分支就存在;但最终要么合并回 develop(正式纳入下一版本),要么被废弃(如实验失败)。
功能分支通常仅存在于开发者本地仓库中,不应推送到 origin。
创建功能(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)完成。这样做可以避免丢失功能分支存在的历史信息,并将实现该功能的所有提交归为一组。对比以下两种情况:
在后一种情况下,从 Git 历史中无法看出哪些提交共同实现了某个功能——你只能手动阅读所有日志消息。若需回滚整个功能(即一组提交),在后一种情况下会非常麻烦,而如果使用了 --no-ff 则很容易实现。
是的,这会多产生一些(空的)提交对象,但收益远大于成本。
发布(Release)分支
- 可能从:
develop - 必须合并回:
develop和master - 命名规范:
release-*
发布分支用于准备新生产版本的发布。它们允许进行最后的润色工作,例如修正拼写错误、调整配置等。此外,它们也用于进行小范围的 bug 修复,并准备发布所需的元数据(如版本号、构建日期等)。通过在发布分支上完成这些工作,develop 分支可以立即开始接收下一个大版本的新功能。
从 develop 分支创建新发布分支的关键时机是:develop(几乎)已经反映了新版本所需的状态。此时,所有计划纳入该版本的功能必须已合并到 develop;而面向未来版本的功能则必须等待发布分支创建后再继续开发。
正是在发布分支创建之时,新版本才会被赋予一个具体的版本号——而不是更早。在此之前,develop 分支代表“下一个版本”,但尚不确定该版本最终会是 0.3、1.0 还是其他。这一决策是在发布分支创建时做出的,并依据项目自身的版本号递增规则执行。
创建(Release)发布分支
发布分支从 develop 分支创建。例如,假设当前生产版本是 1.1.5,我们即将发布一个重大版本。develop 分支的状态已准备好用于“下一个版本”,我们决定将其定为 1.2 版本(而非 1.1.6 或 2.0)。于是我们创建并切换到一个名为 release-1.2 的分支:
$ 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 脚本,用于修改工作副本中的某些文件以反映新版本号。(当然,也可以手动修改——关键是某些文件发生了变化。)然后,我们将更新后的版本号提交。
这个新分支可能会存在一段时间,直到该版本正式发布。在此期间,可以在该分支上应用 bug 修复(而不是在 develop 分支上)。严禁在此添加大型新功能——它们必须合并到 develop,并等待下一个大版本。
结束(Release)分支
当发布分支的状态已准备好成为正式版本时,需要执行以下操作:
首先,将发布分支合并到 master(因为根据定义,master 上的每次提交都是一次新发布)。
其次,对该 master 提交打上标签,以便将来轻松引用该历史版本。
最后,将发布分支上的变更合并回 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 <key> 参数对标签进行加密签名。】
为了保留发布分支上的变更,我们还需要将其合并回 develop:
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive. (Summary of changes)
这一步很可能导致合并冲突(尤其是因为我们修改了版本号)。如果发生冲突,请修复并提交。
现在我们真正完成了,可以删除不再需要的发布分支:
$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).
Hotfix热修复分支
- 可能从:
master - 必须合并回:
develop和master - 命名规范:
hotfix-*
热修复分支与发布分支非常相似,因为它们也都用于准备新的生产版本发布,只不过这是计划外的。当线上生产版本出现严重问题、必须立即修复时,可以从 master 分支上标记该生产版本的对应标签处创建热修复分支。
这样做的本质是:团队成员可以在 develop 分支上继续工作,而另一人则可以准备一个快速的生产修复。
创建热修复(Hotfix)分支
热修复分支从 master 分支创建。例如,假设当前线上运行的生产版本是 1.2,但由于一个严重 bug 导致服务异常,而 develop 分支上的变更尚不稳定。此时我们可以创建一个热修复分支并开始修复问题:
$ 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 并在一个或多个独立提交中提交修复:
$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)
结束热修复(Hotfix)分支
完成后,该 bug 修复需要合并回 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> 参数对标签进行加密签名。】
接下来,也将该 bug 修复包含进 develop:
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive. (Summary of changes)
此处有一条例外规则:如果当前存在发布(Release)分支,则应将热修复变更合并到该发布分支,而不是 develop。当发布(Release)分支最终完成并合并回 develop 时,该修复自然也会进入 develop。(如果 develop 上的工作急需此修复且无法等到发布分支完成,也可以安全地提前将修复合并到 develop。)
最后,删除临时分支:
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).
虽然这个分支模型本身并无惊人创新,但本文开头的那张全景图,在我们的项目中被证明极其有用。它构建了一个优雅且易于理解的心智模型,帮助团队成员对分支和发布流程达成共识。
译文:A successful Git branching model » nvie.com
版权所有 © 【代码谷】 欢迎非商用转载,转载请按下面格式注明出处,商业转载请联系授权,违者必究。(提示:点击下方内容复制出处)
源文: 一个成功的 Git 分支模型 ,链接:https://www.daimagu.com/article/2512171438079260.html,来源:代码谷
评论