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/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