您的位置:  首页 > 技术杂谈 > 正文

一个成功的 Git 分支模型

2021-08-01 22:10 https://my.oschina.net/nehcoab/blog/5117652 nehcoab 次阅读 条评论

译自:A successful Git branching model

反思笔记

更新:2020-03-05

本文中提出的分支模型(git-flow)是在 2010 年构思的,那时 Git 才诞生不久,如今十年已过。在这十年中,该模型逐渐在众多软件团队中大放异彩,以至于人们开始将其视为某种标准 —— 但遗憾的是,更有甚者视其为教条或者所谓万能药。

在这十年中,Git 席卷全球,而使用 Git 开发的那些最流行的软件,其类型则更多的转向 Web 应用。而 Web 应用通常是持续交付的,不会回滚,你无需支持市场中同时运行的多个版本。

这并非我十年前写下这篇博文时想到的那种软件。如果你的团队正在进行软件的持续交付,我建议采用更简单的工作流程(如 GitHub flow),而不是试图将 git-flow 硬塞进你的团队。

然而,如果你正在构建明确版本化的软件,或者需要支持市场中多个版本的软件,那么 git-flow 可能仍然适合你的团队,就像大家在过去十年中使用的那样。在这种情况下,请继续读下去吧。

总而言之,一定要记住,万能药并不存在。应因需取求,对症下药。

介绍

在本文中,我将介绍一种我从大约一年前开始为一些工作及私人项目引入的开发模型,事实证明其非常成功。我筹备这篇文章已经有一阵子了,但在此之前一直没有找到时间动笔。我不会在此谈论任何项目的细节,仅专注于分支策略和发布管理。

为何选用 Git

有关 Git 与集中式源代码控制系统相比的优缺点的详尽讨论,请参见 此页面,那里有很多火爆的唇枪舌战。而作为一名开发者,如今我对 Git 的喜爱程度胜过所有其他工具。Git 切实改变了开发者对合并及分支的理解。在传统的 CVS 或 Subversion 中,合并或分支是一种会让人担惊受怕的,偶尔才做一次的操作。

但对于 Git 来说,这些操作是非常简单的,实际上其是日常工作流程的核心部分之一。例如,在 CVS 或 Subversion 的书籍中,分支和合并操作总会在那些针对进阶用户的章节才会进行讨论,而在 Git 书籍中,在介绍基础知识的章节便已涉及。

由于其简单易用且会高频使用,分支和合并操作便不再另让人恐惧。版本控制工具理应比其他工具更适合辅助分支和合并操作才对。

对工具的讨论到此为止,下面来进入开发模型。我在此介绍的模型,本质上不过是为了使开发过程可管理,而要求每一名团队成员遵循的一套流程。

去中心化但却集中

我们所使用的与本分支模型配合良好的仓库配置,需要有一个「真实的」中央仓库。请注意,此仓库仅仅被视为中央仓库(由于 Git 是一种分布式版本控制系统,因此在技术层面上没有中央仓库之类的概念)。我们将此仓库称为 origin,因为所有的 Git 用户都熟悉这个名字。

每个开发者都从 origin 拉取(pull)并向其推送(push)。但是除了这种集中式的推拉之外,每个开发者也可以从其他同伴那里拉取代码变更,以组成一个子团队。例如,两个或多个开发者协同开发一个重要的新功能时,可避免将代码过早的推送到 origin。在上图中,便有 Alice 与 Bob、Alice 与 David、以及 Clair 与 David 的子团队。

从技术角度讲,这仅表示 Alice 定义了一个名为 bob 的 Git remote,指向了 Bob 的仓库,反之亦然。

主分支

本质上,该开发模型很大程度上是从现有模型获得的灵感。中央仓库包含两个具有无限生命周期的主分支:

  • master
  • develop

每个 Git 用户都应该熟悉 origin 中的 master 分支。与 master 分支还并行存在一个 develop 分支。

我们认为 origin/master 是一个其 HEAD 源码总保持 产品就绪(production-ready) 状态的主分支。

我们认为 origin/develop 是一个其 HEAD 源码总反映下一个发行版最新交付的开发变更的主分支。有人将其称为「整合分支」。这就是自动部署每日构建版(nightly builds)的代码来源。

develop 分支中的源码达到一个稳定的状态并准备发布时,所有的代码变更都应该以某种方式合并回 master,然后标记一个发行版本号。具体细节将在后面进一步讨论。

因此,每当代码变更被合并回 master,都将定义一个新的生产版本。此操作应当被严格的限制,所以理论上每当 master 有提交时,可以使用 Git hook 脚本自动构建软件并部署到生产服务器。

