这篇还是讲一讲烂大街的东西:Git工作流,内容源自我工作中在团队的实践与分享,Git还是蛮重要的,在如今协作为上的情况下(虽然但是单打独斗也是常态)。

项目开发中,多人协作,分支很多,虽然各自在分支上互不干扰,但是我们最终需要把分支合并到一起,而实际项目中涉及到很多问题,例如版本迭代,版本发布,bug 修复等,为了更好的管理代码,需要制定一个工作流程,也就是所谓的工作流。

Git工作流可以理解为团队成员遵守的一种代码管理方案,工作流不涉及任何命令,因为它就是一个规则,完全由开发者自定义,并且自遵守。

对于Git工作流的使用和分析有篇比较经典的教程文章,出自atlassian(jira母公司),以及译文(译文比较老了有些对不上,但是也有阅读价值)推荐阅读参考,本文也大量参考了这些文章。

什么是成功的Git工作流

在评估团队的工作流程时,最重要的是考虑团队的文化。成功的工作流能够提高团队的效率,而不是成为限制生产力的负担。在评估一个 Git 工作流时主要需要考虑的一些点:

  • 这个工作流是适配团队规模
  • 使用这个工作流时是否易于撤销/回滚失误和错误
  • 这个工作流是否给团队带来了新的不必要的认知开销

典型工作流

介绍一些在git的使用过程中典型、大量使用的工作流,展示工作流的具体使用形式。

集中式工作流

集中式工作流是最为简单基础的工作流,对于从SVN过渡而来的团队来说集中式工作流会非常容易理解和上手。与 Subversion 一样,集中式工作流使用中央存储库作为项目所有更改的单一入口点。缺省的开发分支被称为 main,而不是 trunk,所有的更改都提交到这个分支中。此工作流不需要除 main 以外的任何其他分支。

在此工作流中中央仓库只有1个master主分支,各个开发人员clone主分支到本地进行开发,所有更改都提交到这个本地分支,因为本地和中央仓库是完全隔离的,开发者可以把和上游的同步延后到一个方便时间点。

由于所有修改最终都会同步到中央仓库的单个分支,因此在开发人员本地开发完成后如果中央仓库分支已经被其他开发人员更新,其需要先同步中央仓库分支(git pull)解决可能出现的冲突后再重试(git push)

集中式工作流对于小团队来说是很棒的。但随着团队规模的扩大,解决冲突的过程可能会成为瓶颈。

功能分支工作流

功能分支工作流背后的核心思想是所有的功能特性开发应该在一个专门的分支而不是主分支中进行。这种封装使得多个开发人员可以轻松地处理特定的功能Feature而不会干扰主代码库,与此同时它还使Pull Request成为可能(PR可以简单理解为请求仓库owner同步你的分支),在发起PR时可以看到代码的改动和变更以及对代码发表评论,这让团队对彼此的工作进行评估变得非常容易(Code Review),也提供了对Feature准入的决断能力。

该工作流也是比较简单的,与集中式工作流相比,最大的区别在于开发者的修改被归属到一个个Feature,每个Feature对应一个分支(中央+本地副本)。每个Feature开发完毕后发起Pull Request,其他开发人员有机会在该分支合入主分支的一部分之前审查这些变更。

功能分支工作流是一种可组合的工作流,可以被其他高级 Git 工作流所利用,它专注于分支模型,这意味着它是管理和创建分支的指导框架,其他工作流则更加关注代码仓库本身。

Gitflow工作流

Gitflow工作流是一个帮助软件实现持续开发和DevOps的工作流,它定义了一个围绕项目版本设计的严格的分支模型,为管理大型项目提供了一个健壮的框架。它非常适合那些有计划发布周期的项目,也非常适合持续交付(Continues Delivery)的 DevOps 最佳实践。

该工作流没有添加任何相较于功能分支工作流之外的新概念和命令,区别在于它为不同的分支分配了具体的角色,并定义了它们应该如何以及何时进行交互。

在此工作流中主要包含了如下几种分支:

  • master:主分支,该分支每次进行合并都会打上tag版本标签
  • develop:开发分支,集成各个feature,是开发和发布的桥梁
  • feature:功能分支,它的母分支是develop分支,每个功能都驻留在自己的分支中,develop和feature的工作模式实际上就是功能分支工作流
  • release:发布分支,母分支也是develop分支,主要用作发布周期处理,该分支创建后不再接受新的功能,只做bug修复、文档生成和其他发布相关操作
  • hotfix:修复分支,母分支是master分支,用于快速修复已经发布版本的bug

他们之间的创建、合并关系主要如下图:

  1. 基于develop分支和feature分支做功能特性开发,即功能分支工作流操作
  2. 在需要进行发布时,从develop分支check出release分支,在release分支上做发布相关操作(bug修复、生成文档、其他发布相关)
  3. 发布准备就绪即release分支稳定后,release分支会合并到master分支并打上版本tag,同时也将其合并到develop分支上,因为release分支上可能也会做一些更改
  4. 如果发布的代码出现问题需要快速修复,则可以从master分支check出hotfix分支进行bug修复,修复完成后合并到master分支并打上新的版本tag,同时也会合并到develop分支

