「译介」通过稀疏检出控制单一代码库

2021-03-31, 星期三, 16:40

译介

本文发表于 2020 年 1 月 17 日,2021 年 3 月 31 日有更新,原文见 Bring your monorepo down to size with sparse-checkout。作者 Derrick Stolee 从 2017 年开始为 Git 项目贡献代码,主要专注于性能方面。成果包括 git log --graph 速度优化和针对巨型代码库执行 git push 命令的速度优化。限于译者水平,本文并未逐字逐句翻译,有些位置亦有段落内容的重排和删减,有难以理解之处还请阅读原文。

代码目录不断膨胀失去控制,checkoutstatus 命令运行迟缓。当只是需要修改代码中的一小部分时,这些问题尤其令人头疼。稀疏检出特性将允许开发者专注于一小部分文件,特别是在处理“有大量微服务的单一代码库(monorepo)”时,即不会破坏单一代码库模式的优点,又能保持工作流程简单快速。Git 2.25.0 引入了实验性的 git sparse-checkout 命令,改进了现有特性的易用性,有助于提升加载大型项目的性能。

稀疏检出功能已在 git 1.7 实装,启用需要设置 core.sparseCheckout.git/info/sparse-checkout 等内容。git 2.25.0 之后这些工作可以通过 git sparse-checkout 命令完成。

monorepo 的主旨就是将所有代码存储在同一个版本控制(VCS)代码库中。另一种选择是把服务、应用、库的代码存储在不同的版本控制库中,在 Git 的思路下可以使用 subtree 和 submodule 组合。

已存在的仓库启用稀疏检出

执行如下命令

$ git sparse-checkout init --cone
$ git sparse-checkout set <dir1> <dir2> ...

如果给你整懵了,还可以用 git sparse-checkout disable 恢复原样。

init 子命令对 git 进行了必要的配置,并在 .git/info/sparse-checkout 配置了基本的模式:“仅匹配根目录下的文件”。

set 子命令向 .git/info/sparse-checkout 添加模式。注意紧接在指定目录的父目录中的所有文件也会被匹配。例如对 git sparse-checkout set A/B 而言,A/B/C.txt 可以匹配(A/B 的直接子代),A/D.txt 可以匹配( A/B 的直接同级),E.txt 也可以(A 的直接同级)。模式的语法同 .gitignore 类似。

单一代码库中的微服务示例

作者创建了一个示例仓库 derrickstolee/sparse-checkout-example,模拟了一个照片存储和分享的应用程序。仓库保存了许多微服务以及三种客户端的代码,其结构如下:

client/
    android/
    electron/
    iOS/
service/
    common/
    identity/
    list/
    photos/
web/
    browser/
    editor/
    friends/
boostrap.sh
LICENSE.md
README.md

3261af440cf85bfc0008e76e0022dcee.png

如此多互不相关的代码堆放在一个仓库中,使用者并不需要详细了解每一个目录,然而在 fetchmerge 时这些文件又都同样重要。

$ ls
bootstrap.sh*  client/  LICENSE.md  README.md  service/  web/
$ find . -type f | wc -l
1590

事实上这都算少了,想象一下在文件数后面加 3 个 0 吧。

执行 git sparse-checkout init --cone 让工作目录里只留有根目录下的文件(以及 .git 目录)。

$ git sparse-checkout init --cone
$ ls
bootstrap.sh*  LICENSE.md  README.md

$ find . -type f | wc -l
37

eaf6e8d53a894d3168084f1882c6a262.png

Android 客户端开发团队只需要 client/android 下的内容。

$ git sparse-checkout set client/android

$ ls
bootstrap.sh*  client/  LICENSE.md  README.md

$ ls client/
android/

$ find . -type f | wc -l
62

项目管理员可以在根目录下创建一个 boostrap.sh 脚本,引导不同团队使用更复杂的文件匹配模式。例如负责身份验证的团队除了自身的代码还需要一个公用服务。

$ ./bootstrap.sh identity
Running ‘git sparse-checkout init --cone’
Running ‘git sparse-checkout set service/common service/identity’

网页客户端的照片浏览器小组则需要不同的代码目录。

$ ./bootstrap.sh browser
Running ‘git sparse-checkout init --cone’
Running ‘git sparse-checkout set service web/browser’

以稀疏模式复制仓库

在新复制一个仓库时使用 --no-checkout 选项防止工作目录被一大堆文件快速塞爆。

$ git clone --no-checkout https://github.com/derrickstolee/sparse-checkout-example
Cloning into 'sparse-checkout-example'...
remote: Enumerating objects: 14, done.
remote: Counting objects: 100% (14/14), done.
remote: Compressing objects: 100% (12/12), done.
remote: Total 1901 (delta 1), reused 11 (delta 1), pack-reused 1887
Receiving objects: 100% (1901/1901), 170.91 MiB | 74.79 MiB/s, done.
Resolving deltas: 100% (181/181), done.

$ cd sparse-checkout-example/

$ git sparse-checkout init --cone

$ git checkout main

$ ls
bootstrap.sh  LICENSE.md  README.md

Cone mode: restricted patterns improve performance

在大型项目中,模式匹配对性能的影响也是显著的。在 cone mode 下,匹配更加严格速度更快。这一小节涉及文件匹配模式对性能的影响,建议阅读原文,不作翻译。要启用 cone mode,使用 git sparse-checkout init --cone

稀疏检出和部分拷贝

稀疏检出仍然会复制整个仓库,只是检出用户需要的目录。所以如果代码仓库以 GB 论,新复制仓库时只检出 README 文件并不能节省什么时间和流量。

由于开发人员只需要下载部分文件,稀疏检出和部分拷贝一起使用可以更显著地加速工作流程。

$ git clone --filter=blob:none --no-checkout https://github.com/derrickstolee/sparse-checkout-example
Cloning into 'sparse-checkout-example'...
remote: Enumerating objects: 42, done.
remote: Counting objects: 100% (42/42), done.
remote: Compressing objects: 100% (41/41), done.
remote: Total 393 (delta 19), reused 6 (delta 1), pack-reused 351
Receiving objects: 100% (393/393), 81.13 KiB | 509.00 KiB/s, done.
Resolving deltas: 100% (33/33), done.

$ cd sparse-checkout-example

$ git sparse-checkout init --cone

$ git checkout main
remote: Enumerating objects: 2, done.
remote: Counting objects: 100% (2/2), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 1 (delta 0), pack-reused 1
Receiving objects: 100% (3/3), 1.41 KiB | 1.41 MiB/s, done.
Updating files: 100% (3/3), done.
Already on 'main'
Your branch is up to date with 'origin/main'.

$ git sparse-checkout set client/android
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 26 (delta 0), reused 1 (delta 0), pack-reused 23
Receiving objects: 100% (26/26), 985.91 KiB | 1.19 MiB/s, done.
Updating files: 100% (26/26), done.

$ tree -L 3
.
├── LICENSE.md
├── README.md
├── bootstrap.sh
└── client
    ├── README
    └── android
        ├── README.md
        ├── app.js
        ├── auth.js
        ├── config.js
        ├── images
        ├── package-lock.json
        ├── package.json
        ├── static
        └── views
5 directories, 10 files

--filter=<filter-spec> 在 clone 时过滤 blob 对象(文件内容),--filter=blob:none 会过滤所有 blob 而--filter=blob:limit=<size> 会过滤大小超过限制的 blob

在这个例子中,复制整个仓库只使用了 393 个对象,注意每一次 git 指令输出的 Receiving objects 行,初始化时只下载了 3 个对象,而拉取 client/android 目录时只下载了 26 个对象。