辅助分支

除了主分支 masterdevelop 之外,我们的开发模型还使用各种辅助分支来协助团队成员之间的协同开发,简化功能的追踪,为生产版本做准备,并协助快速解决线上版本的问题。与主分支不同,这些分支的生命周期总是有限的,因为他们终将被删除。

这些特殊的分支类型包括:

  • 功能(Feature)分支
  • 发行版(Release)分支
  • 热修复(Hotfix)分支

以上每一条分支都有明确的目的,并受严格的规则约束,即哪些分支可能是其原始分支,以及哪些分支必须是其合并到的目标分支。我们马上就将深入了解。

从技术角度看,这些分支并不是「特殊的」。这些分支类型实际是按我们的使用方式进行分类的。其当然依旧只是普通的 Git 分支。

功能分支

可能的原始分支:

  develop

最终必须合并到的分支:

  develop

分支命名规范:

  除 masterdeveloprelease-*hotfix-* 之外的任何名称

功能分支(或有时称为主题分支)为即将发布或将来要发布的版本开发新功能。最初着手开发一个新功能时,可能还不知道该功能将要合并到的目标版本。功能分支的本质是只要功能正在开发中便存在,但最终会合并回 develop (以确保将新功能添加到即将发布的版本中)或丢弃(对其不满意)。

功能分支通常只存在于开发者的仓库中,而不在 origin 中。

创建一个功能分支

在开始开发一个新功能时,首先从 develop 分支检出。

$ git checkout -b myfeature develop
Switched to a new branch 'myfeature'

将已完成的功能合并到 develop

可以将已完成的功能合并到 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 参数则很容易做到。

没错,这虽然会创建一些(空的)提交对象,但收益远大于成本。

发行版分支

可能的原始分支:

  develop

最终必须合并到的分支:

  developmaster

分支命名规范:

  release-*

发行版分支用来为新的生产版本做准备。它允许在最后一刻做一些细节修改。此外,它允许进行细微的 bug 修复并为该发行版准备元数据(版本号、构建日期等)。通过在发行版分支上完成这些工作,develop 分支将可以轻松接受下一个大版本的功能需求。

develop 分支检出一个新的发行版分支的重要时机是 develop(差不多)达到了新版本所需的理想状态。此时至少必须已合并了待构建发行版的所有功能。对于所有未来准备发布的功能必须等到发行版分支创建之后再合并。

创建发行版分支最开始就要为即将发布的版本分配一个版本号。在这之前,develop 分支反映的都是「下一个发行版」的更改,但在发行版分支创建之前,「下一个发行版」最终叫 0.3 还是 1.0 都是不确定的。这个决定是在发行版分支创建之时根据项目的版本号规则制定的。

创建一个发行版分支

发行版分支是从 develop 分支创建的。例如,当前的生产版本是 1.1.5,并且我们即将发布一个大版本。develop 分支已经为「下一个发行版」做好准备,我们决定将其升级为 1.2(而非 1.1.6 或 2.0)。因此我们将发行版分支检出,并起一个反映新版本号的名称:

$ 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 脚本,它会更改工作环境中的某些文件以反映新版本号。(当然也可以手动更改 —— 这里仅仅是说明某些文件会被更改)然后修改了版本号的文件被提交。

这个新分支可能会存在一段时间,直到可以确定发布该版本为止。在此期间,可能会在此分支(而不是 develop 分支)进行一些 bug 修复。严禁在此添加大型的新功能,其必须合并到 develop 分支中,因此需要等待下一个大版本。

完成一个发行版分支

当发行版分支的状态准备好成为一个真正的发行版时,需要执行一些操作。首先,将发行版分支合并到 master 中(记住这点,因为 master 上的每个新提交都是我们所定义的新发行版)。接下来,必须打一个标签,方便以后引用这个历史版本。最后,需要将发行版分支上所做的更改合并到 develop 中,以便将来的发行版中也包含这些 bug 修复程序。

前两步的操作:

$ 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).

热修复分支

可能的原始分支:

  master

最终必须合并到的分支:

  developmaster

分支命名规范:

  hotfix-*

热修复分支与发行版分支非常相似,尽管其是计划外的,但其目的都是为新的生产版本做准备。它们是对生产环境的不良状态所采取的行动。当生产环境的缺陷必须马上修复时,热修复分支可以基于 master 分支上与线上版本对应的标签创建。

其本质是团队成员(在 develop 上)的工作可以继续,而另一个人准备生产环境的快速修复。

