git对我来说挺难理解的,平时遇到问题也是绕着走,倒也没啥大问题,但基于git软件的工作流却很重要,尤其对于一个组织来说。
git工作流、github工作流、gitlab工作流都属于特性分支(feature branches)的类别,今天主要理解gitlab工作流,它结合了特性驱动开发、特性分支、issuse跟踪。
1:git工作流的问题
git工作流比较古老,最大的问题是太复杂,它包含master、develop分支,还包含了features、releases、hotfixes分支。
它从develop分支开始,然后移动到release分支,最终合并到master分支。
它有两个最大的问题:第一个问题就是必须从develop分支开始(而不是master分支,master is reserved for code that is released to production),这有点反人类,约定俗成大部分工具都是从master分支开始的。
第二个问题就是它引入了hotfixes和release分支,现在大部分组织都是基于master做CD(即master分支是可以直接部署的),而对于CD持续交付来说,是没有hotfixes和release分支概念的,也不建议引入一些规范(比如将代码合并回release分支),另外也会经常犯错(比如把代码合并到master,但没有合并到develop分支)。
总之git工作流太复杂了,其实我也没理解。
2:github工作流
非常简单,就两个分支(master和features),要做的就是将features发送PR到master,提倡频繁部署,减少未发布的代码,良好践行精益开发和持续集成这些最佳实践。
什么意思呢?就是鼓励你尽可能的合并到master(代表可以部署了),这个工作流在github上没有问题,但对于一个组织来说,它还有很多问题没有解决,比如说部署、多环境、发布、issues这些问题(部署和发布不是一个概念,部署针对于代码部署,发布针对于用户)。
那如何解决呢?gitlab来了,它不仅仅是一个git管理工具,更包含一整套的工作流方法。
3:gitlab工作流之生产分支
gitlab工作流有三个变种,先说生产分支。
github工作流假设一旦你将feature分支合并到master后就可以部署了,但现实并不是如此,因为各种原因并不能精准控制release时间,比如说IOS审核,你将代码合并到master的时候其实整个服务还没有release;再比如release时间是固定的,但merge时间可能并不是release时间点。
那怎么保障merge时刻的代码就是真正要发布的代码呢?同时又不影响持续集成,其实merge后,可以将master分支合并到生产分支。
通过这样的工作流,如果想看线上代码是什么,可以直接查看生产分支;如果想精确知晓release时间,还可以基于生产分支打一个tag。
有一个问题,master是可以持续集成了,生产分支也打出来了,但还没到发布时间,这时候突然有个bug,开发人员基于master创建出一个特性分支(其实已经包含了未发布的代码),修复后要紧急上线,怎么办?
第一可以直接将master合并到生产分支发布修复代码;第二可以pick出修复的代码(特性分支)到生产分支,
必须记住,代码在master上就表示可以发布了(也许要做特性开关)。
4:gitlab工作流之环境分支
在真实的世界,每个分支对应于基础设施的环境(环境和分支的名称不一样),比如master分支对应于staging环境,pre-production分支对应于仿真环境,production分支对应于线上环境。
如果想在仿真测试,将master合并到pre-production分支;仿真测试没问题后,再将pre-production分支合并到production分支上。
这种基于下行的工作流(This workflow, where commits only flow downstream)确保每个环境的代码都是经过测试的。
假如要修复一个bug,cherry-pick一个hotfix提交,通常的做法就是(从master还是production)创建一个feature分支,然后合并到master,此时先不要删除feature分支,测试通过后,再将master合并到其它分支(当然也可以提交一个MR到其他任何的downstream分支)。
我们的工作流有点类似环境分支,因为要确保master分支是真正意义上可部署的,但开发环境的代码只是开发人员自己测试,没有把握直接merge到master,所以在master分支前还有一个qa分支,qa分支由测试人员测试,有几点变化:
qa环境测试通过基本代表可发布
其他特性分支统一合并到qa做测试(而非master)
并不是将qa分支合并到master分支,而是qa测试通过后,将特性分支合并到master分支,master分支和qa分支是隔离的(这可能会有问题)。
同理到production分支,不是master分支合并到production分支,也不是qa分支合并到production分支,而是基于特性分支合并到production分支。
对于共享的特性分支来说,尽量减少提交到远端,或者动不动就合并到qa,这是我们现在比较大的一个问题(开发和测试都在qa分支上)。
以上还需要理解的更透彻一些。
5:gitlab工作流之发布分支
这种工作流可能在互联网公司并不常见,这种场景下,每个分支包含一个版本号(比如2-3-stable, 2-4-stable)。
这些分支基于master,而且尽可能晚一点merge到发布分支,因为减少了bug修复的merge。一般情况下,只有非常严重的bug才会再发布一个版本,具体的做法是先合并到master,然后再cherry-pick到发布分支,这样在后续的版本中就不会遇到相同的bug了,先merge到master再pick到release这种做法叫做upstream first策略,google和red hat就是这么做的。
在release分支中修复一个bug后,也会打一个tag并增加版本号。
6:Merge/pull requests
这两个词是由git管理应用程序(比如gitlab和github)创建的,github上叫做pull request(因为第一个步骤是pull特性分支),gitlab叫做merge request(因为最后一个动作是合并特性分支)。
在特性分支工作了几个小时,为了分享工作成果,可以创建一个MR给任何人(也可以@某个人),这个动作表示该请求并不是为了merge(标题以[WIP]开头),而是希望得到反馈或code review。
团队成员能够对MR进行评论,如果发现问题,任何人(一般是MR发起者)发送一个fix push,这个MR会立刻更新。
如果准备将这个特性分支合并到master,一般将这个MR发给具有一定权限的人,他可以选择merge或者直接关闭MR。
在gitlab中,一般会保护长期存在的分支(比如master),所以开发人员一般不会直接修改该分支,只有特定权限的人才能merge到master分支。
合并完成后,一般会删除特性分支,确保gitlab上的分支大部分都是处于工作状态的,另外再开一个相同名的分支也不会出现问题。
7:Issue跟踪
gitlab工作流可以让issue和代码之间的关系更加透明。
任何的代码修改都来源于一个issue(可能是bug,也可能是需求),并尽量让特性分支范围小一点。
在写代码的时候,根据issue创建一个分支(名字和issue编号有关,比如15-require-a-password-to-change-it),解决后发送MR,merge成功一般会产生一个合并提交(不产生Fast-Forward)。
8:从MR中link或关闭 issue
发送MR的时候,“Fixes #16” ,一方面表示关联issue,另外合并成功过后会自动关闭该issue。
9:通过rebase压缩commit
在git中,能够使用交互式的rebase(rebase -i)将多个请求合并为一个(代表完成一个功能)或重新排序,这个功能很有用。
但如果你的提交已经提交到远程仓库(相同的分支还有其他开发者),则必须禁止rebase,因为rebase会产生新的commit(就是commit id会变化),从而导致合并冲突,因为相同的变化有不同的commit id;也会导致合并错误,因为对于工作在相同分支上的人来说,他们的git历史和你的提交并不匹配。
如果rebase已经同步到远端的分支,对于作者和其他合作者都会很麻烦,一些人已经review过代码了,但rebase会让人很难知道上次review后发生了什么。
由于我们现在很多人在一个分支上开发,会经常遇到同一个分支merge的问题,建议不要使用rebase -i或rebase合并(需要进一步理解),而使用merge(虽然会导致git历史不太好看)。
如果合并的时候有很多提交,恢复的时候比较难,可以通过gitlab的Squash-and-Merge功能,就是在合并的时候压缩为一个提交。另外还有一个简单办法可以撤销(revert)所有的提交,就是总是使用“no fast-forward” (—no-ff) 策略。
这个工作流在工作中很常见,需要仔细体会。
10:减少在特性分支上进行merge操作
如果一个分支上有很多merge提交,会让git历史记录很混乱,所以应该尽量避免在特性分支上进行merge操作。
通常在master分支上如果有新的提交,建议通过rebase重新排序或合并commit,从而避免merge操作(Often, people avoid merge commits by just using rebase to reorder their commits after the commits on the master branch)。题外音,很少会在master上提交。
在特性分支上,如果需要同步master的操作,应该使用rebase master,尽量避免merge master,从而保持一个线性的提交。当然上面也说过了,如果你的分支已经在远端和人分享了,应该避免进行rebase操作。
rebase操作会产生很多的工作,每次rebase的时候,会处理相同的冲突,而merge更合适,解决冲突只需要一次。
听了那么多,在本地开发的时候建议rebase -i,在master的时候可以rebase,其他场景建议少用。
接下去回答为啥应该减少在特性分支上进行merge操作。一般情况下进行在特性分支上进行merge有三个原因。
(1)utilizing 新代码,假如你想使用master上的一些新代码(特性分支创建后产生的提交),可以使用cherry-picking一个commit。
(2)解决合并冲突,如果一个特性分支有很多开发者,在更新代码的时候比如会遇到冲突,所以合并也是合理的。
(3)同步master上的代码
为了保证特性分支上的代码较新(short-lived),有的时候会合并master上的代码(有时候我经常这么做),但其实应该减少这样的行为,大部分特性分支应该小于一天的工作量(并不现实,至少我没见过),如果花费很长时间,建议将任务拆分的更小。
对于多余一天工作量的分支,有两种策略保持代码较新:
(1)将代码merge到master做CI,CI/CD提倡自动化测试,其实目前我们做不到,是不太敢直接提交到master分支,这种观念很难扭转过来。
(2)Another option is to only merge in from well-defined points in time, for example, a tagged release(没理解)。
另外合并到master就代表引入了新功能,代表可以部署了,
有的时候特性分支经常性合并到master做CI,虽然测试没问题,但可能功能还没完成或不想暴露出来,此时必须使用feature toggles隐藏未完成的测试。
总之,特性分支应该尽量减少合并提交,但不要消除它们。codebase应该保持干净,但也要记录实际发生的情况(历史记录很重要)。
这个工作流在工作中很常见,需要仔细体会。
11:commit信息应该有意义
commit不仅仅是提交代码,还要体现出意图,所以少用fix,improve这样的字眼。
12:合并之前要测试
一般情况下,特性分支会做CI持续集成,测试通过才会发送MR,这个是没有问题的,但有个问题,只测试特性分支而没有测试merge后的代码。
也就是说merge后还要再测试一次,看上去很浪费时间。
但其实如果合并没有冲突,特性分支合并到master的风险是可控的,如果有冲突,应该将master代码merge到特性分支重新进行测试,测试通过后,再merge到master。
13:在特性分支上进行工作
一般情况下,初始化一个feature分支时总是从最新的master分支(upstream分支)拉取的代码。
假如已经知道你的分支依赖别的分支,则可从该依赖分支拉取代码。
如果特性分支需要合并到别的分支,那么需要在merge commit的信息中写清楚原因。
如果还没有把特性分支的commit提交的远程库,那么可以rebase master或其他分支(这样历史信息更有用)。
如果代码正常工作且不需要合并,那么就不要再一次merge upstream分支,Merging only when needed prevents creating merge commits in your feature branch that later end up littering the master history.
参考:https://docs.gitlab.com/ee/topics/gitlab_flow.html