1、背景与难点
目前,前端平台探索大仓研发模式,通过Monorepo大仓的技术,整合前端平台现有应用的仓库代码,使得各业务域应用质量衡量标准统一,通用基础组件以及工具函数能够快速复用,当基础通用功能出现问题的时候,能快速地在各应用中升级,提升研发工作效率,节省人效。
我们知道在普通的项目开发中进行 git 的克隆和拉取不会遇到什么问题。但是随着我们代码的不断扩充,代码仓库内容会变得越来越大,需要几个G甚至几十上百G的磁盘空间时,如果把所有代码都pull到本地属实是个不现实的方式,不仅是我们没有这么大的磁盘空间,而且还有网络流量的占用问题以及网络速度问题都是没有办法解决。而且,如果Git仓库特别大,每次执行Git命令,等待时间会特别长。对于这些问题,我们做了相关的技术调研。
2、技术调研
我们调研了下Facebook和Google大仓代码按需拉取的实现,其使用方式大致如下:
mercurial:是一个分布式版本控制工具(类似Git,是Matt Mackall开发),采用的是基于内容寻址的技术。当对一个文件进行修改时,mercurial 不会直接修改文件本身,而是创建一个新版本,该版本包括指向之前版本的引用以及所做修改的差异。这样,就可以在没有改变之前版本的基础上构建新版本,同时正确地跟踪文件的变化历史。
Vscode工具:支持下载局部代码 & 全局代码检索等能力
- Piper:代码管理系统(类似github),是一个强大的分布式版本控制和数据管理系统,它使用了 Google 自行开发的 Colossus 文件系统、索引机制等多项技术来实现高效的管理和处理大规模的代码库,并具有高可扩展性、高可靠性和高吞吐量等特点。
- Citc: 云存储客户端,用来和piper进行交互
像Facebook和Google都是自研的类似Git的工具,如果我们自己自研的话,成本会很大。所以,目前是另辟蹊径选择了基于 git 的sparse checkout 来实现。Git在2.25及以上版本提供了sparse checkout的能力,能够实现代码的按需拉取。
3、实现原理
3.1 git sparse checkout的原理
3.1.1 sparse checkout定义
所谓稀疏检出就是,Git本地库检出时不检出全部,只将指定的文件从Git本地库检出到Git工作区,而其他未指定的文件则不予检出(即使这些文件存在于工作区,其修改也会被忽略)。
3.1.2 sparse checkout原理
当开启sparse checkout功能的时候, Git 从远程仓库下载整个仓库对象的元数据(metadata),而不是下载所有的文件。具体来说,Git下载仓库时,首先下载仓库的基础元数据对象(如提交对象、树和blob等),然后将基础数据对象整合成commit对象,并下载相关的历史记录。在这个过程中,Git会逐步下载和存储文件的有关信息(例如文件名、大小和内容哈希值),但并不会立即下载所有文件的内容。只有当执行检出命令时,Git才会根据指定的分支或标签,从远程仓库下载所需的文件的实际内容。
Git 实现这个稀疏检出,是靠一个skip-worktree的标识, 即在 index (即Git暂存区)中为每个文件提供一个名为 skip-worktree 的标志位,默认这个标志位处于关闭状态。如果开启该标志位,则无论Git工作区对应的文件存在是否,或者是否被修改,Git都认为Git工作区该文件的版本是最新的、无变化的。Git通过配置文件 .git/info/spare-checkout 定义一个要检查的目录和或文件列表,当前Git的基于合并(git merge、git checkout)等命令能够根据该配置文件更新的index中文件的 skip-worktree 表示位,实现Git本地库文件的稀疏检出。
- 执行 "git checkout" 命令来检出仅仅包含所选目录或文件路径的副本仓库,不包括筛选掉的文件或目录。
- 当执行 "git add" 命令时,会根据 sparse-checkout 文件进行文件过滤,只有匹配到正则表达式描述的文件才会被加入到 Git 的跟踪列表中。
3.2 基于sparse checkout的CLI实现
我们先来看下在 git 中手动使用sparse checkout 操作的一般步骤 :
第一步,git 初始化
git init
第二步,设置remote仓库地址
git remote add orgin git@pkg.xxx.com:du-monorepo/XXXXX.git
第三步,初始化
git sparse-checkout init —cone
第四步,添加目录
git sparse-checkout add xxx/xx ...
第五步,检出
git pull orgin master
为了更方便地使用 sparse checkout 特性,我们开发了命令行工具,集合封装了按需检出相关的操作步骤。
3.2.1 cli 操作步骤
- 命令行执行 dx init
- 选择业务域
- 选择项目
- 检出目标项目
这里面所做的就是把稀疏检出的操作流程集合在了统一的命令行里面了,便于操作。本质上还是差不多上文demo里面所描述的内容。
图片
上图是前端大仓目前已经迁移的全部应用目录结构,可以看到不同的业务域下有不同的应用,比如客服的研发只关注客服的应用,商家的研发只关注商家的应用,通过上面的CLI操作步骤只需拉取对应目录下的代码就可以了,流程效果如下:
3.2.2 实现流程
图片
如上图所示,整个cli 是基于Pipline 的设计模式,Pipeline模式为管道模式,也称为流水线模式。通过预先设定好的一系列的阶段来处理输入的数据,每个阶段的输出即是下一个阶段的输入(Pipeline其实是使用了责任链模式的思想)。模型图如下:
图片
Pipeline设计模式的精髓在于它的可配置化,并且嵌套可拓展。在使用Pipeline时,如果想调换Valve的顺序,或者某些业务是不是用某个Valve,都是可以在外部配置的。这样就可以很灵活地适配多样化的业务,针对不同的业务配置不同的处理流程,扩展性、灵活性比较强。
3.3 基于sparse checkout的VSCode插件实现
3.3.1 大仓VsCode插件组成要素
大仓VsCode插件由【启动按钮】、主侧边栏【HELP】以及【Monorepo管理面板】三个要素组成
插件组成要素
3.3.2 插件实现原理
下面介绍下这三个元素以及元素间联动的代码实现。
- 插件代码结构&基本架构
代码结构
插件基本架构
- 插件启动按钮
启动按钮是在package.json里配置的,配置项为contributes.viewsContainers.viewsContainers,可以配置按钮的id,和icon
图片
当点击启动按钮后,就会激活插件,并执行activate的钩子函数,activate需要在名为extension.ts的文件中实现并导出:
图片
当插件被销毁时,会调用extension.ts导出的deactivate钩子函数,在这个钩子里可以进行一些资源的销毁。
其中activate执行了打开【Monorepo管理面板】的代码,这样插件在启动时就会自动打开面板。
- 主侧边栏【HELP】
该元素也是在package.json中配置的,配置项为:contributes.viewsWelcome,可以在content配置项中配置内容,绑定视图,并为按钮的点击事件绑定响应指令,这里绑定的指令是自定义指令:monorepo-init-extend.startClone。
图片
在extension.ts中注册自定义指令:monorepo-init-extend.startClone,以及执行该指令的响应。
图片
可以看到该指令将创建并打开【Monorepo管理面板】,从而实现主侧边栏【HELP】和【Monorepo管理面板】之间的联动效果。当用户点击【HELP】中的【请选择应用】时,就会执行该指令并打开【Monorepo管理面板】。
- Monorepo管理面板
Monorepo管理面板是通过在VSCode中创建一个webviewPanel,并注入html模版来实现的。并且插件和webview之间可以通过postMessage api来进行通信。如在【Monorepo管理面板】中,当点击了【确定】按钮,就会通过postMessage将所选应用的信息通过postMessage发送给插件,插件将这些信息作为执行初始化或代码追加指令的参数,借助稀疏检出的命令行工具,执行相应的指令,即可按需拉取代码到本地。在插件执行完指令后,就会将相应的反馈信息通过postMessage发送给【Monorepo管理面板】。
Monorepo管理面板中树形结构的应用列表数据是通过前端统一配置中心获取的,支持动态可配置:
图片
4、技术挑战
基于Git的代码按需拉取虽然实现了,但是基于Git的文件系统是存在弊端的:
- Git 文件系统中的每个版本都是一组完整的快照,因此在执行稀疏操作时,用户需要指定需要的文件或目录,此时 Git 会进行文件或目录的部分检出。但是,由于 Git 文件系统中的快照是完整的,因此即使只检出部分文件或目录,Git 仍需要读取不相关的文件或目录,导致 I/O 操作和网络传输量增加,尤其对于大型代码库来说,这会增加服务器和网络的负担。
Git 仍需要读取不相关的文件或目录:这是因为 Git 文件系统使用的是内容寻址(content-addressable)的存储方式,即每个对象的名称都是由其内容(也就是文件的具体内容)计算出的 SHA-1 校验和。每个提交(commit)都是一个完整的目录树(tree)对象,其中包含了所有的文件和子目录。由此,在执行稀疏检出时,Git 需要读取整个目录树(包括不相关的文件和目录)以计算出其 SHA-1 校验和和对象名,然后根据用户的请求将需要的文件和目录进行检出。
- Git 文件系统中的历史记录是由一系列提交组成的,每个提交最多保存一个完整的快照。因此,如果对代码库进行了稀疏检出,从历史记录中检查或恢复文件或目录可能会变得更加困难,因为历史记录只能访问和操作现在存在的文件或目录,而不包括被检出的、或不再在代码库中的文件或目录。
由于 Git 文件系统是基于快照(snapshot)记录历史记录的,每个提交都包含整个代码库的目录树对象和其中所有文件的快照,那么如果使用稀疏检出机制来指定只检出部分文件或者目录,那么在检查或恢复历史版本的时候,只能访问和操作现在存在的文件或目录。因为那些之前被筛选过去的文件或目录现在不在当前检出的代码库中,所以无法直接访问它们的历史版本。
- sparse checkout是基于Git的元数据实现的,跟Git文件系统天然绑定。如果当代码量达到一定体量的时候,Git的元数据会非常庞大,特别是后续试行主干开发分支的时候,元数据会膨胀的很快,当达到Git本身性能临界点的时候,就会出现git相关操作卡顿的情况,如git add、git commit等相关命令执行会非常缓慢。所以维护好Git的元数据在一定的范围内非常重要,这也是我们后续在分支维护策略上比较大的技术挑战。
5、总结
本文主要对于git sparse checkout 的原理和在其之上的应用——大仓按需拉取cli和 vscode按需拉取插件展开讲解,实现了初版的基础能力,当然不可避免也存在着一些问题,当下的按需检出实现方案可能不是最终极的解决办法,但它却是最适合我们当下业务进程的方案。后面还会继续迭代和优化,同时关注git官方能力的改善以及我们自身对未来满足需求能力上的前置探索。