Forking工作流

Forking工作流与前面讨论的几种工作流有根本的不同,这种工作流不使用单个服务端仓库作为中央代码库,而是为每个独立的开发者提供自己的服务端存储库。这意味着每个贡献者有两个服务端git仓库:一个私有的个人仓库、一个项目维护这的公开仓库。Forking工作流常用于公共开源项目中,最常见的就是github的fork操作。该工作的基本流程如下:

  1. 开发人员fork一份官方的服务端仓库,这将创建出属于他自己的服务端仓库
  2. 将新的服务端仓库clone到开发人员本地系统
  3. 将官方服务端仓库地址添加到git remote path中(通常命名为upstream)
  4. 创建新的本地功能分支
  5. 在新的本地分支上进行修改、开发,然后commit
  6. 将分支push到开发人员自己的服务端仓库
  7. 开发人员指定这个分支和官方仓库来创建一个pull request
  8. 官方维护人员同意了这个PR并给予合并许可,该分支被合并到官方服务端仓库

需要注意的几个点:

  1. fork并不是什么特殊操作,它只是在服务端执行的clone,由第三方git服务管理和托管

  2. 该工作流的核心是分支共享的方式,具体要如何创建、操作分支可以利用之前所述的工作流方式

  3. 该工作流通常需要两个远程地址,默认情况下使用origin来代表服务端仓库,这个在clone到本地的时候会默认创建,你可能需要手动把官方仓库地址添加进来以更加方便地保持本地仓库的最新状态

    git remote add upstream https://the.official.repo.address

Forking工作流的主要优点是可以集成贡献,而不需要每个人都推到单一的中央存储库。开发人员推送到他们自己的服务器端存储库,只有项目维护人员可以推送到正式存储库。这允许维护人员接受来自任何开发人员的提交,而不必向他们提供对官方代码库的写访问权限。

Gtihub工作流

Github flow是 GitHub 制定并使用的工作流模型,由 scott chacon 在 2011 年 8月 31 号正式发布。发布的主要原因是因为 Git Flow 对于大部分开发人员和团队来说,稍微有些复杂。它简化了流程,专门配合”持续发布”,官方推荐流程如下:

  1. 根据需求,从master拉出新分支,不区分功能分支或补丁分支。
  2. 新分支开发完成后,或者需要讨论的时候,就向master发起一个pull request
  3. Pull Request既是一个通知,让别人注意到你的请求,又是一种对话机制,大家一起评审和讨论你的代码。评审过程中,还可以不断提交代码。(PR既可以是一个仓库内也可以是不同仓库之间)
  4. Pull Request被接受,合并进master,重新部署后,原来拉出来的那个分支就被删除。(先部署再合并也可。)

可以看到这个工作流是非常简单的,只有一个master分支作为主分支,默认master分支的最新代码就是当前的线上代码。缺点是这样无法满足一些复杂场景,比如发布周期有要求或者时间较长(审核),这样线上的版本会与master不一致。

Gitlab工作流

Gitlab flow 是 Git flow 与 Github flow 的综合。它吸取了两者的优点,既有适应不同开发环境的弹性,又有单一主分支的简单和便利。它是 Gitlab.com 推荐的做法。这篇官方文档不仅包含了工作流相关的内容,还有一些gitlab的使用技巧,比较实用,值得看看。针对不同使用场景它推荐了两种使用形式。

持续发布形式

该形式最大原则叫做”上游优先”(upsteam first),即只存在一个主分支master,它是所有其他分支的”上游”。只有上游分支采纳的代码变化,才能应用到其他分支。

它建议在master分支以外,再建立不同的环境分支。比如,”开发环境”的分支是master,”预发环境”的分支是pre-production,”生产环境”的分支是production。开发分支是预发分支的”上游”,预发分支又是生产分支的”上游”。代码的变化,必须由”上游”向”下游”发展。比如,生产环境出现了bug,这时就要新建一个功能分支,先把它合并到master,确认没有问题,再cherry-pick到pre-production,这一步也没有问题,才进入production。只有紧急情况,才允许跳过上游,直接合并到下游分支。

版本发布形式

如果你只需要针对release分支做操作,向外发布软件版本,则建议的做法是每一个稳定版本,都从master分支拉出一个分支,比如2-3-stable、2-4-stable等等。之后只有针对指定版本修补bug,才允许将代码合并到这些分支,并且此时要更新小版本号。

Gitlab使用技巧

总结一下文档中提到的几个实用的使用技巧

Merge/Pull Request

