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.
Name branches after the account, and labels after the display
474a812d199a109ba894124cd6f26be12c73285d · Keenan Nemetz · 15d ago · parent ade6107e
Name branches after the account, and labels after the display
A contributor's folder was `refs/heads/contrib/<account-id>/`, which told a
reader nothing without decoding a ULID. It is now the account name,
refs/heads/contrib/<name>/<branch>
and the name is therefore fixed at creation: renaming an account would strand
every ref under its old folder, so `vole admin user rename` is gone and the
users row itself — kept after retirement — is what stops a name being reissued.
What people see is now a separate, mutable display name, validated by the same
rules wherever it is set (CLI, remote admin, registration, `/account`) and shown
on pages, approvals, listings and refusals. Nothing is authorized by it and
nothing on disk is keyed by it: metadata authors stay account names because the
push boundary resolves them for ownership, and approvals stay account ids.
`user_names`, `Snapshot.Reserved` and `Account.Former` are dropped: with a fixed
name there are no former names to remember, and the UNIQUE column already does
the reserving. Opening an older roster adds the `display` column and drops the
alias table in place; contributor refs written under the old id namespace are
left where they are, readable, and landable by a maintainer.
server/web/web.go +26−8
| @@ -50,26 +50,44 @@ type Web struct { | |||
| 50 | 50 | } | |
| 51 | 51 | ||
| 52 | 52 | func New(repos *repo.Store, ix *index.Store, ident *identity.Store, rs *runs.Store, cfg *config.Config) (*Web, error) { | |
| 53 | t, err := template.New("vole").Funcs(template.FuncMap{ | ||
| 53 | auth, err := newAuth(cfg) | ||
| 54 | if err != nil { | ||
| 55 | return nil, err | ||
| 56 | } | ||
| 57 | w := &Web{Repos: repos, Index: ix, Ident: ident, Runs: rs, Cfg: cfg, | ||
| 58 | Auth: auth, Ceremonies: newCeremonyStore(), sem: make(chan struct{}, archiveConc)} | ||
| 59 | // The templates are parsed after the Web exists so the roster-backed helper | ||
| 60 | // below can be a method: metadata files carry account names and approvals are | ||
| 61 | // keyed by ids, and a page should show the display label for either. | ||
| 62 | w.Tmpl, err = template.New("vole").Funcs(template.FuncMap{ | ||
| 54 | 63 | "since": humanSince, | |
| 55 | 64 | "short": format.SHA, | |
| 56 | 65 | "esc": url.PathEscape, | |
| 57 | 66 | "bytes": format.Bytes, | |
| 58 | 67 | "credid": identity.CredentialIDString, | |
| 68 | "who": w.who, | ||
| 59 | 69 | }).Parse(templates) | |
| 60 | 70 | if err != nil { | |
| 61 | 71 | return nil, err | |
| 62 | 72 | } | |
| 63 | auth, err := newAuth(cfg) | ||
| 64 | if err != nil { | ||
| 65 | return nil, err | ||
| 66 | } | ||
| 67 | w := &Web{Repos: repos, Index: ix, Ident: ident, Runs: rs, Cfg: cfg, Tmpl: t, | ||
| 68 | Auth: auth, Ceremonies: newCeremonyStore(), sem: make(chan struct{}, archiveConc)} | ||
| 69 | 73 | w.Logger = func(string, ...any) {} | |
| 70 | 74 | return w, nil | |
| 71 | 75 | } | |
| 72 | 76 | ||
| 77 | // who renders the account an id or name refers to as its display label. It is a | ||
| 78 | // presentation helper only: authors on disk stay account names and approvals | ||
| 79 | // stay ids, and nothing is authorized by what this returns. | ||
| 80 | func (w *Web) who(idOrName string) string { | ||
| 81 | if w.Ident == nil || idOrName == "" { | ||
| 82 | return idOrName | ||
| 83 | } | ||
| 84 | a, err := w.Ident.Find(idOrName) | ||
| 85 | if err != nil { | ||
| 86 | return idOrName | ||
| 87 | } | ||
| 88 | return a.Label() | ||
| 89 | } | ||
| 90 | |||
| 73 | 91 | // Page is embedded by every view. Exported so html/template can promote its | |
| 74 | 92 | // fields; an unexported embedded type is not reachable from a template. | |
| 75 | 93 | type Page struct { | |
| @@ -213,7 +231,7 @@ func (w *Web) Repo(rw http.ResponseWriter, r *http.Request) { | |||
| 213 | 231 | sort.Strings(names) | |
| 214 | 232 | var contributorBranches []string | |
| 215 | 233 | for _, name := range names { | |
| 216 | if _, ok := access.ContributorRefAccount("refs/heads/" + name); ok { | ||
| 234 | if _, ok := access.ContributorRefName("refs/heads/" + name); ok { | ||
| 217 | 235 | contributorBranches = append(contributorBranches, name) | |
| 218 | 236 | } | |
| 219 | 237 | } | |