简介
Monorepos
Monorepos 是一种软件开发实践,其中多个项目(通常是相关或依赖的项目)存储在同一个版本控制库中,而不是分散在多个独立的仓库中。这种方法可以在大型组织中使用,以便更好地管理代码共享、依赖关系和版本控制。
Git Submodule
Git Submodule 是 Git 中的一种机制,允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。每个子模块都有自己的版本控制历史记录。
优缺
Monorepos
优点:
- 统一管理:所有代码在同一个仓库中,便于统一管理和查看。
- 代码共享和重用:共享代码变得简单,不需要设置额外的依赖管理。
- 一致性:可以确保所有模块使用相同的工具和依赖版本,减少版本冲突。
- 原子提交:可以在一次提交中同时更新多个模块,保证变更的一致性。
- 简化 CI/CD:构建和测试可以在同一个环境中进行,减少配置复杂性。
缺点:
- 规模庞大:随着项目的增长,代码库可能变得非常庞大,克隆和操作速度会变慢。
- 安全性:如果你想限制对某些“包”的访问,这几乎是不可能的。
- 工具支持:需要一些额外的工具或脚本来管理和构建大型 monorepo。
- 历史记录失去作用: 例如,如果你在 monorepo 中有后端 + 前端,git 历史记录会变的一团糟。
- 复杂 CI/CD: 当场景为针对不同的包实现不同的CI/CD时,相较于构建多个单一的CI/CD脚本会更加复杂。
Git Submodule
优点:
- 模块化:每个子模块都是独立的 Git 仓库,可以独立开发和管理。
- 权限管理:可以为不同的子模块设置不同的访问权限。
- 灵活性:可以在不同的项目中复用同一个子模块。
缺点:
- 复杂性:管理子模块的版本和依赖关系比较复杂,需要额外的命令和配置。
- 同步问题:子模块的更新需要手动同步,容易出现版本不一致的问题。
Git Submodule 与 monorepos 的比较
- Git 子模块使主存储库的提交历史记录清晰可见。由于子模块是单独的存储库,因此它们不会向主存储库添加任何提交历史记录。这使得跟踪主存储库和软件包中的更改变得容易,从而更易于管理。
- 与 monorepos 相比,Git 子模块更安全。可以将对子模块的访问限制为特定用户或团队。这使得管理对子模块的访问变得更加容易,并确保只有授权用户才能访问它们,或者只有授权用户才能将更改推送到子模块。
- 对于使用不同语言的应用程序,lerna、yarn 工作区等工具并不合适。这些工具旨在使用单一语言,不适合使用多种语言的应用程序。Git 子模块更适合此类应用程序,因为它们与所使用的语言无关。
案例
Monorepos:这里一个得物的大仓权限设计和实践
这里仅简要展示主体过程、详情请看原文链接、
分支模型的定义
- Apps:各业务域的目录结
- _Share:业务域下通用依赖目录
- Abroad-Crm-Micro:具体应用名
- …:后续新增的应用都在业务域目录下
- Components:业务域下通用组件目录(初始化固定目录)
- **…**:可以自定义扩展目录
- Global:国际业务域应用目录
- …:后续新增的业务域目录都在 App 目录下
- Packages:前端平台通用组件、工具函数、配置文件、Hooks 依赖
- Components:平台通用组件目录(初始化固定目录)
- Hooks:平台通用 Hooks 目录(初始化固定目录)
- …:可以自定义扩展目录
角色权限的分配
角色权限没有另辟蹊径,还是沿用 Gitlab 已有的权限配置:Owner、Maintainer 和 Developer。
这里需要考虑的是只要开发者具备 Developer 权限,那么他就可以修改大仓任何目录下的代码,并且本地可以提交,这样会导致本地源码依赖出现很大的风险:会出现本地代码构建和生产环境构建不一致的情况,在研发流程意识不强的情况下很容易引发线上问题。 本着对代码共享的原则,对于代码文件读权限不做控制,也允许研发修改代码,但是对修改的代码的发布会做流程上的强管控。这里就会涉及到 Gitlab 的分支保护机制以及文件 Owner 权限配置。
文件目录权限
当有对应的文件或者目录路径下的文件变更的时候,在 CodeReview 过程中必须由对应的 Owner 成员确认无误之后,才可以 MR 代码。 比如:
- .husky/ 表示 .husky 目录下的文件变更,必须由具体的文件 Owner 评审通过才可以 MR;
- Apps/XXX/crm/ 表示 Apps/XXX/crm 目录下的文件变更,必须由对应的文件 Owner 其中之一审批通过才可以 MR。
通过 GitLab 提供的文件目录权限配置,即使研发可以修改任意目录下的文件代码,但是最终在 CodeReview 的流程中,需要对应的文件 Owner 进行确认评审,这样就避免了研发在不注意的情况下,提交了原本不该变更的文件的代码,同时也避免了线上问题的发生。
研发流程的权限控制
保护分支
在大仓研发模式下,主要有四类分支,其命名规范如下:
- Dev 分支命名规范:feature-[应用标识]-版本号-自定义
- 测试分支命名规范:test-[应用标识]-版本号
- 发布分支命名规范:release-[应用标识]-版本号
- 热修复分支命名规范:hotfix-[应用标识]-版本号
其中 Feature 分支为开发分支,由 Developer 创建和维护;Release 和 Hotfix 分支为保护分支,Developer 和 Maintainer 都可以创建,但是 Developer 角色没有权限直接将 Feature 分支合入 Release 或者 Hotfix 分支,只能由 Maintainer 角色来维护。基于目前不同业务域会经常创建 test 分支用于不同测试环境的部署,这里 test 分支并未设置为保护分支。当然 Matser 分支也是保护分支,只有 Owner 角色才有权限直接将分支代码合并到主干分支。
通过对不同类型的分支的定义,基于 GitLab 提供的保护分支能力,避免了研发本地合并代码的情况,使得 Feature 分支的代码必须走研发流程的 MR&CodeReview 流程,才能最终合入代码。
钩子函数
通过保护分支的约束,避免了本地直接合发布分支带来的风险,但是在本地代码提交的过程中,如果不做权限的校验,就会在 CodeReview 流程中出现文件 Owner 权限不足的情况,为了在代码提交阶段就能识别到非变更文件的提交,这里基于 Git 的钩子函数,做了权限校验,其流程如下:
通过 Git Hooks 提供的 Pre-Commit 和 Pre-Push 两个节点做权限校验,防止出错。Pre-Commit 不是必须的,如果影响代码提交的效率,可以跳过这个步骤,Pre-Push 是必须的,不允许非 Owner 做本地发布。
当然这里也会带来一个问题:当迭代的 Release 分支落后于 Master 分支,此时基于 Master 分支创建的 Feature 分支就会和 Release 分支代码不一致,导致出现很多非必要的变更文件,此时研发会很疑惑为什么会出现没有修改过的变更文件。这个问题在大仓研发模式下是无法避免的,通过分析之后,在本地提交阶段,过滤了 Apps 目录的校验,只保留了大仓顶层部分核心文件的权限校验,因为大部分的变更都在业务域下的应用里面,顶层的文件很少会去修改。
MR&CodeReview
通过保护分支的约束以及钩子函数对部分核心文件的校验,减少了很多在 MR&CodeReview 中本该遇到的问题。基于文件 Owner 权限的 MR 和 CodeReview 流程:Commit 阶段 -> Push 阶段 -> 创建 MR -> CodeReview -> 执行 MR,每个阶段的流程如下:
- Commit 阶段通过对核心文件的 Owner 校验,避免核心文件被乱改的情况;
- CodeReview 阶段通过文件 Owner 权限的校验,确保非本身业务域被修改之后被其他业务域的 Owner 知悉。
这里会带来一个问题:当 Release 分支回合 Master 代码的时候,会创建临时 MR,这个过程也会有文件 Owner 权限的校验(比如客服同学同步代码的时候,也会将商家和供应链的代码一起同步过来),就需要其他业务域的文件 Owner CR 通过才行,但 Master 的代码实际已经是 CR 过的,没有必要重复 CR,并且同步频繁的时候,会经常 CR 确认,导致回合代码的效率非常低。这里给效率技术那边提了需求,在 Release 分支回合 Master 代码的时候,不做文件 Owner 的校验。
通过上面对研发流程中的权限控制,避免了出现本地代码构建和生产环境构建不一致的情况,确保了提交代码的质量和稳定性。
总结
Monorepos带来了代码统一管理共享和重用、以及保证了一致性的同时也带来了非常多的问题。比如代码规模过于庞大带来的仓库操作体验问题。文件的安全性问题。以及大仓的开发流程管理问题。得物的核心解决思路就是通过大量的约定和规范来进行约束开发过程,以保证项目的正常推进。
Git Submodule 图表编辑器子项目管理
背景
图表编辑器是一个编辑图表的低代码平台,通过画布和配置表单搭配的形式对图表的样式进行编辑,最终输出图表的配置项和代码。大屏编辑器是配置和编辑大屏的低代码平台。大屏编辑器中包含图表编辑器中的图表编辑功能,并不包含其输出配置项和代码的功能。
这里图表配置和编辑的功能需要在大屏编辑器和图表编辑器上保持一致。在画布、出码等其余功能上,是相互独立的,互不影响。
图表编辑器采用了 Git Submodule 的方式来管理子项目。大屏编辑器采用NPM
的方式来直接引用子包。
子项目结构
LCharts
: 图表引擎。chartsConfig
: 图表配置表单。chartsPackage
: 默认资源包。xgChartDataUpdater
: 图表数据更新器(行列数据)xgChartPropsUpdater
: 图表样式属性更新器
子项目规范
子项目规范分支分为develop
、feature
、master
分支。
feature
从develop
切出,开发具体功能后合回develop
。
develop
功能完备后,合回master
,并打tag。
develop
分支仅用于开发,并不负责维护版本号和版本日志。
开发流程
初始化项目
构建了一个脚本从CI文件中截取了部分代码来初始化项目
1 | - git submodule deinit --force . |
开发/提交子项目
- 进入子项目目录
- checkout -b feature/xxx
- 开发/提交代码
- 发起 merge request feature/xxx 到 develop
- CodeReview
- 合并代码
- 进入主项目目录 提交子项目的
Hash
值
发版流程与包分发
- 进入子项目目录
- merge develop to master
- 更新 版本号
- 更新
changelog
- 提交版本 打
git tag
- push 代码
- publish 包
- 第三方应用更新包版本。
总结
Git Submodule
和包分发组合的方式,带了一些优点,比如相对友好的开发体验、相对清晰的提交日志、版本管理、 当然也带来了一些问题。比如并不能严格的保证一致性,图表编辑器先开发之后发版子包,第三方应用再更新包版本存在一定的滞后性。提交记录不够原子化,先提交子项目、CodeReview
通过后合入子项目develop
分支,需要再次再提交子项目的Git Hash
到主项目,单个功能并不能一次提交。
参考 & 引用
https://www.aviator.co/blog/managing-repositories-with-git-submodules/
https://dev.to/davidarmendariz/git-submodules-vs-monorepos-14h8