本文将简单讨论下我们在开发过程中尝试的各种分支管理策略,在面对各种复杂场景下呈现的优势与不足,以及我们的妥协和后续期望。 查看原文请点击这里
说到版本控制,就不得不提到分支管理策略。就像学开车必须学学交通规则。分支管理策略是代码版本控制的基础组成部分。为团队定制一套合适的分支管理策略,就好比制定了一套合理的交通规则,可以让团队的代码的更加有序地演进,尽可能降低多分支带来的复杂度,并避免由于分支混乱引发的各种“车祸”。本文将简单讨论下我们在开发过程中尝试的各种分支管理策略,在面对各种复杂场景下呈现的优势与不足,以及我们的妥协和后续期望。
Github-Flow
作为 Github 的重度用户,我首先考虑的当然是 Github-Flow 。
Github-Flow 是一种非常简单的分支管理方案。它的流程只有如下几步:
- 拉出一个新分支;
- 在新分支上进行修改,并提交和推送你的改动;
- 发起一个 Pull Request ,向代码管理员申请将你提交的分支合并到原来的分支;
- 讨论并接受 Code Review。在这个过程中,你依然可以继续推送新的代码到你的开发分支上,并且新的提交在推送后会出现在未完成合并的 Pull Request 页面中;
-
合并和发布。Review 通过后,代码管理员将该分支合并到原来的主分支上。
在 Gitlab 中同样可以使用 Github-Flow,唯一的区别是叫法从 「Pull Request」 变成了「Merge Request」 。下图是一个被成功合并的 Merge Request:
Github-Flow 有如下几个让人着迷的优点:
简单好操作。只有主分支和开发分支。不像 Git-Flow 那样需要引入一堆的辅助分支。
推动 Code Review 。通过 Pull Request 的方式,使得 Code Review 成为了日常开发的必经流程,有助于。
确保可编译。所有 Pull Request 都会触发持续集成测试,只有通过测试的才允许并入主分支。这就杜绝了代码编译不过的情况。
然而,面对复杂的项目,Github-Flow 暴露出了如下的不足:
-
解决冲突困难。多人协作的项目难免会出现冲突,一旦遇到冲突,Merge Request 就没法被直接被合并了。这个时候只能再从目标分支拉出一个分支→合并这个分支→解决完冲突→推上远程仓库再次发起 Merge Request 。对于大多数习惯了用 Merge Request 合并分支的人来说,这个过程一下子复杂了起来。
- Code Review 容易流于形式。虽然 Github-Flow 加入了 Code Review 的过程,但这依然取决于双方对待 Code Review 的积极性。如果没有充分的讨论代码的细节,仍然无法保证代码的质量。实际团队开发中,我们发现在线进行的 Code Review 并不如面对面讨论高效。更严重的是一旦双方没有 keep moving 的意识,大量 Merge Request 被积压,而这些 Merge Request 会不断包含新的 commit 进来,这就会使得 Merge Request 更加难以合并。
- 持续集成测试无法保证子模块可编译。持续集成可以作为 Merge Request 的准入条件,但这仅仅只是主工程的“福利”。而如上一篇文章所说,子模块的持续集成远比主工程复杂得多,难以直接在 Merge Request 页面中给出持续集成测试结果。而对于我们的项目,主工程基本没有多少业务代码,大部分的功能开发都是在子模块上,这就使得 Merge Request 的持续集成功能显得鸡肋了。
-
还有一些 Gitlab 的交互问题。在 Gitlab 中,默认的合并目标分支是 master 分支:
当你花上一分钟填完合并描述,选完指派人后,发现目标分支忘了改。此时就只能点击页面下方的 “Change Branches” 链接进入分支选择页重新选择分支。回来后你会发现你所填写的所有内容,包括指派人都被清空了:
- 不细心的人往往没注意到这个问题,于是提交了一个没有指派人的 Merge Request ,这带来的后果是这个 Merge Request 永远没人关注和合并。
综上所述,Github-Flow 更适用于那些只以 master 分支为主分支,更注重迅速发布的简单项目。这使其非常适合用在维护 Github 上的这些「集市型」的的开源项目,而不适用于「大教堂型」的企业级项目。正如 Github 的 Scott Chancon 大神所说:
For teams that have to do formal releases on a longer term interval (a few weeks to a few months between releases), and be able to do hot-fixes and maintenance branches and other things that arise from shipping so infrequently, git-flow makes sense and I would highly advocate it’s use.
For teams that have set up a culture of shipping, who push to production every day, who are constantly testing and deploying, I would advocate picking something simpler like GitHub Flow.
Scott Chancon, Issues with git-flow
附图 1 《大教堂与集市》这本书讨论了两种软件开发模式及背后的哲学。
Git-Flow
Git-Flow 是由 Vincent Driessen 在他的一篇文章 《A successful Git branching model》 中提出的分支管理策略。
与 Github-Flow 相比,Git-Flow 拥有更多的分支:
- master:可以提供给用户使用的正式版本;
- develop:用来生成代码的隔夜版本(nightly);
- feature:用于开发某个功能;
- hotfix:用于修复线上代码的 bug;
- release:用于正式发布版本前的测试分支。
Git-Flow 提出的分支管理策略完整而实用,它甚至已经成为了一个通用开发流程标准。开发者们可以在多个团队和项目中遵守同一套流程。但 Git-Flow 也不是万金油。遇到复杂的项目,它也未必能完全适用:
容易出现冲突。Git-Flow 设计了多个分支各司其职,但多分支带来的苦恼是容易出现冲突。最常见的问题是,由于我们实现了子模块 commit id 的自动更新,主分支与开发分支的子模块 commit id 经常变动,导致 develop 分支向 master 分支合并的时候出现大量冲突,阻塞发版进度。
-
多产品线的问题。
我们的主工程存在多条产品线:master 分支仅仅维护一个基础模板,而 jilin 、taishan 等分支才是用于产出真正产品的分支。每条产品线的各自有一套 Git-Flow 分支体系,并用前缀区分产品线。例如 jilin 的 develop 分支就叫 jilin-dev 。而子模块既可能和主工程一样多个产品分支,也可能是一个通用模块。对于通用模块,只需要维护一套 Git-Flow 分支体系。例如 common 子模块就只有标准的 master、dev 等分支。
对于多产品分支的主工程和子模块,当改动了某个分支的代码,你就要非常慎重的考虑这部分改动是否通用,是否需要并入其他产品线的分支。而 Git-Flow 并没有探讨多个产品线并存情况下的代码合并方案。
对于通用的子模块,拉 release 分支时又存在「锁」的问题。比如,负责 jilin 产品线的同事即将发版,于是把 common 子模块拉出了一个 release 分支。其他产品线的同事依然可以继续为 common 子模块的 develop 分支提交 feature 。但还没等 jilin 产品线完成发版。taishan 产品线的同事也准备发版了,此时 release 分支早已经被 jilin 的同事拉出来,而这个 release 分支却没有 taishan 产品线要发版需要的 feature 。这就阻碍了 taishan 产品线的发版。
妥协与期望
为了化繁为简,我们做了些妥协:
- 产品线取消 develop 分支。每条产品线取消 develop 分支,并放开产品线的主分支的提交权限。这种方案大幅减少了合并冲突的苦恼,避免发版受阻,而稳定性依然可以通过 feature 分支来保证。我们相信只要日后我们的模板足够完善,产品线的开发成本会越来越低,稳定性也会越来越强。
- 用 cherry-pick 来同步多条产品线的代码改动。对于通用的改动,可以使用 cherry-pick 来将改动同步到其他分支上。我们扩展了 fmanager 的功能,为其实现一个 cherry-pick 命令:
1 $ fmanager cherry-pick <commit id> <分支列表>
例如,假如希望把 weihai 分支上的一个提交同步到其他分支,可以使用如下命令:
1 $ fmanager cherry-pick 023e937d master,jilin,taishan
通用的子模块发版时,始终拉出产品 release 分支。
例如,jilin 产品线需要发版了,于是从 common 模块的 master_dev 拉出了 jilin-release 分支:
拉出分支后,与 jilin 分支有关的临时改动可以在 jilin-release 中进行。同时 common 模块依然可以给负责其他产品线的同事提交新 feature 。此时 taishan 产品线的同事如果要发版,可以拉出 taishan-release 分支:
之后,如果 jilin 产品线的同事修改了一个通用的 bug,同样可以将这个提交 cherry-pick 到其他分支:
其实,与其说是 Github-Flow 和 Git-Flow 的问题,不如说是现阶段我们的产品架构的问题。用分支来实现产品线的差异化使得一个仓库出现了多个主分支,而这种复杂的模式已经超出了通用的分支管理流程所能解决的范畴。另外,现阶段子模块的不稳定也导致开发过程中不断需要跨产品线同步代码,给产品线的开发造成负担。
日后我们希望对工程的架构进行调整,通过配置文件来实现产品差异化,而不再创建产品分支。另外,子模块也会越来越稳定,可以低成本接入到各个产品线中,而不再需要频繁迭代。到了那一天,我们的项目就能够重新回归到严格的 Git-Flow ,将 化繁为简 做到极致。