Git工作流:Git-Flow
文章目录
简介
Gitflow是一个基于feature分支管理的版本发布方案。它是由Vincent Driessen设计研发,开源项目地址gitflow-avh。
大致流程是:
- 不同feature在不同feature分支上单独开发(或测试)。
- 确定版本号和此版本将要发布的功能后,将相应featustre分支统一向develop分支合并,然后拉出新的release预发布分支。
- release分支作为持续集成关注的分支,修复bug。
- 待release分支测试验收通过后,统一向master分支和develop分支合并,并在master分支打tag。
- 根据tag发布apk版本。
若线上发现严重bug,需走hotfix流程。
- 基于master分支拉出hotfix分支修复线上问题。
- bug修复完成统一向master和develop分支合并。
- master分支打上新的tag,发布新版本。
下图是Gitflow发布的经典图片,直观反映了Gitflow发布的全流程。
它的特点是能灵活的根据实际需求发布相应版本,较好的支持并行开发,历史版本用tag进行维护。
下面将介绍如何使用Gitflow命令完成上述版本发布,一条Gitflow指令可能对应了一系列git命令,为的是规范化开发流程,提高代码管理效率。
分支
Gitflow使用两个分支来记录项目开发的历史,而不是使用单一的master分支。在Gitflow流程中,master只是用于保存官方的发布历史,而develop分支才是用于集成各种功能开发的分支。使用版本号为master上的所有提交打标签(tag)也很方便。
事实上,Gitflow流程就是围绕这两个特点鲜明的分支展开的。
用于功能开发的分支
每一个新功能的开发都应该各自使用独立的分支。为了备份或便于团队之间的合作,这种分支也可以被推送到中央仓库。但是,在创建新的功能开发分支时,父分支应该选择develop(而不是master)。当功能开发完成时,改动的代码应该被合并(merge)到develop分支。功能开发永远不应该直接牵扯到master。
注意:组合使用功能开发分支和develop分支的这种设计,其实完全就是Feature Branch Workflow的理念。然而,Gitflow流程并不止于此。且看下文分解。
用于发布的分支
一旦develop分支积聚了足够多的新功能(或者预定的发布日期临近了),你可以基于develop分支建立一个用于产品发布的分支。这个分支的创建意味着一个发布周期的开始,也意味着本次发布不会再增加新的功能——在这个分支上只能修复bug,做一些文档工作或者跟发布相关的任务。在一切准备就绪的时候,这个分支会被合并入master,并且用版本号打上标签。另外,发布分支上的改动还应该合并入develop分支——在发布周期内,develop分支仍然在被使用(一些开发者会把其他功能集成到develop分支)。
使用专门的一个分支来为发布做准备的好处是,在一个团队忙于当前的发布的同时,另一个团队可以继续为接下来的一次发布开发新功能。这也有助于清晰表明开发的状态,比如说,团队在汇报状态时可以轻松使用这样的措辞,“这星期我们要为发布4.0版本做准备。”从代码仓库的结构上也能直接反映出来。常用的一些措辞还有:基于develop新建分支,合并入master;命名规则为:release-或release/
用于维护的分支
发布后的维护工作或者紧急问题的快速修复也需要使用一个独立的分支。这是唯一一种可以直接基于master创建的分支。一旦问题被修复了,所做的改动应该被合并入master和develop分支(或者用于当前发布的分支)。在这之后,master上还要使用更新的版本号打好标签。
这种为解决紧急问题专设的绿色通道,让团队不必打乱当前的工作流程,也不必等待下一次的产品发布周期。你可以把用于维护的分支看成是依附于master的一种特别的发布分支。
安装
|
|
初始化
先将远程仓库克隆到本地。
|
|
初始化Gitflow配置。
|
|
命令行会提示你是否修改Gitflow提供的默认分支前缀。不同场景的分支前缀不同,默认情况下分支前缀是这样的:
分支前缀的作用是区分不同分支的使用场景,同时当你使用Gitflow命令时就不需手动添加分支前缀了,Gitflow会帮你加上。
比如开发新功能需创建一个feature分支,名为gitworkflow,使用下面的代码将会创建一个名为feature/gitworkflow本地分支。
|
|
通常情况下不需要修改默认的命名前缀,只需加上-d就可跳过修改命名阶段。
|
|
feature
通常来说,一种场景的完整生命周期应至少包含以下几种行为:
- start 开始开发
- publish 发布到远程分支
- finish 完成开发、合并到主分支
我们首先以feature场景为例,看看如何完成工作流。
start
新功能开始开发前,需准备好开发分支。
|
|
执行上面的命令将会在本地创建名为<feature_name>
的分支,并切换到该分支,而且不论当前所处哪个分支都是基于develop分支创建,相当于执行了下面的git的命令。
|
|
需要注意基于的是本地的develop分支,执行此命令前一般需要拉取最新的远程代码。
publish
在本地开发完成新功能并commit后,需要将本地代码push到远程仓库。
|
|
这行指令做了三件事。
- 创建一个名为feature/
<feature_name>
的远程分支。 - 本地分支track上述远程分支。
- 如果本地有未push代码,则执行push。
转换成git命令就是下面的样子:
|
|
注意: 如果已经执行过publish后又有新的代码需push,再次执行将报错,因为它始终会试图创建一个远程分支。此时需执行正常的push命令git push origin。
finish
当功能开发完毕后就将进入测试阶段,此时需将一个或多个feature分支统一合并到develop分支。
|
|
这行指令也做了三件事。
- 切换到develop分支。
- 合并代码到develop分支
- 删除本地feature/
<feature_name>
分支。
等价于执行下面的git命令:
|
|
说到merge,就不得不提merge采用的策略,我们使用git merge命令时默认(主分支没有新的提交、没有冲突等情况)使用的是fast-forward模式(以下简称ff),即只移动HEAD指针而不会生成提交记录。 而Gitflow稍有不同,默认情况下它会检查本次merge有多少次commit记录,如果仅有一条采用ff模式,如果超过一条则采用no-ff模式,no-ff模式下会多生成一条merge的commit记录。
这样做的好处是当只有一条提交记录时如果生成一条merge记录实际上会复杂化代码记录的管理;当有多条commit记录时生成的一个merge记录,可以方便的进行代码回退和记录检查。
回到finish主题,如果merge时发生了冲突,则在第二步merge时终止流程,即不会再删除本地分支。但当前已处于develop分支,待本地冲突解决并commit后,重新执行git flow feature finish <feature_name>
即可完成finish流程。
细心的同学可以已经发现finish还有两件事没做。
- develop分支代码还未push。
- 未删除远程分支feature/
<feature_name>
。
也就是还需执行
|
|
另外,finish指令支持三个附加参数。
- -r 即merge前先执行rebase(但即使rebase后符合ff条件也不一定会用ff)。
- -F 即合并完成后连同远程分支一并删除。
- -k 保留本地feature分支,即不执行delete动作。
所以如果想连同远程分支一并删除可使用。
|
|
复制代码如果你对feature指令感兴趣,下面是其支持的所有指令。
|
|
release
下面我们再来看release场景,连同之前的feature场景,整个流程如下。
当新功能开发完毕,将进入测试阶段,此时需要基于develop分支拉出release分支进行集成测试,也有将release场景作为预发布环境进行测试的,即feature场景已完成常规测试,在这种情况下,一般而言release只有少数改动。在这里我们先不讨论项目流程问题。
使用start指令开启一个release场景,通常以版本号命令,我们以v2.0为例:
|
|
此命令会基于本地的develop分支创建一个release/v2.0分支,并切换到这个分支上。
为了让其他协同人员也能看到此分支,需要将其发布出去。
|
|
以上和feature场景十分类似。
待测试通过需要发布正式版:
|
|
这一步做的动作有点多,大致是:
- git fetch
- release/v2.0分支代码向master合并。
- 生成名为v2.0的tag。
- release/v2.0分支代码向develop合并。
- 删除本地release/v2.0分支。
- 切换回develop分支。
如果merge产生冲突不会终止流程,只是不会将本地的release分支删除,待解决完冲突后需再次执行finish指令。
另外需要注意的是,如果本地还有未finish的release分支,将不允许使用start指令开启新的release分支,这一点是对并行发布的一个限制。
release finish只是完成了本地代码的一系列操作,还需要同步到远程仓库。
|
|
或者使用下面的命令推送所有的分支和tag。
|
|
hotfix
当tag打完,也表示正式版本发布出去了,如果此时在线上发现了严重的bug,需要进行紧急修复,流程如下:
此时我们假设版本号变为v2.0-patch。
|
|
这将创建一个hotfix/v2.0本地分支并切换到该分支。
hotfix没有publish指令,认为hotfix应该是个小范围改动,不需要其他协同人员参与。
待本地修改结束commit后,执行finish指令。
|
|
按照Gitflow工作流,它会执行下面的任务,与release基本一致。
- git fetch
- hotfix/v2.0-patch分支代码向master合并。
- 生成名为v2.0-patch的tag。
- hotfix/v2.0-patch分支代码向develop合并。
- 删除本地hotfix/v2.0-patch分支。
- 切换回develop分支。
实践:SourceTree
我们配合SourceTree工具详细说明一下Gitflow在不同的阶段是如何工作的。
初始化
首先将远程代码仓库clone到本地,打开SourceTree,在右上角有一个Git Flow的按钮,单击后如图:
建议不做任何修改,直接OK。SourceTree会自动化进行一些操作,最明显的变化是多了一个develop分支。
将新建的develop分支推送到远端仓库。从此,代码库里就存在了两个永久性的分支:master和develop,未来所有的开发工作都围绕这两个分支进行派生跟合并。这两个分支也被称为“历史性”分支。
新功能开发
当进入开发阶段,需要开发新功能时,需要先创建一个新的分支,注意,这个分支使用develop分支作为父分支,当新功能完成后合并到develop分支,新功能提交不应该直接与master分支交互。
使用SourceTree可以很方便完成这一过程,初始化结束后,再次单击GitFlow按钮,如图,选择New Feature,然后输入分支名称。
当新功能开发完成后,需要合并分支到develop分支,这时单击GitFlow按钮,选择Finish Current按钮,使用默认设置,单击OK。
提测阶段
当开发进入到测试阶段时,需要给测试人员提供测试包,此时需要在develop创建release分支,测试阶段出现的bug,在release分支上进行修改,测试通过后,将release分支合并到develop和master分支。
使用SourceTree实现这一过程,继续单击GitFlow按钮,如图,选择New Release,然后输入分支名,单击OK。
测试结束后,合并release分支,继续单击GitFlow按钮,选择Finish Current按钮,使用默认设置,然后输入Tag名,单击OK。
确认没有冲突后,将本地推送到远程仓库。
线上维护
当产品通过测试后,就会发版上线。可能会遇到一个紧急问题需要解决。这时需要创建hotfix分支了,需要注意,hotfix分支是唯一从master分支fork出来的分支。修复完后,需要立即合并到master分支和develop分支,master分支应该用新的版本号打好tag。
使用SourceTree完成这一过程步骤,继续单击GitFlow按钮,如图,选择New Hotfix按钮,输入分支名称。
修改,测试,都需要在这个分支上完成。当测试通过后,合并分支,继续单击GitFlow按钮,如图,选择Finish Current按钮,输入tag名称,选择OK。
确定没有冲突后,将修改推送到远端仓库。
Git网络图如下:
至此一个完整的开发周期结束,当然上面仅仅模拟了一个简单开发周期,真正的开发一定会比较复杂,但只要按照这个流程管理代码,再复杂的开发情况,也不会出现问题。
唯一需要注意的时候,在合并代码时会出现冲突,一定要先解决冲突后再推送到远端仓库。
Git有害论
Gitflow通过不同分支间的交互规划了一套软件开发、集成、部署的工作流。听起来很棒,迫不及待想试试了?等等,让我们先看看Gitflow不是什么。
- Gitflow不是Git社区的官方推荐工作流。是的,不要被名字骗到,这不是Linux内核开发的工作流也不是Git开发的工作流。这是最早由Web developer Vincent Driessen和他所在的组织采用并总结出的一套工作流程。
- Gitflow也不是Github所推荐的工作流。Github对Gitflow里的某些部分有不同看法,他们利用简化的分支模型和Pull Request构建了适合自己的工作流Github Flow。
- 现在我要告诉你,Gitflow在企业软件开发中甚至不是一个最佳实践。ThoughtWorks Technology Radar在2011年7月刊,2015年1月刊里多次表明了Gitflow背后的feature branch模型在生产实践中的危害,又在最近一期2015年11月刊里专门将Gitflow列为不被推荐的技术。
为什么Gitflow有问题
Gitflow对待分支的态度就像: Let’s create branches just because… we can!
很多人吐槽吐槽,为什么开发一个新feature非得新开一个branch,而不是直接在develop上进行,难道就是为了……废弃掉未完成的feature时删除一个branch比较方便?
很多人诟病Gitflow太复杂。将这么一套复杂的流程应用到团队中,不仅需要每个人都能正确地理解和选择正确的分支进行工作,还对整个团队的纪律性提出了很高的要求。毕竟规则越复杂,应用起来就越难。很多团队可能不得不借助额外的帮助脚本去应用这一套复杂的规则。
然而最根本问题在于Gitflow背后的这一套feature branch模型。
VCS里的branch本质上是一种代码隔离的技术。使用feature branch通常的做法是:当developer开始一个新feature,基于develop branch的最新代码建立一个独立branch,然后在该branch上完成feature的开发。开发不同feature上的developers因为工作在彼此隔离的branch上,相互之间的工作不会有影响,直到feature开发完成,将feature branch上的代码merge回develop branch。
我们能看到feature branch最明显的两个好处是:
- 各个feature之间的代码是隔离的,可以独立地开发、构建、测试;
- 当feature的开发周期长于release周期时,可以避免未完成的feature进入生产环境。
后面我们会看到,第一点所带来的伤害要大于其好处,第二点也可以通过其他的技术来实现。
merge is merge
说到branch就不得不提起merge。merge代码总是痛苦和易错的。在软件开发的世界里,如果一件事很痛苦,那就频繁地去做它。比如集成很痛苦,那我们就nightly build或continuous integration,比如部署很痛苦,那我们就频繁发布或continuous deployment。 merge也是一样。所有的git教程和git工作流都会建议你频繁地从master pull代码,早做merge。
然而feature branch这个实践本身阻碍了频繁的merge: 因为不同feature branch只能从master或develop分支pull代码,而在较长周期的开发完成后才被merge回master。也就是说相对不同的feature branch,develop上的代码永远是过时的。如果feature开发的平均时间是一个月,feature A所基于的代码可能在一个月前已经被feature B所修改掉了,这一个月来一直是基于错误的代码进行开发,而直到feature branch B被merge回develop才能获得反馈,到最后merge的成本是非常高的。
现代的分布式版本控制系统在处理merge的能力上有很大的提升。大多数基于文本的冲突都能被git检测出来并自动处理,然而面对哪怕最基本的语义冲突上,git仍是束手无策。在同一个codebase里使用IDE进行rename是一件非常简单安全的事情。如果branch A对某函数进行了rename,于此同时另一个独立的branch仍然使用旧的函数名称进行大量调用,在两个branch进行合并时就会产生无法自动处理的冲突。
如果连rename这么简单的重构都可能面临大量冲突,团队就会倾向于少做重构甚至不做重构。最后代码的质量只能是每况愈差逐渐腐烂。
持续集成
如果feature branch要在feature开发完成才被merge回develop分支,那我们如何做持续集成呢?毕竟持续集成不是自己在本地把所有测试跑一遍,持续集成是把来自不同developer不同team的代码集成在一起,确保能构建成功通过所有的测试。按照持续集成的纪律,本地代码必须每日进行集成,我想大概有这几种方案:
- 每个feature在一天内完成,然后集成回develop分支。这恐怕是不太可能的。况且如何每个feature如果能在一天内完成,我们为啥还专门开一个分支?
- 每个分支有自己独立的持续集成环境,在分支内进行持续集成。然而为每个环境准备单独的持续集成环境是需要额外的硬件资源和虚拟化能力的,假设这点没有问题,不同分支间如果不进行集成,仍然不算真正意义上的持续集成,到最后的big bang conflict总是无法避免。
- 每个分支有自己独立的持续集成环境,在分支内进行持续集成,同时每日将不同分支merge回develop分支进行集成。听起来很完美,不同分支间的代码也可以持续集成了。可发生了冲突、CI挂掉谁来修呢,也就是说我们还是得关心其他developer和其他团队的开发情况。不是说好了用feature branch就可以不管他们自己玩吗,那我们要feature branch还有什么用呢?
所以你会发现,在坚持持续集成实践的情况下,feature branch是一件非常矛盾的事情。持续集成在鼓励更加频繁的代码集成和交互,让冲突越早解决越好。feature branch的代码隔离策略却在尽可能推迟代码的集成。延迟集成所带来的恶果在软件开发的历史上已经出现过很多次了,每个团队自己写自己的代码是挺high,到最后不同团队进行联调集成的时候就傻眼了,经常出现写两个月代码,花一个月时间集成的情况,质量还无法保证。
如果不用Gitflow…
如果不用Gitflow,我们应该使用什么样的开发工作流?如果你还没听过Trunk Based Development,那你应该用起来了。
是的,所有的开发工作都在同一个master分支上进行,同时利用Continuous Integration确保master上的代码随时都是production ready的。从master上拉出release分支进行release的追踪。
可是feature branch可以确保没完成的feature不会进入到production呀。没关系,Feature Toggle技术也可以帮你做到这一点。如果系统有一项很大的修改,比如替换掉目前的ORM,如何采用这种策略呢?你可以试试Branch by Abstraction。我们这些策略来避免feature branch是因为本质上来说,feature branch是穷人版的模块化架构。当你的系统无法在部署时或运行时切换feature时,就只能依赖版本控制系统和手工merge了。
Branch is not evil
虽然long lived branch是一种不好的实践,但branch作为一种轻量级的代码隔离技术还是非常有价值的。比如在分布式版本控制系统里,我们不用再依赖某个中心服务器,可以进行独立的开发和commit。比如在一些探索性任务上,我们可以开启branch进行大胆的尝试。
技术用的对不对,还是要看上下文。
## 个人理解
笔者认为如果你正在参与一个企业级的项目,一个版本将要上线的features可能因各种原因变动或下线,此时如果没有独立的feature分支而全部在主干分支,将极难处理代码的回滚和保护;换个角度说,merge冲突也可以通过其他方式规避,比如良好的代码分层、事先约定好跨模块通信的接口等。
从持续集成的角度来说,由于Gitflow管理的分支类型较多,不应所有分支都参与持续集成,可结合实际场景选择开启持续集成的分支类型,比如master和release。
参考:
https://juejin.im/post/5d565f9c6fb9a06aca381990
http://insights.thoughtworkers.org/gitflow-consider-harmful/
http://blog.didispace.com/gitflow-info/
https://www.jianshu.com/p/eb293fd79802
文章作者 Forz
上次更新 2020-02-25