Walgit – 一个以对象存储为后端的 Git 服务器

Hacker News Top 工具

摘要

Walgit 是一个 Git 服务器实现,作为单一二进制文件运行,后端使用 S3 或 GCS 等对象存储,实现了无本地状态的可扩展 Git 仓库托管。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/08/25 04:53

tobi/walgit 源码:https://github.com/tobi/walgit

walgit —— 一个仅以单个二进制文件运行于对象存储前端的 Git 服务器

walgit 以 无数据库、无主节点、无需本地持久状态 的方式托管 Git 仓库。您只需运行单个二进制文件,将其指向一个 S3 或 GCS 存储桶,即可获得: 智能 HTTP(v0/v2)拉取与推送, 作为静态文件提供的 bundle-uri 克隆, Git LFS 支持, 浏览型 Web UI, 带 SDK 的 JSON API, 基于仓库的推送策略, Webhooks —— 以及一个能扩展至仓库规模大于运行主机本身的服务器。

运行 walgit 的每台机器都是可丢弃的缓存;存储桶才是仓库本体。

# 1. 准备一个存储桶(任意兼容 S3 的存储或 GCS)和一份配置文件
cat > walgit.toml <<'EOF'
[server]
listen = "0.0.0.0:8080"
public_url = "https://git.example.com"
auto_create_on_push = true

[server.auth]
mode = "token"
anonymous_read = false
tokens = [{ principal = "me", token_env = "WALGIT_TOKEN_ME", write = true }]

[store]
backend = "s3"
bucket = "my-walgit"

[store.s3]
endpoint = "https://s3.us-east-1.amazonaws.com"
region = "us-east-1"
EOF

# 2. 运行服务
WALGIT_TOKEN_ME=$(openssl rand -hex 24)
walgit serve --config walgit.toml

# 3. 使用它——向一个新名称推送即会创建该仓库
git -c http.extraHeader="Authorization: Bearer $WALGIT_TOKEN_ME" push https://git.example.com/acme/app.git main

以上就是全部部署过程。添加更多指向同一存储桶的机器,它们就能一致地提供相同仓库的服务,无需任何协调。即使全部关闭,丢失的也只是缓存热度,而非数据本身。

