Git

Git 捆绑包是存储包文件以及一些额外元数据的文件,包括一组引用和一组(可能为空)的必要提交。有关更多信息,请参阅 git-bundle[1]gitformat-bundle[5]

捆绑包 URI 是 Git 可以下载一个或多个捆绑包的位置,以便在从远程获取剩余对象之前预先引导对象数据库。

一个目标是加快网络连接到源服务器较差的用户的克隆和获取速度。另一个好处是允许像 CI 构建场这样的重度用户将本地资源用于大部分 Git 数据,从而减少源服务器上的负载。

为了启用捆绑包 URI 功能,用户可以使用命令行选项指定捆绑包 URI,或者源服务器可以通过协议 v2 功能通告一个或多个 URI。

设计目标

捆绑包 URI 标准旨在足够灵活,以满足多个工作负载。捆绑包提供程序和 Git 客户端在创建和使用捆绑包 URI 时有多种选择。

  • 捆绑包可以有服务器想要的任何名称。此名称可以通过使用捆绑包内容的哈希来引用不可变数据。但是,这意味着在每次更新内容后都需要一个新的 URI。如果服务器通告 URI(并且服务器知道正在生成新捆绑包),这可能是可以接受的,但对于使用命令行选项的用户来说则不符合人体工程学。

  • 这些包可以专门用于引导完整克隆,但也可以组织起来用于引导增量获取。包提供程序必须决定采用多种组织方案中的一种,以在增量获取期间最大程度地减少客户端下载,但 Git 客户端也可以选择对其中任何一种操作使用包。

  • 包提供程序可以选择支持完整克隆、部分克隆或两者。如果存在,客户端可以检测哪些包适用于存储库的部分克隆筛选器。

  • 包提供程序可以使用单个包(仅用于克隆),或多个包。在使用多个包时,提供程序可以指定客户端是否需要所有包 URI 才能进行完整克隆,或者任何一个包 URI 是否足够。这允许包提供程序对不同的地理区域使用不同的 URI。

  • 包提供程序可以使用启发式方法(例如创建令牌)来组织包,以帮助客户端避免下载不需要的包。当包提供程序不提供这些启发式方法时,客户端可以使用优化来最大程度地减少下载的数据量。

  • 包提供程序无需与 Git 服务器关联。客户端可以选择在 Git 服务器未对其进行通告的情况下使用包提供程序。

  • 客户端可以选择发现由 Git 服务器通告的包提供程序。这可能发生在 git clone 期间、git fetch 期间、两者同时发生或两者都不发生。用户可以选择最适合自己的组合。

  • 客户端可以选择在任何时候手动配置包提供程序。客户端还可以选择将包提供程序手动指定为 git clone 的命令行选项。

每个存储库各不相同,每个 Git 服务器都有不同的需求。希望包 URI 功能足够灵活,可以满足所有需求。如果不是,则可以通过其版本控制机制扩展该功能。

服务器要求

为了提供包服务器的服务器端实现,不需要 Git 协议的其他部分。这允许服务器维护人员使用 CDN 等静态内容解决方案来提供包文件。

