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 7 slice 2: a release is a tag the web can read

c56911e2c3ede0f70c7cde560042806dbf4b4ab0 · Keenan Nemetz · 17d ago · parent f76c635b

Phase 7 slice 2: a release is a tag the web can read

A release is an annotated tag, so nothing new is stored: the tag message is the
notes, the tree is the tarball, and both are already in every clone.

- internal/gitx/tags.go lists refs/tags peeled to their commits and tells an
  annotated tag from a lightweight one; a lightweight tag's %(contents) is the
  commit message, so reading it as notes would invent a release description
  nobody wrote
- internal/gitx/archive.go streams `git archive`; tarballs are generated per
  request and never stored, so they cannot disagree with the tag
- internal/web/releases.go renders the list (lightweight tags labelled), the
  release page (notes through the same sanitiser as every pushed body, commits
  since the previous release, two downloads) and the archive handlers: ETag is
  the tag object id plus the format, conditional requests get 304, always
  Content-Disposition: attachment with nosniff, compressed streams queue behind
  a semaphore of 4 while plain tar does not
- "since the previous release" is `git describe --abbrev=0 <tag>^`, not the next
  row of the tag list: two tags made in the same second sort arbitrarily, and a
  page that changes its baseline between requests is worse than one without one
- the page warns when the tagged tree tracks LFS, because git archive copies
  pointer files verbatim (measured in spike D)
- cmd/vole release create writes the annotated tag and refuses an empty message
  or an existing name: re-tagging is how a download link comes to mean different
  bytes

Measured end to end against the binary with an 11.6 MiB, 2,566-file repository:
release list 6.1 ms, release page 12.6 ms, .tar 53.7 ms / 13.7 MiB, .tar.gz
218 ms / 1.8 MiB, conditional request 304. Every file in the served tarball
hashed to its ls-tree blob id (2,565 of 2,565).

docs/roadmap.md +36−17

@@ -402,6 +402,28 @@ when they are cut, binary files are named rather than faked, and every line is e
402 402 execute. A repository's README renders on its home page, and markdown blobs render as
403 403 markdown, through the same sanitiser issues use.
404 404
405 ✅ **Slice 2 — releases are tags, on the web.** `internal/gitx/tags.go` lists
406 `refs/tags` peeled to the commits they name, and `internal/web/releases.go` renders
407 them: `/r/<repo>/releases` lists every tag and labels the lightweight ones, and
408 `/r/<repo>/releases/<tag>` shows the tag message as notes (through the same markdown
409 sanitiser as every other pushed body), the commits since the previous release, and two
410 downloads. Tarballs are generated per request by `git archive` and never stored, so a
411 tarball cannot disagree with its tag; the `ETag` is the tag object id plus the format,
412 and a conditional request gets a `304`. `vole release create <tag> -m|-F` writes the
413 annotated tag and refuses the two mistakes that produce a release page with nothing on
414 it: an empty message and an existing tag. "Since the previous release" is
415 `git describe --abbrev=0 <tag>^` rather than the next row in the tag list, because two
416 tags created in the same second sort arbitrarily and a page that changes its baseline
417 between requests is worse than one with no baseline. Compressed archives queue behind a
418 semaphore of 4; plain `tar` does not, because it streamed at 5,005 MiB/s under 8
419 concurrent requests. `git archive` copies LFS pointers verbatim, so the page warns when
420 the tagged tree tracks LFS, which is the honest alternative to resolving pointers while
421 streaming.
422 (Measured on an 11.6 MiB, 2,566-file repository served by the binary: release list
423 6.1 ms, release page 12.6 ms, `.tar` 53.7 ms / 13.7 MiB, `.tar.gz` 218 ms / 1.8 MiB,
424 conditional request `304`; every file in the served tarball hashed to its `ls-tree` blob
425 id, 2,565 of 2,565.)
426
405 427 Planned:
406 428
407 429 - Identity without ceremony: `vole whoami` resolves the key a person pushes with to an
@@ -414,14 +436,14 @@ Planned:
414 436 empty state that prints the exact command that fills it.
415 437 - Per-file history (`/commits/<ref>/<path>`), so "when did this change" is one click.
416 438
417 Releases and artifacts — design settled by [spike D](spikes/archive-streaming/README.md),
439 Release artifacts — design settled by [spike D](spikes/archive-streaming/README.md),
418 440 nothing built yet:
419 441
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.
442 - **The tag stays the release; a record at `releases/<tag>.json` on `refs/heads/vole`
443 holds only what a tag cannot.** Slice 2 needed no record at all — notes, tree and
444 provenance all live in the tag. Drafts, a published flag and the artifact list do not
445 fit in a tag without rewriting it, and rewriting a tag is exactly what a published
446 release must not do, so those move to one JSON file per release.
425 447 - **An artifact is a literal LFS pointer blob** at `releases/<tag>/<name>`. Measured:
426 448 `gc.Run` marks only blobs that `lfs.ParsePointer` accepts (`internal/gc/gc.go:84`)
427 449 and walks them from `rev-list --objects --all` (`internal/gitx/gitx.go:705`), which
@@ -437,17 +459,13 @@ nothing built yet:
437 459 for 13× the CPU, and `core.compression` is inert (levels 1, 6 and 9 produce
438 460 identical bytes). Only compressed requests need a semaphore; 8 concurrent plain tar
439 461 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.
462 - **A source archive of an LFS repository contains pointers, not bytes** (measured), and
463 slice 2 warns on the page when the tagged tree tracks LFS. Resolving pointers from the
464 store while streaming is the change that would make the tarball complete; the trigger
465 is the first repository where a tarball of pointers is not good enough.
444 466
445 Slices:
467 Remaining slices:
446 468
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 469 - **Slice 3 — artifacts.** `vole release attach <tag> <file>...` uploads over SSH
452 470 through the existing `put-object` path, then commits the pointer on the metadata
453 471 branch — bytes first, ref second, and the GC grace period covers the gap. Downloads
@@ -464,8 +482,9 @@ Slices:
464 482 approvals on one page — without cloning; a new user can go from `vole init` to a pushed
465 483 commit following the README alone; every page has a working path to its neighbours; a
466 484 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).
485 matches `sha256sum` locally, and returns `404` on a private repository (unmet: artifact
486 uploads are slice 3, so only the source-archive half of the release story is built;
487 `404` on a private repository is proven by `TestReleasePagesAreNotPublicForPrivateRepos`).
469 488
470 489 ### Deferred — signing and audit
471 490