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).
internal/gitx/gitx.go +7−2
| @@ -365,6 +365,11 @@ func (r *Repo) Symref() string { | |||
| 365 | 365 | return strings.TrimSpace(string(out)) | |
| 366 | 366 | } | |
| 367 | 367 | ||
| 368 | // commitFormat is one line per commit: sha, author, email, epoch, subject. | ||
| 369 | // %x1f is a unit separator, which cannot appear in a subject the way a tab or a | ||
| 370 | // newline can. | ||
| 371 | const commitFormat = "%H%x1f%an%x1f%ae%x1f%at%x1f%s" | ||
| 372 | |||
| 368 | 373 | type Commit struct { | |
| 369 | 374 | SHA string | |
| 370 | 375 | Author string | |
| @@ -409,7 +414,7 @@ func scrubbedEnv() []string { | |||
| 409 | 414 | ||
| 410 | 415 | // Log returns up to n commits from rev. | |
| 411 | 416 | func (r *Repo) Log(rev string, n int) ([]Commit, error) { | |
| 412 | out, errs, err := r.run(nil, "log", "--format=%H%x1f%an%x1f%ae%x1f%at%x1f%s", | ||
| 417 | out, errs, err := r.run(nil, "log", "--format="+commitFormat, | ||
| 413 | 418 | "-n", strconv.Itoa(n), rev, "--") | |
| 414 | 419 | if err != nil { | |
| 415 | 420 | return nil, fmt.Errorf("log %q: %w: %s", rev, err, errs) | |
| @@ -536,7 +541,7 @@ func (r *Repo) CountMerges(old, new string) (int, error) { | |||
| 536 | 541 | // backlinks, so the search happens in git rather than in a second index. | |
| 537 | 542 | func (r *Repo) LogGrep(rev, pattern string, n int) ([]Commit, error) { | |
| 538 | 543 | out, errs, err := r.run(nil, "log", | |
| 539 | "--grep="+pattern, "--fixed-strings", "--format=%H%x1f%an%x1f%ae%x1f%at%x1f%s", | ||
| 544 | "--grep="+pattern, "--fixed-strings", "--format="+commitFormat, | ||
| 540 | 545 | "-n", strconv.Itoa(n), rev, "--") | |
| 541 | 546 | if err != nil { | |
| 542 | 547 | return nil, fmt.Errorf("log --grep %q: %w: %s", pattern, err, errs) | |