GitLab Flow - 合并/分支/发布
引言
使用Git的版本管理使分支和合并比旧版本管理系统(如SVN)容易得多。它允许各种分支策略和工作流程。几乎所有这些都是对git之前使用的方法的改进。但是许多组织止步于一个没有完整定义的、过于复杂或没有集成Issue追踪系统的工作流。因此,我们提出了GitLab Flow作为明确定义的一套最佳做法。它结合了功能驱动开发和使用Issue追踪功能分支。
许多组织从其他版本空置系统转到Git会经常发现难以开发一个有效的工作流程。
来自其他版本控制系统的组织经常发现很难开发有效的工作流程。本文将介绍Git Flow与Issue追踪系统集成在一起的GitLab Flow。它提供了一个简单,透明和有效的方式来进行Git工作。
要使用git,你需要习惯一件事,那就是每当你提交一个和同事共享的版本时,都要先完成三个步骤。而大多数版本控制系统只需完成一步:把工作的备份提交到一个共享的服务器。在git中,你首先要把文件从文件拷贝添加到缓存区中,之后再把他们提交到本地库中,最后把他们推送到远程的共享仓库中。熟悉了这三个步骤后,分支模型就是接下来要面对的问题。
由于很多刚开始使用Git的组织都没有约定如何处理,所以很快就会变得混乱。他们遇到的最大的问题是许多长期运行的分支都包含部分变化。人们很难弄清楚他们应该在哪个分支开发或部署到生产环境中。 对这个问题经常的应对是采用标准化模式,例如 Git Flow
和 GitHub Flow
。我们认为仍有改进的余地,并将详细说明我们称之为 GitLab Flow
的一套做法。
Git Flow
和它带来的问题
Git Flow
比较早的使用的了git分支,并且已经获取了很多的关注。Git Flow
提倡一个master
分支和一个develop
分支,同时也包括了feature
分支、release
分支、hotfix
分支。开发工作在develop
分支进行,然后提交到release
分支,最后合并到master
分支。Git Flow
很标准但是使用很复杂,这导致了两个问题。
第一个问题是程序员必须使用develop
分支来开发,而不是master
分支。master
分支被用来作为线上环境的分支。而作为惯例,master
分支一般是默认分支,别的分支都来自master
或者是最后合并到master
。由于大部分工具自动的把master
作为默认分支,而且git默认显示master
分支,开发时不停切换分支是很麻烦的。
第二个问题是引入了hotfix
和release
分支,这些分支对于大部分开发者来说都是没有必要的。现在大部分机构使用持续交付,这意味着你的默认分支最后会被部署。hotfix
和release
分支应该避免他们自身引进的各种规范,例如合并到release
分支。尽管一些工具解决了这些问题,但是这同样使问题变得复杂。在期间,开发者很可能会犯错,例如修改的代码直接合并到了master
分支,却没有合并到develop
分支。这些问题的最根本原因是Git Flow
程对于大多数人来说太复杂了。
GitHub Flow
更简单的替代选择
Git Flow
的一个更简单的替换方案是GitHub Flow
。它只有一个feature
分支和一个master
分支,简单而干净,很多机构成功的使用这种方案。Atlassian推荐了一种类似的方案。合并代码到master分支并且发布,那么你应该尽量减小每次提交的代码量,坚持依赖和持续集成这些好的习惯。但是这种工作流中仍然有很多问题没有解决,例如部署、环境、发布和issue的管理。在GitLab Flow
中,我们使用其他的规则来解决提到的这些问题。
GitLab Flow
中的生产分支
GitHub Flow
认为你可以通过合并feature
分支直接把代码部署到线上。对于SaaS应用,这是可能的,但是在很多情况下并不是这样子的。一种情况是你无法控制准确的发布时间,例如IOS应用需要通过苹果的审核,或者是每天发布的时间是固定的,但是你merge的时间并不一定是那个时间。在这些例子中,你需要创建一个production
分支来放置发布的代码。你可以通过合并master
到production
分支来发布一个新的版本。假如你想知道线上的版本包含了哪些代码,只需要查看production
分支即可。而且大概的发版时间也可以从merge的信息中看到。假如你是自动部署production
分支的,那么这个时间就很准确。假如你想记录更精确的时间,可以通过写脚本来给每次部署打tag。
GitLab Flow
的环境分支
有一个环境可以自动升级到master
分支是一个好的选择。在这种情况下,环境的名字可能和分支名不同。假设你有一个staging
环境,一个preproduction
环境,和一个production
环境。在这种情况下master
部署在staging
环境。当有人想部署到preproduction
环境,他们会创建一个从master
分支到preproduction
分支的merge request
。而且上线代码通过合并preproduction
分支到production
分支。这种下行(downstream)的工作流保证了所有代码都在环境中测试过。如果你需要cherry-pick
一个hotfix
,常见的是在feature
分支中开发,然后发起merge request
合并到master
分支,先不要删除feature
分支。如果master
分支通过ci并且运行正常,就可以合并到其它分支。如果需要更多的手工测试,你可以发送合并请求从feature
分支到downstream
分支。一个environment分支的极端情况是为每一个feature分支建立一个环境,正如Teatro所做的。
GitLab Flow
的发布分支
在很少的情况下你需要使用release
分支对外界发布软件。在这种情况下,每个分支都会包含一个版本号(2-3-stable
, 2-4-stable
, 等等)。这种stable
分支都会从master
分支起始,并且要尽可能晚的创建。创建的越晚,你就会越可能的减少bug fix的合并。在release
分支发布后,只有严重的bug修复才加到release
分支中。如果可能的话,这种bug的修改首先应合并到master
,然后cherry pick
到release
分支。这样子,你就不会忘记把修复bug的代码cherry pick
到master
,从而防止其他分支也出这个bug。这被称为upstream first
上游优先策略,也被Google和Red Hat使用。每次release
分支修改bug之后,都要先设置tag,然后增加版本号(遵从语义化版本号)。有些项目也有stable
分支用来指向最新发行版本的commit,这种情况下一般没有production
分支(或Git Flow
的 master
分支)。
在GitLab Flow
中的Merge/pull requests
操作
merge
和pull
都在git中创建,然后指向一个人,由此人合并两个分支。像GitHub
和 Bitbucket
这样的工具会选择pull request
的名字,因为第一个手动的操作是pull
feature
分支。像 GitLab
和 Gitorious
这样的工具会选择merge request
的名字,因为对被指向的人来说这是最后一个操作。在这篇文章中,我们会称这个操作为merge request
。
假如你在一个feature
分支工作比较长时间,最好是向其他人实时的分享你的工作内容。具体操作为:进行merge request
而不指向任何人。这意味着你的代码并没有准备好做merge request
,但是欢迎大家来提意见。你的团队成员可以对这个merge request
的整体或指定行提出意见。merge requests
作为code review
工具来使用,并不需要Gerrit
和 reviewboard
。假如review时发现了一些问题或者bug,一般会由写代码的人来push一个fix commit。当新的commit push
到这个分支后,merge request
的代码也会自动更新。
当你觉得合适了,就把merge request
指定给熟悉你正在做的事的人,提醒他来查看代码给你反馈。如果别人觉得你的代码不太好,他可以直接关掉merge request
。
在GitLab中,保护长时间存在的分支是很常见的,这是为了防止其他开发者修改这个分支代码。因此假如你想把代码合并到一个受保护的分支,那么你可以把merge request
指定给对这个分支有master权限的人。
GitLab Flow
如何处理issue
GitLab Flow
可以使代码和issue之间的关系更加清晰。
代码的任何修改都应该开始于一个目标明确的issue。任何代码修改都应是有原因的,可以通知到团队中的每个人,并帮助开发人员保持feature
分支的范围足够小。在GitLab Flow
中,代码库的任何变更都源于问题追踪库中的一个issue。假如还没有issue,那么应该首先根据重大的任务创建Issue(超过1个小时的开发需求)。对于一些组织来说这是很自然的事,因为这些issue会用来评估冲刺(sprint)点。Issue的标题应该描述出最后想要的结果来,例如"作为一个管理员,我要删除用户,并且不发生错误",而不是"管理员不能删除用户"。
当你准备写代码时,首先为issue创建一个新分支,这个分支的名字应该以issue number开始,例如“15-require-a-password-to-change-it”
当你开发完毕或者想与人讨论代码的时候,就创建一个merge request
,可以在线讨论和review代码。创建一个merge request
需要手动进行,你并不是每次都需要把push的新分支代码合并,因为它很可能是一个长时间开发的分支或者是发布的分支。假如你创建merge request
没有指定给任何人,表明这是一个正在开发的分支,目的是为了讨论推荐的实现方式,但还没有准备好在master
分支包含此功能。小提示:这种merge request
的标题使用[WIP]
或 WIP
开头,避免还没准备好的功能被合并。
当作者认为代码已经准备好合并,就指定这个merge request
给reviewer
(代码评审员)。当reviewer
认为代码已经准备好包含在master
分支时,点击合并按钮,这样代码就合并了,并且生成了一个合并提交,之后可以很容易看到这个合并事件。merge request
总是提交一个合并提交,即使是空提交。这种合并策略在Git中被称为不使用
Fast-Forward
。在合并后feature
分支就被删除,不需要使用它了,这个删除分支操作在GitLab中是可选的。
假设分支合并后,但遇到了问题,Issue被重新打开。这种情况下,重用相同的分支名称是没问题的,因为它在合并时已经被删除了。在任何时候,每个Issue最多有一个分支。一个feature
分支可能会解决多个Issue。
使用Merge request
关联和关闭Issue
从commit的message中或者是merge request
的描述信息中可以关联相关的issue,具体语法是:fixes #14
,closes #67
等等。在GitLab中,这样的操作会在issue中创建一个评论,并且merge request
会显示相关联的issue。假如这个merge request
被接受,这个issue会自动被关闭。
假如你仅仅想关联这个问题,但是不关闭这个问题,你可以这样写:Ducktyping is preferred(普通注释信息). #12
假如你有一个issue关联着好几处的修改,最好的办法是为每一个修改都创建一个issue,最后把这些issue关联到一个issue。
使用rebase合并commits
Git中你可以使用一个交互式的rebase命令(rebase -i
)来把多个提交压缩成一个,并且重新排序它们。在GitLab EE 和 GitLab.com你也可以在Web界面使用rebase before merge。假如你在开发过程中对于一个小功能有多次提交,你想要把他们合并成一个提交,或者你想给提交排序以显得更加有逻辑性,那这个功能就很有用了。但是假如你已经把commits推到了远程分支,那么就不可以使用rebase了。如果别人拉取或cherry pick了你的commit,当你rebase你的commits时,一切就变的混乱了。如果人们已经review完了你的代码,当你rebase后,别人就没法知道你哪一块是新提交的代码了。另一个原因是,rebase会导致作者信息丢失,可能有人创建了一个merge request
,另一个人 push 提交到这里,第三个人合并了它,这种情况下,将所有提交合并,可规范引用和分享git blame 的代码片段,从而避开其他作者。
我们鼓励经常commit和push代码,这样别人就会知道你在做哪一部分工作。然而太多的commit,会导致提交历史很难被理解。但是总体来说,更多的commit优点还是更多一些。为了明白一处改变,我们可以查看merge commit的相关信息,这个commit把会很多小的commit分组了。
当你从feature
分支把很多commit合并到master分支,就很难会做回滚操作。假如你把多个commits压缩成了一个,那么只需回滚一个就可以了。但是千万要记着,如果commits已经push了,那么就不可以再有rebase操作了。幸运的是,git提供了回滚之前的一个合并的功能。然而这需要你指定需要回滚的merge commit。假如你回滚了一个merge,你又改变想法了,又想回滚回去,这个操作是git不允许的。
当你创建一个merge commit时都应该使用参数--no-ff
,这样你就可以回滚这次merge了。当你接受一个merge reques
时,Git管理软件总是会创建一个merge commit。
不要用rebase对commits进行排序
在feature
分支你可以使用rebase来把commits排到master后面,这样就会避免一个merge commit,创造一个清晰的线性历史。然而如果已经推到了一个远程库,那么就不要使用rebase了。如果你按照之前的建议经常地把commit提交共享给别人,那么rebase就会导致工作变得很混乱。当你使用rebase更新feature分支时,你需要一次次解决类似的冲突。你有时候需要reuse recorded resolutions (rerere),但是如果不适用rebase,你仅仅需要一次来解决这样的冲突。也有更好的办法来避免很多的merge commits。
避免很多的merge commits办法就是不要频繁的把master
分支合并到feature
分支。我们接下来要讨论在master
分支合并的三个原因:使用代码、解决merge冲突、长时间存在的分支。假如在你创建feature
分支之后,你需要从master
使用一些代码,可以使用cherry-pickin命令;
假如你的feature
分支有一个冲突,那么创建一个merge commit很正常。你应该尽量避免冲突的发生,例如使用gitattributes 以随机排序文件内容。一个例子是在GitLab中,我们的CHANGELOG文件在.gitattributes
中配置:CHANGELOG.md merge=union
,这样可以减少合并冲突。
最后一个创建merge commit的原因是有一个长期存在的分支,你需要与项目的最新状态保持一致。Martin Fowler,在他有关feature
分支的文章中,讲过Continuous Integration (CI)。在GitLab中我们把CI和分支测试混淆是不合适的。从Martin那里引用的话:“我听说大家在每个分支的每次提交的时候都做CI,因为他们要做build,可能是用一个CI服务器来做。那是持续的build,这是一件好事,但是没有集成,因此并不是CI”。避免很多merge commits的方法是,你的feature分支存在时间尽量短,大多数应该少于一天。假如你的分支存在多于一天了,那么就考虑工作分的更细一些或者使用feature toggles。
对于多于一天的分支,会有两种策略来解决。
- 在CI策略中,你可以在开始阶段就从
master
分支合并,可以避免中途的合并。 - 在同步策略中,你可以仅仅在规定好的时间来合并,例如一个tag。这种策略很被
Linus Torvalds提倡
,因为这些时间点的代码状态可以更容易被知道。
GitLab Enterprise Edition提供一种在合并前rebase的方式。你可以配置给每个项目,可在项目配置页面,勾选Merge Requests Rebase
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在接受merge request
之前,选择rebase before merge
。
GitLab将在合并之前,尝试干净的rebase。如果办不到,会使用常规合并。如果干净的rebase是可行的,目标分支的历史在这次合并后将更新。
总之,我们应该努力避免merge commit,但是不要限制他们。你的代码应该是干净的,但是你的历史应该准确的呈现出过去发生的事情。开发软件有时候会有一些混乱的操作,这些直接反映在历史中就可以了。你可以使用工具来查看commits的可视化图,以此来理解创建代码的乱七八糟的历史。假如你错误的使用rebase,那使用工具也没法纠正这个,因为工具也不能改变commit identifier。
给 Issue 和 Merge request
点赞
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NtD6LqTA-1594205680074)(…/reference/imgs/award_emoji.png)]
通过+1 或 -1 来表示认同或不认同。在GitLab中,您可以使用emojis给issue和merge request
点赞。
推送和删除分支
我们推荐大家经常的把feature分支push上去,即使还没有全部完成代码。这样做,可以防止别的成员去解决这个问题。在问题跟踪系统中,每个问题都会指定给一个人,也可以避免这种情况,可是有时候大家会忘记把问题指定给某个人。
当一个分支被合并后,这个分支应该被删除。在GitLab中当merge request
时,这个选项是可选的,这保证了在gitlab中看到的分支都是在处理的issue的,并且当在一个新分支中重新解决一个issue时,分支可以使用之前的名字。
经常commit 并正确的书写描述信息
我们推荐经常地commit,每次完成一个功能点,都应该commit一次。这样做的优点是当写新代码或重构旧代码出现问题时,很容易回滚会旧的版本。对于SVN的用户,这是一个很大的改变,当工作完成要共享给其他人时才会commit代码。有一个小技巧是,当你准备把代码提交给别人时进行pull/merge操作即可,中间不需要进行。
commit的描述信息应反映你的目的,而不是提交的内容。commit的描述信息是最容易被看到的,问题是为什么要这么做,一个好的commit描述示例:“合并模板以规范用户视图”。也有不好的例子,因为它们没有包含足够的信息,例如:改动、改进和重构。也应该避免使用fix
和fixes
这类词汇,除非与Issue数字放在一起。查看更多关于commit描述信息的内容,可以查看Tim Pope的博文。
在合并前测试
在旧的工作流中,Continuous Integration (CI)服务器通常运行master
分支的测试。开发者必须保证他们的代码不会把master
分支搞坏。
而使用GitLab Flow
,开发者从master
分支创建自己的分支,改完代码在merge request
之前必须测试完毕才可以合并代码。持续集成软件(Travis,GitLab CI)在merge request
时会显示build结果,这样可以保证测试通过。这样有一个缺点是由于只测试自己的feature
分支,没有测试合并后的代码。最好是有人可以测试合并后的代码,但是每次有人合并分支到master
后就需要测试一次,这样的测试代价是昂贵的,而且需要经常地等待测试结果。假如没有合并冲突,feature
分支合并到master
的风险是可以承受的。假如有合并冲突,你需要先把master代码合并到feature分支重新进行测试。
假如你的feature分支好多天也没有关闭,那么你需要使issue更小一些。
使用feature
分支工作
一般初始化一个feature
分支时总是从最新的master
分支拉取的代码。假如你之前就知道你的分支依赖别的代码,那么可考虑从别的分支拉取代码。假如你需要合并别的分支,那么需要在merge commit的信息中写清楚原因。假如你还没有把你的commit提交的远程库,那么可以使用rebase把你的commit合并在master或其他分支。
假如你的代码不合并upstream分支也可以正常工作,那么就不要合并。Linus说过“不应该随意的合并至upstream分支,应只合并主分支代码”。仅仅当需要的时候合并代码可以尽量的减少merge commit,这样可以使历史更清楚。