vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Releases: a tag and a pointer; the tarball is generated, not stored
f76c635b898957a8ce1b5267a21fe887f8234a13 · Keenan Nemetz · 18d ago · parent ada367f1
Releases: a tag and a pointer; the tarball is generated, not stored Writes the release/artifact design into Phase 7 as three slices, with the spike's numbers behind each decision: - a release is an annotated tag plus releases/<tag>.json on refs/heads/vole - an artifact is a literal LFS pointer blob, so the existing sweep marks it (gc parses pointers and walks every ref) and pre-receive already checks its oid and size; a JSON list of sha256 strings would be swept as garbage - source archives are streamed per request: plain tar by default, compression a request option, semaphore only for compressed formats - slice 4 makes published releases immutable at the gate, because a download link that can change bytes is worse than no link Adds one Phase 7 exit criterion, marked unmet because nothing is built, and moves releases out of the README's implicit promises into "Not yet".
docs/roadmap.md +50−1
| @@ -414,9 +414,58 @@ Planned: | |||
| 414 | 414 | empty state that prints the exact command that fills it. | |
| 415 | 415 | - Per-file history (`/commits/<ref>/<path>`), so "when did this change" is one click. | |
| 416 | 416 | ||
| 417 | Releases and artifacts — design settled by [spike D](spikes/archive-streaming/README.md), | ||
| 418 | nothing built yet: | ||
| 419 | |||
| 420 | - **A release is an annotated tag; its record is `releases/<tag>.json` on | ||
| 421 | `refs/heads/vole`.** The tag is the authority — immutable, visible to every clone, | ||
| 422 | `git show v1.2.0` works — and the record carries what a tag cannot: draft versus | ||
| 423 | published, artifact names, and an edit history. A lightweight tag is refused for a | ||
| 424 | release, because a tag with no tagger has nobody to attribute it to. | ||
| 425 | - **An artifact is a literal LFS pointer blob** at `releases/<tag>/<name>`. Measured: | ||
| 426 | `gc.Run` marks only blobs that `lfs.ParsePointer` accepts (`internal/gc/gc.go:84`) | ||
| 427 | and walks them from `rev-list --objects --all` (`internal/gitx/gitx.go:705`), which | ||
| 428 | includes `refs/heads/vole` — so published artifacts are reachable with **no GC | ||
| 429 | change** — and `pre-receive` pointer hygiene already checks the oid and the size at | ||
| 430 | the boundary. A JSON array of sha256 strings would parse as no pointer, count as | ||
| 431 | unreachable, and be swept once the grace period passed. | ||
| 432 | - **Source archives are generated per request, never stored.** Spike D: a real | ||
| 433 | 11.6 MiB repository archives in 54 ms plain and 221 ms gzipped, byte-identical | ||
| 434 | across runs and from a fresh clone, first byte at 4 ms. `ETag` is the tag object id | ||
| 435 | plus the format, so even a cache could not serve a stale byte — and none is needed. | ||
| 436 | Plain `tar` is the default: the 100 MiB binary fixture gzips 119.2 MiB to 80.4 MiB | ||
| 437 | for 13× the CPU, and `core.compression` is inert (levels 1, 6 and 9 produce | ||
| 438 | identical bytes). Only compressed requests need a semaphore; 8 concurrent plain tar | ||
| 439 | streams ran at 5,005 MiB/s against 273 MiB/s gzipped. | ||
| 440 | - **A source archive of an LFS repository contains pointers, not bytes** (measured), | ||
| 441 | so the download says so. Resolving pointers from the store while streaming is the | ||
| 442 | change that would make the tarball complete, and the trigger to do it is the first | ||
| 443 | repository where a tarball of pointers is not acceptable. | ||
| 444 | |||
| 445 | Slices: | ||
| 446 | |||
| 447 | - **Slice 2 — release from a tag.** Annotated tags listed on the repository page and | ||
| 448 | shown at `/r/<repo>/releases/<tag>` with the tag message as notes, commits since the | ||
| 449 | previous tag, and a streamed source archive; `vole release create <tag> -F <file>`. | ||
| 450 | Does not touch artifacts. | ||
| 451 | - **Slice 3 — artifacts.** `vole release attach <tag> <file>...` uploads over SSH | ||
| 452 | through the existing `put-object` path, then commits the pointer on the metadata | ||
| 453 | branch — bytes first, ref second, and the GC grace period covers the gap. Downloads | ||
| 454 | at `/r/<repo>/releases/<tag>/<name>` carry `ETag` = sha256, always | ||
| 455 | `Content-Disposition: attachment` with `nosniff` (an uploaded `.html` served inline | ||
| 456 | is stored XSS on the instance origin), and follow the clone read rule: anonymous for | ||
| 457 | a public repository, `404` for a private one. | ||
| 458 | - **Slice 4 — immutability and docs.** `pre-receive` refuses to move or delete a tag | ||
| 459 | a published release names, refuses a pointer path changing its oid, allows adding | ||
| 460 | artifacts but not removing them, and makes draft→published one-way. Without this a | ||
| 461 | download link can silently come to mean different bytes. | ||
| 462 | |||
| 417 | 463 | **Exit criteria:** a reviewer can approve a change using only the web — diff, checks and | |
| 418 | 464 | approvals on one page — without cloning; a new user can go from `vole init` to a pushed | |
| 419 | commit following the README alone; every page has a working path to its neighbours. | ||
| 465 | commit following the README alone; every page has a working path to its neighbours; a | ||
| 466 | tagged release with a 100 MiB artifact attached over SSH downloads anonymously and | ||
| 467 | matches `sha256sum` locally, and returns `404` on a private repository (unmet: the | ||
| 468 | release slices are not built, so this criterion is unmeasured). | ||
| 420 | 469 | ||
| 421 | 470 | ### Deferred — signing and audit | |
| 422 | 471 | ||