gitlab选用的名称是merge request,因为最终的行为是合并功能分支,而且gitlab通常应用于私有部署环境,项目开发人员工作在同一个仓库,不太会有类似fork的操作。MR的目的是要求指定的人合并两个分支,同时:

  1. 如果你在分支上工作了较长时间或有大量更改,可以使用MR来分享中间结果,创建一个不分配给任何人的MR,所有成员都可以看到你的修改
  2. MR可以直接用来Code Review,并不需要其他单独的CR工具,任何人都可以承诺并推动修复,新的commit推送到服务端后,MR中的差异对比(diff)将会更新
  3. MR的标题、评论可以添加注释前缀(如WIP:work in process,表明正在开发过程中)以及@相关开发人员,所以MR不一定要完全开发完才提
  4. 可以对重要的、长期保留的分支进行保护(protect),保证大多数开发人员不能直接修改它们
  5. 功能分支合并后,应该将它删除,这保证分支列表界面显示的都是正在进行的工作,同时如果有人重新打开了issue可以复用分支名而不至于重复

Issue Tracking

对代码的任何重大更改都应该从描述目标的问题开始。每次代码更改都有一个原因,这有助于告知团队的其他成员,并保持feature分支的范围较小。issue标题应该描述系统的期望状态,比如标题”作为管理员,我希望在不收到错误的情况下删除用户“比”管理员不能删除用户“要好。

通过在commit消息或MR的描述中提到问题来链接到相关issue,例如,“ Fixes # 16”。然后GitLab 会创建与上述issue的链接,并在issue评论中创建相关MR的评论。当包含了”Fixes“或者”closes“这些词的时候,当代码合并到默认分支时Gitlab会自动关闭这些issue。

Commit often and push frequently

每当有一组测试和代码时,都应该做commit。将工作分解成单独的commit为开发人员以后查看代码提供了上下文。较小的commit清楚地说明了一个特性是如何开发的。它们可以帮助您回滚到特定的时间点,或者在不恢复几个不相关的更改的情况下恢复一个代码更改。

commit也通常会帮助你分享你的工作,这很重要,这样每个人都知道你在做什么。你应该频繁地推进你的feature分支,即使它还没有准备好接受评审。通过在特性分支或MR中共享你的工作也可以防止团队成员进行重复工作。在工作完成之前分享你的工作并讨论和反馈有关的变化,这样可以在代码进行评审之前帮助改进代码。

如果一次合并涉及到很多commit,对于撤销不太友好,那么你可以考虑使用 GitLab Squash-and-Merge 特性,在合并之前将所有更改压缩为一个提交来解决这个问题。

Git日常

感觉目前来说很少会有不用Git的开发人员了吧,除了一些略为冷门的开发行业,当然这是我没有任何根据的猜测。在船厂那会做开发的时候还真没怎么用过Git,后面用了SVN还是外面的开发商引入过来的,感觉应该由于开发环境所限吧,那时候大部分情况下还是一个人编写某项功能,纯内网工作(上不了任何外网)。这也从反面反映了Git的一些实用场景,但是即使不使用Git的任何远程功能,拿来做纯本地版本管理也挺不错的,甚至有人基于Git实现了文件系统(Gitfs)。

平常工作少不了与Git打交道,但是真的用的好的人还挺少见的,可能与我的具体环境有关,但是不得不说Git在使用时有很强的灵活性,简单可以很简单,复杂可以很复杂(这是优秀工具的普遍特性)。大部分人对于Git的使用还是偏于随意,所以有不少可视化的UI工具、流程约束工具来作为辅助项帮助良好地使用Git,比如SourceTree、Gitflow等等,而且IDE通常也会对Git有比较好的集成。很难说这些东西对开发人员全是好处,我个人还是比较推崇使用命令来操作,不要太依赖GUI了,不过查看历史记录这类是例外,这种概览式的功能确实是GUI的拿手好戏。由于Git的高度灵活性,在严谨的或者具备一定规模的一些公司可能不会允许开发人员进行自由操作,会把Git嵌入到CI/CD工具中进行完备地权限、流程限制。

在Git的使用过程中,我个人觉得最麻烦地是如何良好地进行commit,对于我来说往往写着写着就超出一个commit的范围或者突然灵感来了把另外的问题给修复了一下,这个时候就比较尴尬,把修改完的代码细拆为几个commit还是有点过于麻烦了,这个时候往往会导致所有修改直接全部作为一个commit,commit信息随便乱写,比如就俩字:“更新”。一来一回整个commit历史就变得很乱,失去了原有的意义。

Git的这种高度灵活性有时候真的会是一种灾难,这里特别推荐了解一下语义提交,最早出现的语义提交规范源自AngularJS项目,团队创建了一个详细的文档来规定他们提交记录的方式和目标。之后又有各种各样的变体,应用于JQuery、Ember等等项目。语义提交使得提交消息具有语义性,提交被分为有意义的类型,表明了提交的本质,它们拥有规范的结构。此外还可以了解使用下git emoji,让commit更加生动。

总结

  1. 功能分支工作流是一个很基础、强大的工作流,可以嵌套到不同复杂的工作流中
  2. 在熟悉工作流时保持对服务端分支和本地分支有清晰的认知,就能够比较快速地理解一些复杂的工作流
  3. Git的灵活度极高,在实战使用时往往需要配合约束工具或者团队规范