自从 Git 出现之后,分支管理就深入人心。但是随着我们团队在合并 master 分支时,开始优先采用 squash merge,事情还是有了变化。我也开始采用另一种不同于传统开发模式的分支合并方法。在此我简单撰文阐述一下。
这个模式还是争议很大的,文末我也列举了很多不适用的情况,还请读者不吝提出质疑。
首先,我们可能需要解释一下,为什么我们采用 squash merge 而不是传统的 merge 合并代码。直接原因很简单:为了保持 master 分支的纯净和简洁。Master 分支所应该表现的,就是我们因应各种明确的需求、优化、bug 修复所进行的代码变化。随着 master 版本的逐步前进,我们可以窥探到代码生长的过程。
在团队开发中,一般来说大家遵从的 Git 使用方法是这样子的:
但是,作为代码分支,其过程可简可繁,针对 master 分支,我们重点关注的是为了完成一个需求,代码做了哪些变更,但是在开发者开发过程中,这个分支不可能只有一个提交点,特别是发现了 bug 之后,肯定也会调整逻辑再 commit 一次。对于这些很快就发现并在上线之前就修复掉的 commits,我们并不需要它出现在 master 分支中。实际上我们的解决方法是:
Squash 合并模式,又称为 “压缩合并”,会将分支的所有提交点(commit)合并成一个,然后再合并到 master 分支上。当然,这种模式也是有前提的:
上面我只讲了 master 和个人需求分支的管理方式。但是在团队开发中,往往有很多开发者、很多需求,大家一起共用一个开发 / 测试环境,为了能够共用,那么各位开发者往往会再创建一个 develop
分支,大家把自己的开发进度,如果自测通过,那么就合并到 develop
分支上,这样以确保同一个环境都能够包含多人都包含的 feature。
然而,这种方法,其实也遇到了我们推崇 squash merge 时所要解决的同样的问题:我们是不是需要关注那么多的过程信息?显然,即便未合并到 master 分支,那么对于一个共用分支来说,也是不需要的。很大程度上,所谓的 develop
分支仅仅是一个用来包容所有已开发 / 测试代码的路径而已,网上推代码的开发者们压根就不关注其变更历史。为此,在第五人的开发过程中,我提出了另一种分支模式,我称为 rebase & squash
模式。
这个模式的具体操作方式,我们以下面的例子,对比传统模式来说明一下吧:
首先,我们手头有一个 master 分支。因为我们采取了 squash merge 的模式,因此这个分支非常纯净,就是一条单纯的曲线
假设张三和李四分别需要开发一个分支,那么他们自然就是从最新的 master 分支中 checkout 新的 feature 分支,进行开发
与此同时,还有其他同学也在开发分支,并且合并到了 master,因此大家的分支都在往后生长
到了某个时间节点,张三扯着嗓子吼一声:“开发环境我用一下噢!”这个时候李四也说:“我也要用,帮我发一下”。张三看到 master 分支已经发生变化了,于是张三从 master 分支 checkout 了一个 develop 分支,并且把自己和李四的代码分支都合并到分支上。
在 rebase & squash
模式下也是一样的,但不同的是,传统模式下,develop
分支创建之后,会一直存在于远端,继续合并;而 rebase & squash
模式下,这个分支只会临时地存在于远端,当完成了流水线的编译、发布之后,就将分支从远端删除。我们将这种临时分支表上虚线框以示区别。
张三合并了分支,发布到环境上调试。哎,发现有 bug,这就需要修改代码了。OK,张三在自己的 feature 分支上改了代码,然后再合并到 develop
分支:
在 rebase & squash
模式下,之前的临时分支已经删除了,那么只是重复一下上一次的操作,再创建临时 develop
分支,用完即弃就行了。
这个时候,两种模式下 develop
分支的复杂度差异已经可以看出端倪了。
这个时候,王五也加入来参与开发了,ta 自然是创建了一个新的分支。等到王五开发完毕,也准备用到统一环境的时候,王五也喊了一声:“开发环境有谁在用吗?”张三李四说:“合并到 develop 分支再发。”王五看了一下 develop
分支,因为李四的开发,develop
分支相比上一个小节,又往前了一个 commit;此外,还因为 master 分支往前也走了一步, 所以 master 分支的变动也被某位同学合并到了 develop 分支,以确保 develop 分支不能落后于线上版本。
不过这并不影响王五的操作方案,于是王五把自己的分支往 develop
一合,再提交了一个。
在 rebase & squash
模式下,还是老三样,直接从 master 拉出临时分支,然后王五扯嗓子喊一声:“开发环境谁在用?我要合哪些分支?”这个之后张三李四说:“我们建了一个共享文档,你就按照文档上的分支合就行。”于是王五把自己的分支和张三李四的分支都合并、编译、发布,然后删除临时分支。
在传统模式下,个人的分支,一旦被别人 merge 了,或者是 merge 到别的分支了,那么这个个人分支就不能乱动,不能轻易进行压缩、rebase 等破坏分支链一致性的操作,否则进行了修改之后的分支,会被视为新的分支。比如张三修了 bug 之后,觉得自己的分支太复杂了,好多叫做 “dev”、“bugfix” 的 commit message,哎合并一下算了。但是合并了之后,重新 merge 到 develop
分支的时候,张三发现多出来一条旁路了:
在 rebase & squash
模式下,个人的分支,被视为个人在工作层面的 “私有财产”,这里的 “私有” 指的是,分支的所有权属于个人,个人可以对这个分支作任何操作,如果觉得分支过于难看的话,完全可以自作主张进行合并,也可以为了跟上 master 分支的进度,进行 rebase 操作,这在这个模式下是极为常见的操作:
我们假设,张三完成了开发、测试,并且提交了 MR / PR。通过了之后,通过系统成功压缩合并到了 master 分支。那么在传统模式下,尽管张三删除了远端名为 feature/zhangsan
分支,但由于 Git 的分支特点,张三原来的那个开发分支的支架,依然存在于 develop
分支上:
那么基于 develop
应该不落后于 master 分支的原则, 需要再执行一次合并:
在 rebase & squash
模式下,事情就没那么复杂了,张三的分支合并到了 master,其他的分支你们随意,能保持最新的话,就 rebase,不急的话也没问题。张三的开发分支,也不会在其他的分支中留下痕迹。
这个时候,李四也要进行调试了,我们对比一下两种模式下,分支的变化:
传统模式:
对于一个成熟的开发团队而言,开发分支是动态的,不停滚动前进的。如果采用传统模式,很难遇到某一个时间点,develop 分支上的 MR 都合并入 master,从而可以删除并开启全新的分支。因此,develop
分支的混乱交叉,会在远端长时间地存在,并且实质上,这些分支的历史信息,一点意义都没有。
rebase & squash
模式:
采用这种模式,我们将关注的重点聚焦于各特性分支,而不是分支的合并上。分支也能够一直保持最新和整洁。
有经验的同学估计很快就能发现一个关键问题点:如果分支发生冲突了怎么办?
传统模式下,分支的冲突处理可以轻易地通过 merge 来解决,但是在 rebase & squash
模式下,rebase 天生就带来重复解决冲突的副作用。
首先最理想的情况是,冲突点尽快合入 master 分支,然后相关的分支重新 rebase master。但实际操作中,冲突点可能无法快速解决,这个时候,这个模式也是有解决方法的。
我们回到张三李四的场景。我们假设发生了冲突:
首先,解决冲突的时候,冲突的当事双方必然需要进行协商,然后选定解决冲突的方案,这即便是传统的分支模式也不例外。此时,我们应该找到冲突点,然后基于冲突点,执行 merge 并解决冲突,生成一个基准分支
然后,将这个基准分支,基于 master 进行 rebase 和 squash 操作,合并为一个提交点(或者想要保留之前的 commit,其实也行):
然后,相关当事人基于这个新的基准分支,将自己的分支进行 rebase 操作:
有了基准分支之后,当事分支将自己的基准分支改为这个新的基准分支。基准分支也可以随时跟着 rebase master。基准分支发生变化,特性分支也可以选择跟随,也可以选择不跟随,其实影响不大。
而如果引入了新的开发者王五的时候,王五并没有什么冲突的压力,ta 依然可以按照原本的模式,正常从 master 分支拉出来干活,并且合并别人的分支:
基准分支是为了解决冲突而产生的,不希望长期存在。当由基准分支派生出来的任意一个特性分支通过了 MR / PR,并且压缩合并入 master 的时候,我们就可以考虑基准分支的删除操作了。这个删除可以由刚刚合入代码的开发同学来负责。我们先看看分支合并以后的状态(灰色分支表示不存在了):
这个时候,另一个特性分支,只需要 rebase master,就可以回归了,毕竟冲突点早就合入 master 了,世界重归宁静。
在食品领域,抛开剂量谈毒性就是耍流氓;在软件工程,抛开场景谈应用也是耍流氓。我提的这种模式,并不是要革传统分支模式的命,在统一 squash merge 模式的大背景下,两种模式最终的走向都是保持主干分支的简洁可读,同时解决团队开发的问题。
就我个人的实践经验来说,rebase & squash
模式比较适合以下场景:
反过来,我们也可以列举出不适合这种分支模式的情况:
不过其实,我觉得两种模式并不是你死我活的关系。即便是针对传统模式或者是其他的分支管理模式,本文提出的这种模式也是可以在子模块的小团队中采用的。比如说,小团队开发阶段采用 rebase & squash,但最终合并到大项目的关键分支中则采用项目模式。
虽然我提出了这种模式,不过我个人是从未强制其他人 follow。原因嘛其实也挺明显,且不说这个模式解决冲突时的麻烦,据我了解大部份开发者并不特别在意保持与 master 分支的更新,而主要关注自己开发中的分支。
这个模式的个人喜好特征非常明显。我个人就一直使用这种混合模式对自己的代码进行管理,同时参照团队模式进行合并。其实本质上,就是如何选取基准分支的问题——master
分支也可以是相对的,在不同的场景下,我们开发中可以视另一个分支为我们的基准分支,那么 rebase 其实也就是另一种 squash merge 而已。
另外一种场景是推荐给 scrum master 同学,特别是需要清晰掌握各开发分支状态的 scrum master。这种模式可以非常方便 scrum master 清晰掌握项目中各分支的进展。但是在这种模式下,scrum master 也应该承担起分支合并冲突处理以及基准分支的管理职责。
Anyway,这是我个人在开发过程中摸索出的一种新玩法,分享出来,是请开发者批判性地学习这种模式啦。
本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
原作者: amc,欢迎转载,但请注明出处。
原文标题:《一种邪道的 Git 整洁之法——rebase & squash》
发布日期:2024-11-25
原文链接:https://cloud.tencent.com/developer/article/2470889。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。