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.
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).
internal/gitx/archive.go +37
| @@ -0,0 +1,37 @@ | |||
| 1 | package gitx | ||
| 2 | |||
| 3 | // Archive streams a tar of a revision. A release tarball is generated this way | ||
| 4 | // rather than stored: `git archive` output is a pure function of the revision | ||
| 5 | // (entry mtimes come from the commit), so caching it would be an optimisation | ||
| 6 | // that cannot serve stale bytes, and skipping the store means a tarball cannot | ||
| 7 | // drift from the tag it claims to be. Measured at 54 ms tar / 221 ms tar.gz for | ||
| 8 | // a real repository — see docs/spikes/archive-streaming. | ||
| 9 | |||
| 10 | import ( | ||
| 11 | "bytes" | ||
| 12 | "fmt" | ||
| 13 | "io" | ||
| 14 | "strings" | ||
| 15 | ) | ||
| 16 | |||
| 17 | // Archive writes `git archive --format=<format> <rev>` to w. prefix, when set, | ||
| 18 | // is prepended to every path so extraction creates one directory instead of | ||
| 19 | // scattering the tree into the current one. | ||
| 20 | // | ||
| 21 | // The caller owns the response: bytes are written as git produces them, so a | ||
| 22 | // failure part-way is a truncated download, not an error this can report. | ||
| 23 | func (r *Repo) Archive(w io.Writer, rev, format, prefix string) error { | ||
| 24 | args := []string{"archive", "--format=" + format} | ||
| 25 | if prefix != "" { | ||
| 26 | args = append(args, "--prefix="+prefix) | ||
| 27 | } | ||
| 28 | args = append(args, rev) | ||
| 29 | cmd := r.command(args...) | ||
| 30 | var errb bytes.Buffer | ||
| 31 | cmd.Stderr = &errb | ||
| 32 | cmd.Stdout = w | ||
| 33 | if err := cmd.Run(); err != nil { | ||
| 34 | return fmt.Errorf("archive %s: %w: %s", rev, err, strings.TrimSpace(errb.String())) | ||
| 35 | } | ||
| 36 | return nil | ||
| 37 | } | ||