它是对 Cursor 在《任意规模的 Git》(https://cursor.com/blog/git-at-any-scale)(他们称之为 Continuity 系统)一文中所描述架构的 Rust 实现,并针对在小于仓库规模的机器上运行进行了必要的修改。该文值得一读,其原文保留于 docs/reference/cursor-git-at-any-scale.md


为何如此设计

Git 是分布式的,这使得托管它变得痛苦,原因在于:包文件(packfiles)

仓库中的一切都被压缩进巨大的二进制包中,这些包为了体积最小化而非顺序读取而优化布局;任何 git 操作都是在数 GB 数据上的随机漫步。在本地磁盘且文件位于页缓存中时这没问题,但在网络文件系统上则是灾难性的,这也是为何所有尝试过的大厂都将仓库“简单地放在 NFS 上”的方案都失败了。

存活下来的设计(如 GitHub 的 Spokes)将真正的仓库保留在本地 NVMe 上,由上游 git 执行操作,并在包文件级别进行具有强一致性的复制——这需要跨固定副本集的三阶段提交、一个将每个仓库映射到其机器的数据库,以及一套“宠物式”(pet)服务器。

Continuity 的洞见改变了经济学模型:将对象存储中的预写日志(WAL)作为唯一真理源,将每个磁盘上的仓库都视为缓存。

一次推送会作为一个不可变对象存储在桶中,只有当一个微小的清单(manifest)通过比较并交换(CAS)被重写后才会变得可见。这个 CAS 就是共识机制——无需选举、无需法定人数、无需主节点。任何实例都可以接受推送;两个竞争的实例无法同时获胜。一个从未见过某仓库的副本读取日志后,即拥有了该仓库。读取无需协调即可保证一致性,因为每次读取前都会先向存储询问是否有变更(条件 GET,通常是 304)。压缩(Compaction)由持有租约的实例完成一次,并将结果发布到日志中,因此副本下载压缩后的包,而不是重新打包。

由于 WAL 是真理源,因此拥有完整的来源记录:每次推送和每次重新打包,均可回放到任何时间点。

walgit 直接采用了这一架构,并增加了在小型机器上运行大型单体仓库(monorepo)所需的功能:通过 HTTP 范围请求(range requests)为实例无法容纳其包文件的仓库提供引用(refs)和网页服务(远程读取器);将提交和树对象保留在本地,而大对象(blobs)保留在桶中(历史包);以及将克隆所需的字节传输完全移出服务器(bundle-uri:全新的克隆和追赶式同步由存储桶或 CDN 分发的静态文件提供)。

功能概览

功能描述
git智能 HTTP v0/v2:支持前缀的 ls-refs、带 filter/shallow/deepen/sideband-all 的 fetch、receive-pack(原子推送、删除、标签、push 选项、report-status-v2)、/ 命名空间、sha1 和 sha256 仓库。上游 git 执行 upload-pack/repack/bundle;walgit 执行 receive-pack、WAL 和底层管道操作。
bundle-uri包(bundles)按日历时间槽切割(每周全量、链式日增量、小时增量),是 WAL 的纯函数:全新克隆从桶中下载最新的全量包及其链上的增量包,仅向服务器请求剩余部分;追赶式同步仅下载其缺失的槽。每个仓库两个列表:bundles/list 用于克隆,bundles/catchup 用于 fetch。支持 --filter=blob:none 的无大对象(Blobless)克隆族。
LFS批量 API + 基础传输,对象存储于桶中,可选从上游 LFS 服务器进行直通读取(用于导入的仓库)。
Web UI + API基于读多写少的 JSON API(位于 /{owner}/{repo}/api/* 下)的 React UI(树、大对象、提交、差异、WAL 自身健康页面)。通过 sha 寻址的答案不可变且可全局缓存;较长的答案以 SSE 流式返回进度。repos.js 是一个无依赖的 SDK,可供页面、代理和脚本使用。
策略(Policy)每个仓库可配置推送规则(policy.json):受保护的引用、引用组、仅允许快进、绕过列表。详见 docs/POLICY.md
设置(Settings)每个仓库的配置(包调度、压缩、上游跟随)发布到 WAL 中并保留历史。
事件(Events)一个小型桥接模块跟踪 WAL 并将引用事件 POST 到 webhook,对 (仓库, 序号, 引用) 保证精确一次投递,并有持久化游标。详见 docs/EVENTS.md
维护(Maintenance)检查点(checkpoints)、包构建(bundle builds)、几何级压缩(geometric compaction)、基础重建(base rebuilds)、连通性审计和修复——一个循环在每次运行时根据(配置, WAL)计算期望状态,并执行最重要缺失工作中的一个有限单元。设计上即具备自愈能力:故障不会留下缺口;被删除的构件会被视为“缺失”并完全一致地重建。
认证(Auth)none(本地回环)、token(静态令牌)、oidc(任意 OpenID Connect 颁发者:浏览器登录、ID 令牌,以及 walgit 为 git 颁发的访问令牌)。/services/public/install.sh 可通过一条幂等命令为开发者配置好机器。
存储(Stores)S3 及兼容 S3 的存储(AWS, MinIO, rustfs, R2, Ceph 等)以及 GCS 作为一等公民;另有一个用于测试的内存存储。

简述工作原理

仓库即桶中的 WAL。

repos/<所有者>/<仓库>/ 下:manifest.pb(微小,CAS 重写:头部序列、活跃包集、检查点指针、设置——线性化点)、log/.pb(不可变条目:PUSH, COMPACT, CHECKPOINT, SETTINGS)、wal/.pack|.idx|.rev|.bitmap|.commit-graph(不可变、内容寻址的包及其辅助文件)、checkpoints/<序列号>/(折叠的引用快照 + 包清单,使冷启动变为快照 + 尾部)、bundles/leases/(带 TTL 的 CAS——唯一的跨实例互斥锁)、policy.jsonlfs/objects/events/cursor.json

一次推送: 我们的 receive-pack 索引包文件(在临时目录中执行 git index-pack --fix-thin --rev-index),检查连通性和策略,上传 包文件 ∥ 索引 ∥ 日志条目,然后 CAS 更新清单。遇到 412(Precondition Failed)错误时,它会重新读取清单,重新验证每个引用的旧值并重试。在一个实例上对同一仓库的并发推送会被成组提交(group commit)到一个 CAS 操作中。只有当存储桶确认后,客户端才会收到 ok

一次读取: 对清单执行一次条件 GET;304 → 从本地副本提供服务,200 → 应用新条目。“应用”的含义取决于请求的需求:引用(快照 + 日志 → packed-refs,无包文件:用于广告、API、包列表)、服务(按本机能容纳的大小提供包集:小包和历史包本地化,过大的基础包通过范围读取)、全量(所有本地化,用于重新打包)、对象(远程读取器,用于仓库不适合本机时的 UI)。包下载在它们自己的运行时中执行,永不阻塞引用请求。

放置(Placement)即配置。 [placement] serve / maintain 的通配符模式说明了主机为哪些仓库执行对象操作;引用级别的读取则在任何地方都有效。单机部署:保持默认即可。多机部署:将单体仓库放在带 SSD 的主机上(cache.mode = "disk"),其他仓库放在小型主机上,并通过 // 前缀进行路由。

无隐式等待。 任何耗时的操作都是一个带有 ID、日志和进度流的任务——在 sideband 2 上向 git 叙述(remote: * ...),并在浏览器中以 SSE 形式呈现。AGENTS.md 是完整的架构和操作手册:约束条件、WAL 策略、每个设计决策及其推理、不变式以及成本模型(对存储桶的往返次数是预算)。

运行

# 构建(需要按 rust-toolchain.toml 配置的 rust、protoc、用于 Web UI 的 node 24 + pnpm)
just web-build && cargo build --release -p walgit-cli
# 或:nix build .#walgit
# 或:podman build -t walgit -f Containerfile .

# 单机部署,由 walgit 自身提供 TLS,本地 S3 存储(容器中的 rustfs)
just dev-store ./target/release/walgit-server --config walgit.standalone.toml
open https://walgit.localhost:8080/
  • walgit.standalone.toml —— 单机模式配置(自签名 TLS、rustfs、所有角色)。从这里开始。
  • walgit.example.toml —— 包含所有配置项及其默认值和注释。
  • Containerfileflake.nix —— OCI 镜像和 Nix 包/开发环境。
  • deploy/nginx.conf.example —— 可选的前置 nginx:公共 TLS、每个凭证一个 auth_request、以及字节卸载:walgit 使用 X-Accel-Redirect 响应包/LFS 下载,nginx 负责从存储桶流式传输并缓存对象(S3 预签名 URL 或使用 walgit 的 bearer 令牌访问 GCS)。该文件详细说明了其契约。

角色(server.roles):

  • serve(git、API、UI、bundles、LFS)
  • maintain(检查点、包构建、压缩、fsck/修复)
  • events(webhook 桥接)

留空 = 所有角色。任意数量的 serve 主机可以指向同一个存储桶;为每个仓库分配一个维护者(通过放置通配符)即可。

认证

模式谁能进入git 如何认证
none所有人都是具有写权限的 anon —— 用于本地回环实验
token配置中的静态 tokenstoken_env 从环境变量读取密钥)Authorization: Bearer <令牌>,或将令牌作为 HTTP Basic 密码
oidc任意 OpenID Connect 颁发者(issueroauth_client_id/secretallowed_domains/allowed_emails):Google、Entra、Okta、Auth0、Keycloak、Dex、GitLab…walgit 访问令牌:在浏览器中一次性登录,在 /_auth/tokens 创建一个令牌,将其粘贴到安装器中。无状态(使用 session_secret 进行 HMAC,access_token_ttl 设置有效期);轮换密钥会撤销所有令牌。来自颁发者的 ID 令牌(audiences)和静态 tokens 也可用。

开发者设置只需一条幂等命令——sh -c "$(curl -fsSL 'https://git.example.com/services/public/install.sh')"——该命令将令牌存储在只有用户可读的文件中,安装一个小型 git 凭证助手(git ≥ 2.46:它通过 get 指令以 authtype=Bearer 响应,在真正的 401 错误时会 erase 令牌并告知获取新令牌的位置),并启用 transfer.bundleURI。随后 ?repo=owner/name 会立即开始克隆。

开发

just test          # 快速隔离层级(< 1分钟):单元 + 快速集成,内存存储,真实 git
just e2e           # 对服务器执行真实 git 操作(~20秒)
just warnings      # 所有目标零 rustc 警告
just ci            # 以上所有
cargo test -p walgit-server --test sim  # 故障注入模拟(崩溃、分区、陈旧读取)
just test-s3       # 针对本地 rustfs 的存储契约测试

代码地图:

crates/
  walgit-proto     protobuf 模式(wal.proto),日志帧,存储键
  walgit-store     ObjectStore trait(CAS 版本、条件 GET、范围、组合);后端 s3、gcs、memory;租约
  walgit-git       磁盘上的裸仓库,receive-pack,包摄入,refs ↔ packed-refs,广告,upload-pack 驱动程序
  walgit-wal       RepoHandle:同步级别,发布(成组提交 + CAS),检查点,日志读取器,远程读取器,任务
  walgit-bundle    bundle-uri:时间槽和链,构建,头部 ∘ 包组合,列表,保留策略
  walgit-server    axum:智能 HTTP,LFS,bundles,认证(none/token/oidc),维护者循环,上游跟随,web/(API、UI、SDK 路由、SSE),setup.rs(安装器 + 配方),事件桥接
  walgit-config    walgit.toml(+ WALGIT__ 环境变量覆盖),按仓库设置合并,失败关闭验证
  walgit-cli       `walgit serve|import|compact|bundle|wal|mirror|synth|config|repo`;`walgit-server` = `walgit serve`
web/               React SPA(Vite)+ sdk/repos.ts,编译进二进制文件;线路协议见 web/API.md
docs/              BUNDLE_URI_DESIGN, ROUNDTRIPS(成本模型), POLICY, LFS, INTEGRITY, EVENTS, CONTRACT, patches/

值得牢记的不变式

  • 清单 CAS 是唯一的提交点;它之前的一切不可见,它之后的一切都是幂等且可重放的。
  • 不可变对象是内容寻址的;除了清单、包列表和租约,没有任何东西会被覆盖。
  • 每次读取都首先向存储桶重新验证;不存在“最终一致”。
  • 本地磁盘是缓存。内存是缓存。存储桶才是仓库。
  • 放置是配置决定的,而非推断得出的;引用级别的读取在任何地方都有效,对象操作仅在被放置的地方有效。
  • 维护者的输出是(配置, WAL)的纯函数;缺失只是“尚未构建”。
  • 成本不得在任何热路径上随引用数量增长,也不得在包过大的小机器上随包大小增长。
  • 耗时工作是任务:可发现、可附着、可叙述。
  • 正确性并非充分条件:每个协议更改都以其对存储桶的往返次数(docs/ROUNDTRIPS.md)作为评判标准。

许可证

MIT —— 详见 LICENSE

相似文章

Gitolite

Lobsters Hottest

Gitolite 是一个用于在中央服务器上托管 Git 仓库的工具,具有细粒度的访问控制和许多其他强大功能。

我教会了一个桶说Git

Hacker News Top

作者演示了如何使用billy文件系统抽象,使Tigris对象存储桶表现得像一个文件系统,从而让go-git直接将其视为git仓库服务器。