创建热修复分支

热修复分支是从 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(-)

完成一个热修复分支

完成后,该热修复需要合并回 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> 参数来对 tag 进行加密签名。

然后,将此修复也合并到 develop 中:

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

该规则有一个例外,当存在一个发行版分支时,需要将该热修复的改动合并到该发行版分支中,而非 develop。将热修复合并到发行版分支中时,当发行版分支完成时,最终也会将热修复合并到 develop 中。(如果立即从事的开发工作需要修复此 bug,而又不能等待发行版分支完成,也可以安全的将热修复合并到 develop 中。)

最后,删除临时的分支:

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

总结

尽管此分支模型没有什么真正令人震惊的新东西,但本文开头的「大图」在我们的项目中被证明非常有用。它形成了一个易于理解的优雅思维模型,并使团队成员可以对分支和发布过程形成共识。

这里也提供了该图的 高质量 PDF 版本。可将其挂在墙上以随时快速参考。

更新:如果有人需要,这里有份主图的 Keynote 源文件

展开阅读全文
打赏
6 赞
64 收藏
分享
加载中
正好看十年前看过你十年前的版本。
实践下来,按照你的思维方式没有问题。
但是操作上,没必要用“合并”,而是用“衍合”
就是按照你说的过程去操作,但是不是让git的代码图呈现你画图的效果。
“衍合”可以保持单条路线,对于代码树的“修改历史”方便多了。
建议大家都用“衍合”rebase 代替“合并”。
一家之言,不喜勿喷。
07/02 11:00
回复
举报
这么说吧,一般 rebase 用在多人协同开发同一个 feature branch 的情况,可以让提交保持清晰可读。如果合并到主分支的时候还用 rebase,那么完全失去了 Git 分支的意义。都叫分支了,怎么可能是单条路线。
07/11 06:14
回复
举报
nehcoab博主
补充一下,除了协同开发的情况,本地分支在 merge 回主分支之前要 rebase 主分支的代码,或平常也可以经常 rebase 主分支代码,顺便 fix conflicts,即在 feature-a 分支下执行 git rebase develop。但一个重要原则是,不要在主分支上使用 rebase,即不能在 master/develop 分支下执行 git rebase feature-a,而应该使用 git merge --no-ff -m "feature A finished" feature-a。
07/11 09:52
回复
举报
嗯,这个可能是因为我们面临的具体情况细节不同造成。 开始我也这么搞,但是反倒造成了混乱(很多事情不在控制范围之内)。 后来干脆简化掉,要求他们全部以发布的分支为基准衍合上来,不发布的就别衍合上来。发布分支不准强推,等等。反倒乱七八糟的问题少了不少。 在一个人员不是自发使用git而是被推动使用git的单位,你要改变什么本来就遭人恨。你再提高人家学习成本就更难执行了。 一个好的措施,首先要是一个能执行下去的措施,才能使好措施。
07/12 09:34
回复
举报
nehcoab博主
该评论暂时无法显示,详情咨询 QQ 群:912889742
nehcoab博主
回复的内容不知道为啥被吃了,总之我也在一个使用 SVN 的公司推行过 Git,很理解你。
07/12 10:04
回复
举报
嗯,对,基本就是类似情况。你懂我懂,哈哈。之前在另一个公司,从 cvs 换道 svn 就碰到了类似情况。 后来,另一家公司,从 svn 想换到 git 就是历史重演。 “历史从来不会重复,历史只会重演。”
07/13 11:12
回复
举报
请教个问题,git-flow模型,假设此时已经发了多个版本,master上就有多个tag,比如1.0、1.1、2.0、3.0,假设都要维护,突然1.0有个bug修改,但改了之后会导致不兼容咋办?合不合到master呢?
07/02 10:01
回复
举报
不能合并回master的分支,等于另一个产品.
07/02 10:11
回复
举报
nehcoab博主
基础 git-flow 不能满足于同时维护多个主要版本,后来大家为解决此需求在其中加入了 support 分支,该分支在受支持的生命周期中与 master 和 develop 分支共同存在。具体方法可以搜一下,比如这里 https://stackoverflow.com/questions/64233413
07/02 10:58
回复
举报
装个sourcetree
07/02 09:29
回复
举报
文章很不错。
07/02 09:15
回复
举报
更多评论
  • 0
    感动
  • 0
    路过
  • 0
    高兴
  • 0
    难过
  • 0
    搞笑
  • 0
    无聊
  • 0
    愤怒
  • 0
    同情
热度排行
友情链接