Skip to content

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. 对象模型

text
immutable objects
  packs/<hash>.pack
  packs/<hash>.idx
  manifests/<hash>.json

mutable pointer
  repos/<community>/<repo>/HEAD-MANIFEST
       content = manifest hash
       ETag = CAS version

Manifest 描述当前 refs 与需要的 immutable packs。物理 pack 以内容寻址,上传是 create-only;同内容重复上传可安全去重。

2. 读取/clone/fetch

text
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 写入

text
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 开始:

text
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 discussionNostr event + Relay DB
Git refs/object reachabilityobject 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 丢 packmanifest 无法重建,是严重 durability 破坏
cache 损坏hash 验证失败,丢弃并回源

12. 形式化取证

仓库提供 GitOnObjectStore.tla,模型关注并发 push、pointer CAS、manifest 重建与失败排序。设计文档给出相对于对象存储假设的证明草图;它不是对真实 S3 实现的无条件证明,部署仍需验证 provider 的 conditional write 与 consistency 契约。

13. 源码入口

独立源码研究笔记 · 非 Buzz 官方文档