Git backed by object storage because you can't stop me
git object-storage kefka
10

Configure Feed

Select the types of activity you want to include in your feed.

docs: add CLAUDE.md guide for the objgitd codebase

Captures the non-obvious bits: the shared daemon backing both
transports, why git:// receive-pack hides PackfileWriter to avoid
deadlocking on EOF, why s3fs needs a tempBuffer for go-git's
read-while-write PackWriter, and the suffix-based smart-HTTP dispatch.

Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso (May 27, 2026, 11:15 PM EDT) 984bd5d7 fb1c642e

+66
+66
CLAUDE.md
··· 1 + # CLAUDE.md 2 + 3 + This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 + 5 + ## What this is 6 + 7 + `objgitd` is a single-binary Git server that stores repositories as objects in a Tigris/S3 bucket instead of on a local filesystem. It speaks two transports against the same backend: 8 + 9 + - **Smart HTTP** (`-http-bind`, default `:8080`) — primary transport, where auth middleware would wrap. 10 + - **git://** (`-git-bind`, default `:9418`) — unauthenticated TCP, opt-in. 11 + 12 + Module path: `tangled.org/xeiaso.net/objgit`. Go 1.26. 13 + 14 + ## Commands 15 + 16 + ```text 17 + go build ./... # build everything 18 + go build -o objgitd ./cmd/objgitd 19 + go test ./... # full test suite 20 + go test ./cmd/objgitd/... # protocol tests (require `git` on PATH; skipped otherwise) 21 + go test -run TestSmartHTTP ./cmd/objgitd/... 22 + 23 + # Run locally. Flags can also come from env via flagenv (UPPER_SNAKE of the flag name). 24 + # A .env file in CWD is auto-loaded by godotenv. 25 + ./objgitd -bucket $BUCKET -http-bind :8080 -allow-push 26 + ``` 27 + 28 + `flagenv` maps `-allow-push` → `ALLOW_PUSH`, `-bucket` → `BUCKET`, etc. Tigris client credentials come from the standard AWS SDK chain (`AWS_PROFILE` etc.). 29 + 30 + ## Architecture 31 + 32 + ### The `daemon` is the shared backend 33 + 34 + `cmd/objgitd/main.go` constructs one `*daemon` holding `(fs billy.Filesystem, loader transport.Loader, allowPush bool)` and serves it through both transports concurrently under an `errgroup`. Repository resolution, the `allowPush` gate, and **create-on-first-push** all live on `*daemon` (`loadOrInit` in `git_protocol.go`) so both transports behave identically. 35 + 36 + - `cmd/objgitd/git_protocol.go` — git:// TCP server: `Serve` → `handle` decodes a `packp.GitProtoRequest`, then dispatches to `transport.UploadPack` / `UploadArchive` / `ReceivePack`. 37 + - `cmd/objgitd/http.go` — `*daemon` implements `http.Handler` directly. Dispatch is by **URL suffix** (`/info/refs`, `/git-upload-pack`, `/git-receive-pack`) because repo paths are variable-depth and `http.ServeMux` wildcards can't capture a prefix before a fixed suffix. Smart-HTTP uses the same go-git server commands with `StatelessRPC: true` (and `AdvertiseRefs: true` for `GET /info/refs`). 38 + 39 + ### Two subtle protocol points 40 + 41 + 1. **`streamingStorer` in `git_protocol.go`** wraps the storer for git:// receive-pack to **hide its `PackfileWriter` capability**. `UpdateObjectStorage`'s `PackfileWriter` path uses `io.CopyBuffer` and only returns on `io.EOF`, which deadlocks over a persistent TCP socket (the client is waiting for report-status). Hiding the capability falls through to `Parser.Parse`, which knows the pack's end from the format itself. HTTP doesn't need this — request bodies have a real EOF — so HTTP keeps the faster PackfileWriter path. Trade-off: git:// pushes write loose objects (one S3 PUT per object). 42 + 43 + 2. **No-op closers everywhere.** `transport.UploadPack`/`ReceivePack` call `Close` on the reader (and sometimes the writer) between negotiation rounds. The git:// socket can't survive that, and the HTTP `ResponseWriter` doesn't implement `Close`. Wrap with `io.NopCloser` (reader) and `ioutil.WriteNopCloser` from `go-git/v6/utils/ioutil` (writer). 44 + 45 + ### `internal/s3fs` — billy.Filesystem on Tigris 46 + 47 + Vendored from Austin Poor's s3fs and adapted to **billy v6** and the Tigris `storage-go` client. Treats an S3 bucket as a filesystem so go-git's `filesystem.NewStorage` can store loose objects and packs against it. 48 + 49 + The non-obvious piece is `tempfs.go`. go-git's streaming `PackWriter` creates a temp pack file, **immediately reopens the same path for reading**, and reads it back concurrently while writing — to build the index. S3 cannot do that on a single live object, so until the final `Rename` actually uploads, the bytes live in an in-memory `tempBuffer` registered on the `S3FS` by canonical key. `readAt` returns `(0, io.EOF)` past the current end so go-git's `syncedReader` can distinguish "no data yet" from a hard error and retry. 50 + 51 + All S3 keys go through `S3FS.key` → `cleanPath` + leading-slash strip. Any new S3 op must funnel through there or chroot/path semantics will desync. 52 + 53 + ### `internal/slog.go` 54 + 55 + Trivial JSON-handler init. The convention across the codebase is `slog` with `"err"` (not `"error"`) as the error key. 56 + 57 + ## Conventions 58 + 59 + - Flags use **kebab-case** and are paired with `flagenv` for env fallback. 60 + - Errgroup owns server lifecycle; `signal.NotifyContext` provides cancellation; HTTP shutdown uses a 10s `context.WithTimeout`. 61 + - Tests are **table-driven with `tt`** and gated by `exec.LookPath("git")` when they shell out to a real git client (see `http_test.go`, `git_protocol_test.go`). Shared helpers (`runGit`, `tryGit`, `seedRepo`) live in `git_protocol_test.go` and are reused across the package. 62 + - Plans for non-trivial work go in `docs/plans/` (see `git-http-protocol.md` for the style). 63 + 64 + ## Tigris / object storage notes 65 + 66 + When working with Tigris buckets, access keys, or IAM, the `tigris-storage` skills are available. Tigris is S3-compatible; the client used here is `github.com/tigrisdata/storage-go` (a thin Tigris-aware wrapper that the s3fs layer talks to directly).