在包 URI 功能的当前范围内,所有 URI 预计为 HTTP(S) URL,其中内容使用对该 URL 的 GET 请求下载到本地文件。服务器可以对这些请求包含身份验证要求,目的是触发配置的凭据帮助程序以进行安全访问。(未来的扩展可以使用“file://”URI 或 SSH URI。)

假设服务器返回200 OK响应,则会检查 URL 中的内容。首先,Git 尝试将文件解析为版本 2 或更高版本的捆绑文件。如果文件不是捆绑文件,则使用 Git 的配置解析器将文件解析为纯文本文件。该配置文件中的键值对应描述捆绑 URI 列表。如果这两种解析尝试均不成功,则 Git 会向用户报告错误,指出捆绑 URI 提供了错误的数据。

服务器提供的任何其他数据均被视为错误数据。

捆绑列表

Git 服务器可以使用一组key=value对来通告捆绑 URI。捆绑 URI 还可以提供包含这些相同key=value对的 Git 配置格式的纯文本文件。在这两种情况下,我们都将其视为捆绑列表。这些对指定有关捆绑的信息,客户端可使用这些信息来决定要下载哪些捆绑以及要忽略哪些捆绑。

一些键关注列表本身的属性。

bundle.version

(必需)此值提供捆绑列表的版本号。如果未来的 Git 更改启用了需要 Git 客户端对捆绑列表文件中新键做出反应的功能,则此版本将递增。当前唯一的版本号是 1,如果指定任何其他值,则 Git 将无法使用此文件。

bundle.mode

(必需)此值有两个值之一:allany。当指定all时,客户端应期望需要所有与存储库要求匹配的已列出的捆绑 URI。当指定any时,客户端应期望与存储库要求匹配的任何一个捆绑 URI 都足够。通常,any选项用于列出位于不同地理位置的多个不同的捆绑服务器。

bundle.heuristic

如果存在此字符串值键,则捆绑列表旨在与增量git fetch命令配合使用。启发式信号表示为每个捆绑提供了其他键,这些键有助于确定客户端应下载的捆绑子集。当前计划的唯一启发式是creationToken

其余键包括一个<id>段,该段是服务器为每个可用捆绑指定的名称。<id>必须仅包含字母数字和-字符。

bundle.<id>.uri

(必需)此字符串值是下载捆绑<id>的 URI。如果 URI 以协议(http://https://)开头,则 URI 是绝对的。否则,URI 被解释为相对于用于捆绑列表的 URI。如果 URI 以/开头,则该相对路径相对于用于捆绑列表的域名。(使用相对路径的目的是为了更容易地在具有不同域名的多个服务器或 CDN 上分发一组捆绑。)

bundle.<id>.filter

此字符串值表示对象过滤器,该过滤器还应出现在此捆绑的标头中。服务器使用此值来区分不同类型的捆绑,客户端可以从中选择与他们的对象过滤器匹配的捆绑。

bundle.<id>.creationToken

此值为用于对捆绑包列表进行排序的非负 64 位整数。当 bundle.heuristic=creationToken 时,这用于在获取期间下载捆绑包子集。

bundle.<id>.location

此字符串值通告捆绑包 URI 所提供服务的真实位置。这可用于向用户提供选项,以选择要使用的捆绑包 URI,或仅作为 Git 选择的捆绑包 URI 的信息性指示器。仅当 bundle.modeany 时,此值才有用。

以下是一个使用 Git 配置格式的捆绑包列表示例

[bundle]
	version = 1
	mode = all
	heuristic = creationToken
[bundle "2022-02-09-1644442601-daily"]
	uri = https://bundles.example.com/git/git/2022-02-09-1644442601-daily.bundle
	creationToken = 1644442601
[bundle "2022-02-02-1643842562"]
	uri = https://bundles.example.com/git/git/2022-02-02-1643842562.bundle
	creationToken = 1643842562
[bundle "2022-02-09-1644442631-daily-blobless"]
	uri = 2022-02-09-1644442631-daily-blobless.bundle
	creationToken = 1644442631
	filter = blob:none
[bundle "2022-02-02-1643842568-blobless"]
	uri = /git/git/2022-02-02-1643842568-blobless.bundle
	creationToken = 1643842568
	filter = blob:none

此示例使用 bundle.mode=all 以及 bundle.<id>.creationToken 启发式。它还使用 bundle.<id>.filter 选项来提供两组并行的捆绑包:一组用于完整克隆,另一组用于无 blob 部分克隆。

假设此捆绑包列表在 URI https://bundles.example.com/git/git/ 中找到,因此两个无 blob 捆绑包具有以下完全展开的 URI

  • https://bundles.example.com/git/git/2022-02-09-1644442631-daily-blobless.bundle

  • https://bundles.example.com/git/git/2022-02-02-1643842568-blobless.bundle

通告捆绑包 URI

如果用户知道要克隆的存储库的捆绑包 URI,则他们可以通过命令行选项手动指定该 URI。但是,Git 主机可能希望在克隆操作期间通告捆绑包 URI,以帮助不了解该功能的用户。

此功能的唯一要求是服务器可以通告一个或多个捆绑包 URI。此通告采用新的协议 v2 功能的形式,专门用于发现捆绑包 URI。

客户端可以选择一个任意捆绑包 URI 作为选项通过一些探索性检查选择具有最佳性能的 URI。由捆绑包提供程序决定是否拥有多个 URI 比通过服务器端基础设施进行地理分布的单个 URI 更可取。

使用捆绑包 URI 克隆

捆绑包 URI 的主要需求是加快克隆速度。Git 客户端将根据以下流程与捆绑包 URI 交互

  1. 用户使用 --bundle-uri 命令行选项指定捆绑包 URI客户端发现 Git 服务器通告的捆绑包列表。

  2. 如果从捆绑包 URI 下载的数据是捆绑包,则客户端将检查捆绑包标头以查看客户端存储库中是否存在先决条件提交 OID。如果缺少一些,则客户端将延迟解绑,直到解绑其他捆绑包,使这些 OID 存在。当所有必需的 OID 都存在时,客户端将使用 refspec 解绑该数据。默认 refspec 为 +refs/heads/*:refs/bundles/*,但可以配置此项。这些 ref 被存储,以便以后的 git fetch 协商可以将每个捆绑 ref 作为 have 进行通信,从而减少通过 Git 协议进行获取的大小。为了允许从该 ref 命名空间中剪除 ref,Git 可能会引入一个编号命名空间(例如 refs/bundles/<i>/*),以便可以删除过时的捆绑包 ref。

  3. 如果文件是捆绑包列表,则客户端将检查 bundle.mode 以查看列表是 all 还是 any 形式。

    1. 如果 bundle.mode=all,则客户端将考虑所有捆绑包 URI。根据与客户端存储库的部分克隆筛选器匹配的 bundle.<id>.filter 选项缩小列表。然后,请求所有捆绑包 URI。如果提供了 bundle.<id>.creationToken 启发式,则按创建令牌的递减顺序下载捆绑包,当捆绑包具有所有必需的 OID 时停止。然后可以按递增创建令牌顺序解绑捆绑包。如果捆绑包列表未通告具有较大创建令牌的捆绑包,则客户端将存储最新的创建令牌作为避免将来下载的启发式。

    2. 如果 bundle.mode=any,则客户端可以选择任何一个捆绑包 URI 进行检查。客户端可以使用多种方法在这些 URI 中进行选择。如果初始选择未返回结果,客户端还可以回退到另一个 URI。

请注意,在克隆期间,我们希望所有捆绑包都是必需的,并且可以将诸如 bundle.<uri>.creationToken 的启发式用于按时间顺序或并行下载捆绑包。

如果给定的捆绑包 URI 是具有 bundle.heuristic 值的捆绑包列表,则客户端可以选择将该 URI 存储为其选择的捆绑包 URI。然后,客户端可以在以后的 git fetch 调用期间直接导航到该 URI。

在下载包 URI 时,客户端可以选择在提交下载整个内容之前检查初始内容。这可能会提供足够的信息来确定 URI 是包列表还是包。如果是包,客户端可能会检查包头以确定所有已通告的提示都已在客户端存储库中,并取消剩余的下载。

使用包 URI 提取

当客户端提取新数据时,它可以在从原始远程提取之前决定从包服务器提取。这可以通过命令行选项来完成,但更有可能使用在克隆期间指定的配置值(例如)。

提取操作遵循相同的过程从包列表下载包(尽管我们希望在此处使用并行下载)。我们希望当瘦包中的所有先决条件提交 OID 已在对象数据库中时,此过程将结束。

当使用 creationToken 启发式方法时,如果客户端的创建令牌不大于存储的创建令牌,则客户端可以避免下载任何包。在提取新包后,Git 会更新此本地创建令牌。

如果包提供程序不提供启发式方法,则客户端应尝试在下载完整包数据之前检查包头,以防包提示已存在于客户端存储库中。

错误条件

如果 Git 客户端在根据包 URI 或在该位置找到的包列表下载信息时发现意外情况,则 Git 可以忽略该数据并继续,就好像未给它包 URI 一样。远程 Git 服务器是最终的真实来源,而不是包 URI。

以下是一些示例错误条件

  • 客户端无法使用给定的 URI 与服务器连接,或者连接丢失且无法恢复。

  • 客户端收到 400 级响应(例如 404 Not Found401 Not Authorized)。客户端应使用凭据帮助程序查找并提供 URI 的凭据,但在处理特定 400 级错误方面与 Git 的其他 HTTP 协议的语义匹配。

  • 服务器报告任何其他故障响应。

  • 客户端收到无法解析为包或包列表的数据。

  • 包包含与预期不符的筛选器。

  • 客户端无法解包包,因为先决条件提交 OID 不在对象数据库中,并且没有更多包可供下载。

还有一些情况可能被视为浪费,但不是错误条件

  • 下载的包包含的信息多于克隆或提取请求所请求的信息。一个主要示例是,如果用户使用 --single-branch 请求克隆,但下载了存储所有可达提交的包,这些提交来自所有 refs/heads/* 引用。这可能最初是浪费的,但也许这些对象将通过客户端关心的稍后的引用更新变得可达。

  • git fetch 期间下载的包包含对象数据库中已有的对象。如果我们使用包进行获取,这可能是不可避免的,因为在对远程服务器执行“追赶”获取后,客户端几乎总是在包服务器之前。当客户端的获取频率远高于服务器计算包的频率时,这种额外的工作最浪费,例如,如果客户端使用带后台维护的每小时预取,但服务器每周计算包。因此,除非服务器通过 bundle.heuristic 值明确建议使用包 URI,否则客户端不应将其用于获取。

示例包提供者组织

包 URI 功能经过精心设计,可以灵活地适应包提供者组织对象数据的方式。但是,在此处描述一个完整的组织模型可能会有所帮助,以便提供者可以从此基础开始。

此示例组织是 GVFS 缓存服务器(请参阅本文档结尾附近的章节)使用的简化模型,尽管使用了 Git 之外的额外软件,但该模型在加速克隆和获取非常大的存储库方面是有益的。

包提供者在多个地理位置部署服务器。每个服务器管理自己的包集。服务器可以跟踪多个 Git 存储库,但基于模式为每个存储库提供包列表。例如,在 https://<domain>/<org>/<repo> 镜像存储库时,包服务器可以在 https://<server-url>/<domain>/<org>/<repo> 提供其包列表。原始 Git 服务器可以在“any”模式下列出所有这些服务器

[bundle]
	version = 1
	mode = any
[bundle "eastus"]
	uri = https://eastus.example.com/<domain>/<org>/<repo>
[bundle "europe"]
	uri = https://europe.example.com/<domain>/<org>/<repo>
[bundle "apac"]
	uri = https://apac.example.com/<domain>/<org>/<repo>

此“列表的列表”是静态的,只有在添加或删除包服务器时才会更改。

每个捆绑服务器管理它自己的捆绑集。初始捆绑列表仅包含一个捆绑,其中包含从源服务器克隆存储库收到的所有对象。该列表使用 creationToken 启发式方法,并根据服务器的时间戳为捆绑创建 creationToken

捆绑服务器定期对捆绑列表运行计划更新,例如每天一次。在此任务期间,服务器从源服务器获取最新内容,并生成一个捆绑,其中包含可从最新源引用访问但未包含在先前计算的捆绑中的对象。此捆绑将添加到列表中,注意 creationToken 必须严格大于先前的最大 creationToken

当捆绑列表增长过大(例如超过 30 个捆绑)时,最旧的“N 减去 30”个捆绑将合并到一个捆绑中。此捆绑的 creationToken 等于合并捆绑中的最大 creationToken

此处提供了一个示例捆绑列表,尽管它只有两个每日捆绑,而不是完整的 30 个捆绑列表

[bundle]
	version = 1
	mode = all
	heuristic = creationToken
[bundle "2022-02-13-1644770820-daily"]
	uri = https://eastus.example.com/<domain>/<org>/<repo>/2022-02-09-1644770820-daily.bundle
	creationToken = 1644770820
[bundle "2022-02-09-1644442601-daily"]
	uri = https://eastus.example.com/<domain>/<org>/<repo>/2022-02-09-1644442601-daily.bundle
	creationToken = 1644442601
[bundle "2022-02-02-1643842562"]
	uri = https://eastus.example.com/<domain>/<org>/<repo>/2022-02-02-1643842562.bundle
	creationToken = 1643842562

为了避免永久存储和提供对象数据,尽管这些数据在源服务器中变得不可访问,此捆绑合并可以更加谨慎。与其采用旧捆绑的绝对并集,相反,可以通过查看较新的捆绑并确保其必要的提交都可在此合并捆绑(或其他较新的捆绑中)获得来创建捆绑。这允许“过期”对象数据,这些数据不会在此时间窗口中被新提交使用。该数据可以通过稍后的推送重新引入。

此数据组织的目的是有两个主要目标。首先,通过从更近的源下载预先计算的对象数据,可以使存储库的初始克隆变得更快。其次,git fetch 命令可以更快,特别是如果客户端几天没有获取。但是,如果客户端 30 天没有获取,则捆绑列表组织将导致重新下载大量对象数据。

使此组织对频繁获取的用户更有用的方法之一是更频繁地创建捆绑。例如,可以每小时创建一次捆绑,然后每天将这些“每小时”捆绑合并到“每日”捆绑中。每日捆绑在 30 天后合并到最旧的捆绑中。

如果此存储库的客户端希望使用无 blob 的部分克隆,建议使用 blob:none 过滤器重复此捆绑策略。此无 blob 捆绑列表与完整捆绑列表位于同一列表中,但使用 bundle.<id>.filter 键来区分这两组。对于非常大的存储库,捆绑提供程序可能希望提供无 blob 捆绑。

实施计划

此设计文档作为愿望文档单独提交,目标是在多个补丁系列中实现所有提到的客户端功能。以下是提交这些功能的潜在大纲

  1. 使用 --bundle-uri 选项将捆绑 URI 集成到 git clone 中。这将包括一个新的 git fetch --bundle-uri 模式,用作 git clone 下面的实现。此处的初始版本将在给定的 URI 处期望单个捆绑。

  2. 实现从捆绑 URI 解析捆绑列表并更新 git fetch --bundle-uri 逻辑以正确区分 bundle.mode 选项的能力。具体设计该功能,以便配置文件格式解析将键值对列表馈送到捆绑列表逻辑中。

  3. 创建 bundle-uri 协议 v2 命令,以便 Git 服务器可以使用键值对通告捆绑 URI。插入到捆绑列表逻辑的现有键值输入中。允许 git clone 发现这些捆绑 URI 并从捆绑数据引导客户端存储库。(此选择是通过配置文件选项和命令行选项进行选择加入的。)

  4. 允许客户端了解 bundle.heuristic 配置键和 bundle.<id>.creationToken 启发式方法。当 git clone 发现带有 bundle.heuristic 的捆绑 URI 时,它会将客户端存储库配置为在以后的 git fetch <remote> 命令期间检查该捆绑 URI。

  5. 允许客户端在 git fetch 期间发现捆绑 URI,并在设置 bundle.heuristic 的情况下为以后的获取配置捆绑 URI。

  6. 实现“检查标头”启发式方法,以在 bundle.<id>.creationToken 启发式方法不可用时减少数据下载。

在审查这些功能时,此计划可能会更新。我们还期望随着此功能的成熟并在实际场景中使用,发现并实现新的设计。

Git 协议已经具备一项功能,即 Git 服务器可以在为客户端请求提供服务时,将一组 URL 与包文件响应一起列出。然后,预期客户端在这些位置下载包文件,以便完全理解响应。

Gerrit 服务器(使用 JGit 实现)使用此机制,并且在减少 CPU 负载和提高克隆的用户性能方面非常有效。

此机制的一个主要缺点是,源服务器需要确切知道这些包文件中包含什么,并且在服务器响应后,用户需要一段时间才能使用这些包文件。源和包文件数据之间的这种耦合很难管理。

此外,此实现很难与获取一起使用。

GVFS 协议 [2] 是一组 HTTP 端点,在 Git 的部分克隆创建之前独立于 Git 项目设计。此协议的一个特性是“缓存服务器”的概念,该服务器可以与构建机器或开发人员办公室并置,以便在不超载中央服务器的情况下传输 Git 数据。

VFS for Git 以其 GET /gvfs/objects/{oid} 端点而闻名,该端点允许按需下载对象。这是该产品文件系统虚拟化的关键部分。

但是,一个更微妙的需求是 GET /gvfs/prefetch?lastPackTimestamp=<t> 端点。给定可选的时间戳,缓存服务器会响应一个预先计算的包文件列表,其中包含在这些时间间隔内引入的提交和树。

缓存服务器使用以下策略计算这些“预取”包文件

  1. 每小时生成一个带有给定时间戳的“每小时”包。

  2. 每晚,将前 24 个每小时包汇总到一个“每日”包中。

  3. 每晚,将所有超过 30 天的预取包汇总到一个包中。

当用户针对具有缓存服务器的存储库运行 gvfs clonescalar clone 时,客户端会请求所有预取包文件,最多为 24 + 30 + 1 个包文件,仅下载提交和树。然后,客户端向源服务器发出对引用的请求,并尝试签出该提示引用。(有一个额外的端点可帮助从给定的提交中获取所有可达的树,以防该提交尚未包含在预取包文件中。)

git fetch 期间,钩子使用先前下载的预取包文件的最新时间戳请求预取端点。只有时间戳较新的包文件列表才会被下载。大多数用户每小时获取一次,因此他们最多获取一个每小时预取包。机器已关闭或在 30 多天内未获取的用户可能会重新下载所有预取包文件。这种情况很少见。

需要注意的是,客户端始终联系原始服务器以获取 refs 通告,因此 refs 通常“领先于”预取包数据。在需要时,使用 GET gvfs/objects/{oid} 请求按需下载丢失的对象,例如 git checkoutgit log 命令。一些 Git 优化禁用了检查,这会导致这些按需下载过于激进。

scroll-to-top