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.

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/web/web.go +46−30

@@ -34,6 +34,9 @@ type Web struct {
34 34 Cfg *config.Config
35 35 Tmpl *template.Template
36 36 Logger func(format string, args ...any)
37
38 // sem bounds compressed archive streams; see releases.go.
39 sem chan struct{}
37 40 }
38 41
39 42 func New(repos *repo.Store, ix *index.Store, ident *identity.Store, rs *runs.Store, cfg *config.Config) (*Web, error) {
@@ -45,7 +48,8 @@ func New(repos *repo.Store, ix *index.Store, ident *identity.Store, rs *runs.Sto
45 48 if err != nil {
46 49 return nil, err
47 50 }
48 w := &Web{Repos: repos, Index: ix, Ident: ident, Runs: rs, Cfg: cfg, Tmpl: t}
51 w := &Web{Repos: repos, Index: ix, Ident: ident, Runs: rs, Cfg: cfg, Tmpl: t,
52 sem: make(chan struct{}, archiveConc)}
49 53 w.Logger = func(string, ...any) {}
50 54 return w, nil
51 55 }
@@ -157,6 +161,16 @@ func (w *Web) Repo(rw http.ResponseWriter, r *http.Request) {
157 161 if w.Cfg.PagesDomain != "" {
158 162 site = "http://" + m.Name + "." + w.Cfg.PagesDomain
159 163 }
164 // A release is an annotated tag, so the count comes from the tag list rather
165 // than from anything the web has to maintain.
166 releaseCount := 0
167 if tags, err := g.Tags(); err == nil {
168 for _, t := range tags {
169 if t.Annotated {
170 releaseCount++
171 }
172 }
173 }
160 174 branches, _ := g.ListRefs("refs/heads/")
161 175 names := make([]string, 0, len(branches))
162 176 for k := range branches {
@@ -164,36 +178,38 @@ func (w *Web) Repo(rw http.ResponseWriter, r *http.Request) {
164 178 }
165 179 w.render(rw, "repo", struct {
166 180 Page
167 Name string
168 Description string
169 Ref string
170 CloneURL string
171 SSHURL string
172 Entries []gitx.Entry
173 Commits []gitx.Commit
174 Commit string
175 Count int
176 Branches []string
177 IssueCount int
178 Site string
179 Readme template.HTML
180 Runs []*runs.Run
181 Name string
182 Description string
183 Ref string
184 CloneURL string
185 SSHURL string
186 Entries []gitx.Entry
187 Commits []gitx.Commit
188 Commit string
189 Count int
190 Branches []string
191 IssueCount int
192 ReleaseCount int
193 Site string
194 Readme template.HTML
195 Runs []*runs.Run
181 196 }{
182 Page: w.base(m.Name),
183 Name: m.Name,
184 Description: m.Description,
185 Ref: ref,
186 CloneURL: cloneURL(r, m.Name),
187 SSHURL: sshURL(w.Cfg.SSHAddr, m.Name),
188 Entries: entries,
189 Commits: commits,
190 Commit: commit,
191 Count: count,
192 Branches: names,
193 IssueCount: issueCount,
194 Site: site,
195 Readme: readme,
196 Runs: recentRuns,
197 Page: w.base(m.Name),
198 Name: m.Name,
199 Description: m.Description,
200 Ref: ref,
201 CloneURL: cloneURL(r, m.Name),
202 SSHURL: sshURL(w.Cfg.SSHAddr, m.Name),
203 Entries: entries,
204 Commits: commits,
205 Commit: commit,
206 Count: count,
207 Branches: names,
208 IssueCount: issueCount,
209 ReleaseCount: releaseCount,
210 Site: site,
211 Readme: readme,
212 Runs: recentRuns,
197 213 }, 0)
198 214 }
199 215