Using Git-upload-pack for a simpler CI integration
使用 Git-upload-pack 简化 CI 集成
Arnold Noronha Uncategorized 2025年5月9日 2025年5月10日 4 分钟
在 Screenshotbot 的早期,我们做出的一个决定是不读取您的 GitHub 仓库。
事实证明,这对我们来说是一个巨大的优势。它让我们的客户在使用我们的产品时更加有信心,并且更容易通过大型企业的安全审查。
而且,它也让我们更容易与其他 Git 提供商(如 GitLab、BitBucket 或 Phabricator)集成。如果我们不依赖自定义 API,我们的代码就可以为所有人工作。(我们仍然需要访问他们的 API 才能在 Pull Request 上发表评论,但在许多平台上,该权限更加精细。)实际上,我们的部分功能也可以在自托管的 Git 仓库上运行。
但是,即使我们不需要读取您的仓库,我们仍然需要访问“图”来进行诸如“找到我们有可用构建的最后一个提交”或“找到最佳合并基础”之类的决策。我们将讨论我们过去是如何做到这一点的,并且还将讨论我们使用 git-upload-pack
协议的新功能,如果您想了解 Git 内部原理,这可能是一篇有趣的文章。
昨天之前我们是如何做的
为了实现这一点,Screenshotbot 在我们的服务器上为每个仓库存储一个“commit-graph”。一个 commit-graph 只是提交的 SHA:对于每个提交,我们存储它的父级。我们不存储关于您的 Git 仓库的任何其他信息。
在您的 CI 运行中,我们会使用类似于 git log --all --pretty="%H %P" --max-count 100
的命令来查看最近的 1000 个提交。我们将它们上传到我们的服务器(同样,只有 SHA),服务器负责将这些图与我们已经知道的图合并。实际上,我们总是可以访问完整的图。
但这确实会导致一些问题:
- 更难在 CI 中支持 shallow clones,因为本地仓库无法访问所有提交来在本地生成图。
- 对于大型 monorepo,我们可能会为每个提交生成和合并多次相同的图。当然,我们可以更优化我们合并图的方式。
使用 git-upload-pack
我们最近与一位客户合作,他们绝对需要 shallow clones。如果不使用 shallow clones 克隆他们的仓库,会使他们的构建增加几分钟。
我们想避免依赖 GitHub 特定的 API 来解决这个问题。同样,这很难维护,而且更难测试。这也意味着我们的用户需要执行额外的配置步骤来授予我们 API 访问权限,这是我们不想做的。
但是我们意识到这一点:我们大多数客户的 CI 作业已经具有对其 Git 仓库的 SSH 访问权限。肯定有一种方法可以直接通过 SSH 获取我们需要的信息,并且希望是高效的?
确实有,那就是 git-upload-pack
。
当您克隆或拉取仓库时,您的本地客户端会建立到远程服务器的 SSH 连接并运行 git-upload-pack
。这会启动一个交互式会话,协商需要传输哪些信息。
官方协议是一个丑陋且混乱的二进制协议,并且对实际上不应该进行微优化的事情进行微优化(恕我直言)。但是,如果您忽略确切的线路格式,则该协议大致如下:
- 服务器告诉我们所有的 refs(例如
refs/heads/main
)以及关联的提交 SHA。它还告诉我们服务器支持哪些功能(“filter”功能特别有用,“allow-reachable-sha1-in-want”也很有用)。 - 客户端向服务器询问它想要哪些 refs,以及它已经拥有哪些对象。对象可以是提交、blob 或关于文件系统树的信息。如果服务器支持“filter”功能,我们也可以告诉它避免发送 blob,这会显着减少网络流量。
- 服务器使用 packfile 格式发送完成我们的图所需的所有对象。
Packfile 只是对象的集合。对象可以是提交、关于文件和目录的“tree”信息或 blob。粗略地说,Packfile 是 <类型, 长度, zlib 压缩内容>
的集合。
将 git-upload-pack
连接到 Screenshotbot
现在我们了解了 git-upload-pack,我们可以执行以下操作:
- 在 CI 作业中,我们的 CLI 工具打开到远程
git-upload-pack
的连接 - Git 服务器告诉我们关于 refs 和提交的信息。
- 我们的 CLI 工具然后与 Screenshotbot 服务器检查我们需要哪些 refs 和提交来完成 Screenshotbot 版本的图。
- CLI 工具将它需要的提交以及它拥有的信息传递给 Git 服务器
- Git 服务器有效地发送提交信息,我们将其转换为我们可以发送到 Screenshotbot 服务器的内容
这就是全部了。据我们所知,没有现有的命令行方式可以做到这一点,所以我们不得不从头开始实现该协议。如果您有兴趣,这是实现。
注意事项
如果您尝试使用 git-upload-pack
协议,请注意以下事项:
- 该协议很丑陋。例如,为什么“features”作为第一个“have”和“want”的一部分发送,而不是作为它自己的行?为什么对象类型优化为 3 位,而不是 8 位?这使得代码更加复杂,并且对象的数量将远远小于对象的内容。有一个 v2 协议我没有看过,但我们选择了 v1 以获得更广泛的支持。
- Packfile 格式也很丑陋。例如,为什么格式会根据对象的类型而有所不同?但我有些理解为什么 Packfile 需要是丑陋的,因为它在实际执行查询的热路径中。
- 一些 Git 服务器(特别是 Phabricator)在发送 Packfile 后不发送 EOF,这取决于您的 zlib 库如何处理数据而具有重要意义。例如,如果您的 zlib 库在处理之前一次读取多个字节,它可能不知道何时停止。您需要使用一次读取一个字节的 zlib 库。
- 不同的 Git 服务器可能有不同的限制。Azure DevOps 要求客户端使用 multi_ack 模式,我们没有构建该模式。我们只是决定不支持在具有更具体要求的服务器上的新流程,而是在这些情况下回退到我们以前的实现。
总结
TL;DR:如果您是 Screenshotbot 用户,您现在可以使用 shallow clones 了!
如果您在这里寻找如何使用 git-upload-pack,我希望这篇评论和我们的实现能帮助您。欢迎在下面提出任何问题。