Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Phase 6 slice 1: the tree is the site

d3296cda0c18f78097e37373822292707113d245 · Keenan Nemetz · 17d ago · parent 64ac5071

Phase 6 slice 1: the tree is the site

A repository's `pages` branch is served at <repo>.<pages_domain>, straight out
of git. No export step, no build to drift: the push moves the branch and the
next request reads the new tree.

- internal/pages: a pool of `git cat-file --batch` processes per active
  repository (closed after two idle minutes), an LRU keyed by (commit, path),
  and tree parsing in git's binary format with the object-id width detected
  rather than assumed
- ETag is the blob sha, Last-Modified the commit time, Content-Type from the
  extension or a sniff; If-None-Match and If-Modified-Since answer 304
- directories serve index.html and redirect to add the trailing slash; path
  resolution walks trees, so a traversal-shaped request is a missing page
- repository and ACL are resolved once per one-second memo window together with
  the branch tip, which is what makes the read path fast: doing it per request
  spawned three git processes and cost 3.8 ms, against 6.7 µs warm
- sites follow clone visibility, so a private repository answers 404; the repo
  page links to the site when pages_domain is configured

Measured: a site pushed over SSH is live in 1.4 s (the phase criterion is 5 s);
warm requests 6.7 µs, cold 144 µs. Custom domains and their TLS certificates are
the remaining Phase 6 work.

docs/roadmap.md +31−13

@@ -340,19 +340,37 @@ through history, and against an archived CI log).
340 340
341 341 ### Phase 6 — Site hosting
342 342
343 - A designated pages ref per repo, served as static files directly from the git tree — no
344 export step (spike C). A pool of `git cat-file --batch` processes (default 4; 1→4 doubled
345 throughput) with a read-through in-memory blob cache keyed by `(commit, path)`
346 - Cache is **required, not an optimisation**: cold git reads are ~1.2 ms p50, warm is
347 0.075 ms. LRU eviction keyed by commit, since a busy pages ref would otherwise grow it
348 - Per-repo hostname (`<repo>.<instance-domain>`) plus optional custom domain
349 - Automatic TLS via ACME when a domain is configured, with DNS-01 for wildcards
350 - ETag/`Last-Modified` from blob SHAs; correct `Content-Type` from extension and content sniff
351 - Cache invalidation keyed by commit, so there is no rebuild step to drift
352
353 **Exit criteria:** a pushed doc tree is live in under 5 seconds; conditional requests return
354 `304`; a custom domain gets a certificate with no manual steps; serving a repo with a
355 path-traversal-shaped filename is safe.
343 ✅ **Slice 1 — the tree is the site.** A repository's `pages` branch is served
344 directly out of git at `<repo>.<pages_domain>`: no export step, no build to drift.
345 Blobs come from a pool of `git cat-file --batch` processes (default 4, per active
346 repository, closed after two idle minutes) and results are cached by `(commit, path)`
347 in an LRU. `ETag` is the blob sha, `Last-Modified` the commit time, `Content-Type`
348 comes from the extension or a content sniff, and conditional requests answer `304`.
349 A directory serves its `index.html`, and a directory URL without a trailing slash
350 redirects so relative links keep working. Path resolution walks trees in memory, so a
351 traversal-shaped URL is simply a missing page — it never reaches the filesystem.
352 Sites follow clone visibility: a private repository has no site, because there is no
353 session with which an anonymous reader could authenticate. The ref tip *and* the
354 authorization decision are resolved together and memoised for one second, and a moved
355 tip drops the previous commit's cache entries, so a push is live well inside the
356 five-second criterion. (Measured on this implementation: 6.7 µs per warm request,
357 144 µs with the caches cleared — the difference between a docs site that feels like a
358 file server and one that feels like a database. Resolving the repository and its ACL
359 per request cost 3.8 ms, because it spawned three `git` subprocesses; that is why both
360 are resolved once per memo window.)
361
362 Remaining:
363
364 - Custom domains per repository, and automatic TLS (ACME) for them
365 - A per-repository pages ref; today `pages_ref` is one setting for the instance
366 - The `pages` branch has no review story of its own: it is a normal branch, so
367 protecting it is `vole acl protect`
368
369 **Exit criteria:** a pushed doc tree is live in under 5 seconds ✅ (measured: pushed
370 over SSH, served on the site host, polled until the new bytes appeared); conditional
371 requests return `304` ✅; a custom domain gets a certificate with no manual steps ❌
372 (needs the ACME slice); serving a repo with a path-traversal-shaped filename is safe ✅
373 (and so is a traversal-shaped *request*).
356 374
357 375 ### Phase 7 — Signing and audit
358 376