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/auth.go +3−2
| @@ -152,7 +152,7 @@ func (w *Web) page(r *http.Request, title string) Page { | |||
| 152 | 152 | p := Page{Title: title} | |
| 153 | 153 | if pr := w.principal(r); pr != nil { | |
| 154 | 154 | p.LoggedIn = true | |
| 155 | p.User = pr.account.Name | ||
| 155 | p.User = pr.account.Label() | ||
| 156 | 156 | p.AccountID = pr.account.ID | |
| 157 | 157 | p.CSRF = pr.session.CSRF | |
| 158 | 158 | } | |
| @@ -234,6 +234,7 @@ type ceremony struct { | |||
| 234 | 234 | AccountID string | |
| 235 | 235 | InviteToken string | |
| 236 | 236 | Name string | |
| 237 | Display string | ||
| 237 | 238 | Label string | |
| 238 | 239 | Expires time.Time | |
| 239 | 240 | } | |
| @@ -422,7 +423,7 @@ const authJS = `(function(){ | |||
| 422 | 423 | await post('/auth/invite/finish',resp(c)); | |
| 423 | 424 | location.href='/'; | |
| 424 | 425 | }else if(form.dataset.auth==='register'){ | |
| 425 | const b=await post('/auth/register/begin',{username:form.username.value.trim()}); | ||
| 426 | const b=await post('/auth/register/begin',{username:form.username.value.trim(),display:form.display?form.display.value.trim():''}); | ||
| 426 | 427 | const c=await navigator.credentials.create({publicKey:prepCreate(b)}); | |
| 427 | 428 | await post('/auth/register/finish',resp(c)); | |
| 428 | 429 | location.href='/'; | |