一个成功的 Git 分支模型

编程 (274) 2025-12-17 17:00:14

回顾

这个模型构思于 2010 年,距今已超过十年——而 Git 本身也才刚刚诞生不久。在这十年间,git-flow(本文所描述的分支模型)在众多软件团队中变得极为流行,以至于人们开始将其视为某种“标准”——但不幸的是,有时也被当作教条或万能药。

与此同时,Git 本身席卷全球,而使用 Git 开发的最主流软件类型也逐渐转向 Web 应用(至少在我的信息圈内是如此)。Web 应用通常采用持续交付(Continuous Delivery),几乎不会回滚,也不需要同时支持多个线上版本。

这并非我十年前撰写这篇博客时所设想的软件类型。如果你的团队正在做持续交付,我建议采用更简单的工作流(例如 GitHub Flow),而不是强行套用 git-flow。

然而,如果你开发的是明确带有版本号的软件,或者你需要在线上同时维护多个版本,那么 git-flow 对你的团队可能依然非常适用——就像过去十年对许多团队那样。在这种情况下,请继续阅读。

最后要强调:世上没有万能药。请结合你自己的上下文来判断。不要盲目排斥,也不要盲目崇拜。自己做出明智的选择。


在本文中,我将介绍一种我在大约一年前为一些项目(包括工作和个人项目)引入的开发模型,实践证明它非常成功。我一直想详细写一写这个模型,但始终没找到合适的时间,直到现在。本文不会涉及任何具体项目的细节,只聚焦于分支策略和发布管理。

git-model@2x

为啥选用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 组成的子团队。

一个成功的 Git 分支模型_图示-43b13e4e5b3c410fbd898090b857240f.png
【图:开发者之间形成子团队的协作示意图】

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


开发模型中主要分支

在核心层面,该开发模型深受现有模型的启发。中央仓库包含两个永久存在的主干分支:

  • master / main
  • develop

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

一个成功的 Git 分支模型_图示-3a35c47b45d64f8f8d8795f22a29fb3a.png
  • 可能从develop
  • 必须合并回develop
  • 命名规范:除 masterdeveloprelease-*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)完成。这样做可以避免丢失功能分支存在的历史信息,并将实现该功能的所有提交归为一组。对比以下两种情况:

未标题-1
【图:使用 --no-ff 与不使用 --no-ff 的历史记录对比图】

在后一种情况下,从 Git 历史中无法看出哪些提交共同实现了某个功能——你只能手动阅读所有日志消息。若需回滚整个功能(即一组提交),在后一种情况下会非常麻烦,而如果使用了 --no-ff 则很容易实现。

是的,这会多产生一些(空的)提交对象,但收益远大于成本。


发布(Release)分支

  • 可能从develop
  • 必须合并回developmaster
  • 命名规范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热修复分支

 

一个成功的 Git 分支模型_图示-37feeae221f74e87921a83904ee75789.png
hotfix
  • 可能从master
  • 必须合并回developmaster
  • 命名规范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


评论
User Image
提示:请评论与当前内容相关的回复,广告、推广或无关内容将被删除。

相关文章
回顾这个模型构思于 2010 年,距今已超过十年——而 Git 本身也才刚刚诞生不久。在这十年间,git-flow(本文所描述的分支模型)在众多软件团队中变得极
在现代软件开发中,版本控制是团队协作的基石。而 Git 作为最流行的分布式版本控制系统,其强大的分支功能让开发者能够高效地并行开发、测试和发布代码。然而,如何合
    // 删除本地分支 git branch -d localBranchName // 删除远程分支 git push origin --delete remoteB...
git版本需要大于2.28.0 执行命令 git config --global init.defaultBranch main 搞定
Merge 与 Rebase不知怎么,git rebase 命令被赋予了一个神奇的污毒声誉,初学者应该远离它,但它实际上可以让开发团队在使用时更加轻松。你可以
idea git合并代码说 please tell me who are you..  
处理回滚有两种方案是软回滚,保留中间的git记录,让最新的commit代码与所选恢commit复版本相同。硬回滚,直接干掉所选回滚记录前的所有commit记录方法一(软回滚)(正式多人项目推荐)...
一句通俗话介绍 Git Cherry-Pick:“cherry-pick 就是从别的分支‘摘’一个或几个提交,直接‘贴’到当前分支上。” 详细使用说明1. 作用
stash命令作用stash 命令能够将还未 commit 的代码暂存起来,让你的工作目录变得干净,同时讲解idea中stash界面使用操作。应用场景某一天你正
一句话先说透(请刻进 Git 肌肉记忆):git rebase 就是把你本地的“新活儿”(提交)先拿下来,等别人最新的活儿干完后,再把你的活儿重做一遍,假装你一
问题描述git 提交代码报错 :error: RPC failed; HTTP 413 curl 22 The requested URL returned error: 413导致原因1. 本...
背景该方式用于合并代码非常有用步骤1:拉取需要合并的分支到本地 步骤2:Merge 提示:不要直接点右下角的分支,"Merge into current",该操作会合并后自本地提交
Commit message 场景格式规范每次提交,Commit message 都包括三个部分:header,body 和 footer。&lt;type&gtl;(&lt;sc
ideagit回滚版本