16 · Git on Object Storage
Buzz 的 Git 服务没有把一个长期 bare repository 挂在共享 POSIX 盘上,而是把 immutable Git packs 与 manifest 放进 S3-compatible storage。每次请求临时 hydrate bare repo;push 产生新 pack 和 manifest;refs 的并发线性化由 S3 conditional write CAS 完成。Nostr event 是提交后的通知,不是 refs 权威。
1. 对象模型
immutable objects
packs/<hash>.pack
packs/<hash>.idx
manifests/<hash>.json
mutable pointer
repos/<community>/<repo>/HEAD-MANIFEST
content = manifest hash
ETag = CAS versionManifest 描述当前 refs 与需要的 immutable packs。物理 pack 以内容寻址,上传是 create-only;同内容重复上传可安全去重。
2. 读取/clone/fetch
Git client
│ smart HTTP + NIP-98 auth
▼
Relay git transport
├─ resolve tenant/repo binding/policy
├─ read manifest pointer
├─ fetch manifest + packs (local immutable cache may hit)
├─ hydrate ephemeral bare repo
└─ run upload-pack → stream response临时 repo 只是计算工作区。请求结束可删除,因为 object store 才是持久真相。
3. Push 写入
receive-pack in hydrated repo
│ hooks + ref policy
▼
new immutable packs / candidate manifest
│ upload all immutable dependencies first
▼
CAS manifest pointer (If-Match old ETag)
├─ success → commit point
│ └─ publish kind 30618 notification
└─ conflict → loser retries from new manifest; uploaded packs become harmless garbage最关键的不变量是依赖先落盘,指针后提交。如果先更新 pointer 再上传 pack,读者可能看到无法重建的 manifest。
4. 为什么 CAS 足够
假设对象存储提供:
- immutable write 完成后 durable。
- 对新对象 read-after-write。
If-Match/If-None-Match条件写线性化。
两个并发 push 都从 ETag E0 开始:
P1: upload A ── CAS(E0→M1) ✓
P2: upload B ── CAS(E0→M2) ✗
└─ reload M1, re-evaluate ref updates, retry只有一个 CAS 成功,故不会 silent lost update。失败者可能留下未引用 pack,但不破坏 refs;GC 可以稍后回收。
5. 为什么不用 advisory lock
数据库锁只能在参与者都走同一数据库时协调,而且持锁跨 S3/git receive-pack 容易超时。CAS 把互斥放在真正权威 pointer 上:
- Relay pod 崩溃不会留下长锁。
- 多 pod 不需固定路由。
- 冲突只浪费上传/计算,不破坏正确性。
代价是高争用 repo 会重复 hydrate/upload,需要退避、冲突预算和 orphan GC。
6. 本地 pack cache
Relay 可缓存 immutable packs:
- key 是内容 hash,不存在“同 key 新内容”。
- cache 只提升性能,miss 时从 object store 恢复。
- 总字节数有界,按策略淘汰。
- 读取后仍可验证 hash/size。
mutable manifest pointer 不适合长 TTL 缓存,否则 clone 会看到旧 refs;若缓存必须绑定 ETag/短验证。
7. 鉴权与仓库绑定
Git credential helper 生成 NIP-98 authorization,Relay 解析 tenant、repo name 和操作。权限来源包括:
- 社区/Relay 成员。
- NIP-OA owner delegation。
- repo binding 与保护规则。
- read/push/admin scope。
- branch/ref pattern、角色、force-push/delete policy。
Repo name normalization 必须在认证签名、HTTP path、DB binding 和 object key 四处一致,防编码差异映射到另一仓库。
8. Hook 与 ref policy
receive-pack 不是接受所有 Git 更新:
- 解析 old/new OID 与 ref。
- 区分 create/update/delete/force update。
- 按保护规则匹配 branch pattern。
- 验证 push actor/role。
- 拒绝越权 force-push、删除或受保护分支写入。
策略在 CAS 前执行;CAS 冲突后必须基于新 refs 重新验证,不能沿用旧状态下的许可结论。
9. Nostr Git 事件
Buzz 还定义 repo announcement、patch、PR、issue、refs/manifest 等 kinds。它们服务发现、协作和实时通知,但分两类:
| 类别 | 权威 |
|---|---|
| patch/issue/PR discussion | Nostr event + Relay DB |
| Git refs/object reachability | object store manifest pointer CAS |
kind 30618 类 manifest event 必须在 CAS 成功后发布。消费者错过事件仍可读 pointer;看到事件也应以 pointer/object store 为准。
10. Git 签名工具
git-credential-nostr:为 smart HTTP 请求生成 NIP-98 credential。git-sign-nostr:用 Nostr secp256k1 identity 签 Git object/commit,按 NIP-GS 表达签名。
这让代码提交、Relay 身份和协作事件可以追溯到同一公钥体系,但 credential 签 HTTP 与 commit 签名是两种不同证明,不能互相替代。
11. 失败与恢复
| 失败 | 后果/恢复 |
|---|---|
| pack upload 中断 | pointer 未变;孤立对象可 GC |
| CAS 冲突 | reload/re-evaluate/retry |
| CAS 成功后事件发布失败 | Git 已提交;后续读取 pointer/补发通知 |
| Relay 崩溃 | 临时 repo 可丢;对象存储可重建 |
| object store 丢 pack | manifest 无法重建,是严重 durability 破坏 |
| cache 损坏 | hash 验证失败,丢弃并回源 |
12. 形式化取证
仓库提供 GitOnObjectStore.tla,模型关注并发 push、pointer CAS、manifest 重建与失败排序。设计文档给出相对于对象存储假设的证明草图;它不是对真实 S3 实现的无条件证明,部署仍需验证 provider 的 conditional write 与 consistency 契约。
13. 源码入口
docs/git-on-object-storage.md:完整协议、故障与证明。docs/spec/GitOnObjectStore.tla:并发状态机模型。crates/buzz-relay/src/api/git/:hydrate、manifest、CAS publish、pack cache、policy 与 transport。crates/buzz-db/src/git_repo.rs:repo binding/reservation。crates/buzz-core/src/git_perms.rs:ref pattern 与保护规则。crates/git-credential-nostr/:NIP-98 credential helper。crates/git-sign-nostr/:Nostr Git signature。docs/nips/NIP-GS.md:Git signing 规范。