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.
Name branches after the account, and labels after the display
474a812d199a109ba894124cd6f26be12c73285d · Keenan Nemetz · 16d 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.
forge/identity/passkey_test.go +7−7
| @@ -36,7 +36,7 @@ func testCredential(t *testing.T, label string) Credential { | |||
| 36 | 36 | func TestCreateWithInviteAndAcceptPasskey(t *testing.T) { | |
| 37 | 37 | t.Parallel() | |
| 38 | 38 | s := newStore(t) | |
| 39 | a, inv, err := s.CreateWithInvite("alice", "operator", time.Hour) | ||
| 39 | a, inv, err := s.CreateWithInvite("alice", "", "operator", time.Hour) | ||
| 40 | 40 | if err != nil { | |
| 41 | 41 | t.Fatal(err) | |
| 42 | 42 | } | |
| @@ -99,7 +99,7 @@ func TestCreateWithInviteAndAcceptPasskey(t *testing.T) { | |||
| 99 | 99 | func TestNewInviteSupersedesTheOldOne(t *testing.T) { | |
| 100 | 100 | t.Parallel() | |
| 101 | 101 | s := newStore(t) | |
| 102 | _, first, err := s.CreateWithInvite("alice", "operator", time.Hour) | ||
| 102 | _, first, err := s.CreateWithInvite("alice", "", "operator", time.Hour) | ||
| 103 | 103 | if err != nil { | |
| 104 | 104 | t.Fatal(err) | |
| 105 | 105 | } | |
| @@ -118,7 +118,7 @@ func TestNewInviteSupersedesTheOldOne(t *testing.T) { | |||
| 118 | 118 | func TestExpiredInviteIsInvalid(t *testing.T) { | |
| 119 | 119 | t.Parallel() | |
| 120 | 120 | s := newStore(t) | |
| 121 | _, inv, err := s.CreateWithInvite("alice", "operator", time.Hour) | ||
| 121 | _, inv, err := s.CreateWithInvite("alice", "", "operator", time.Hour) | ||
| 122 | 122 | if err != nil { | |
| 123 | 123 | t.Fatal(err) | |
| 124 | 124 | } | |
| @@ -137,7 +137,7 @@ func TestExpiredInviteIsInvalid(t *testing.T) { | |||
| 137 | 137 | func TestPasskeyOnlyAccountSurvivesReopen(t *testing.T) { | |
| 138 | 138 | t.Parallel() | |
| 139 | 139 | s := newStore(t) | |
| 140 | a, inv, err := s.CreateWithInvite("alice", "operator", time.Hour) | ||
| 140 | a, inv, err := s.CreateWithInvite("alice", "", "operator", time.Hour) | ||
| 141 | 141 | if err != nil { | |
| 142 | 142 | t.Fatal(err) | |
| 143 | 143 | } | |
| @@ -210,7 +210,7 @@ func TestUpdatePasskeyCounterAndRemove(t *testing.T) { | |||
| 210 | 210 | func TestRetireEndsInvitesAndSessionsAndHidesCredentials(t *testing.T) { | |
| 211 | 211 | t.Parallel() | |
| 212 | 212 | s := newStore(t) | |
| 213 | a, inv, err := s.CreateWithInvite("alice", "operator", time.Hour) | ||
| 213 | a, inv, err := s.CreateWithInvite("alice", "", "operator", time.Hour) | ||
| 214 | 214 | if err != nil { | |
| 215 | 215 | t.Fatal(err) | |
| 216 | 216 | } | |
| @@ -288,7 +288,7 @@ func TestCreatePasskeySelfRegistration(t *testing.T) { | |||
| 288 | 288 | s := newStore(t) | |
| 289 | 289 | id := "01M5JR2JK29KKMAA1T9MDNTPBP" | |
| 290 | 290 | cred := testCredential(t, "first passkey") | |
| 291 | a, err := s.CreatePasskey("alice", id, cred, "self-registration") | ||
| 291 | a, err := s.CreatePasskey("alice", "", id, cred, "self-registration") | ||
| 292 | 292 | if err != nil { | |
| 293 | 293 | t.Fatal(err) | |
| 294 | 294 | } | |
| @@ -302,7 +302,7 @@ func TestCreatePasskeySelfRegistration(t *testing.T) { | |||
| 302 | 302 | ||
| 303 | 303 | // A second registration for the same name must not leave an account behind. | |
| 304 | 304 | other := "01M5JR2JK29KKMAA1T9MDNTPBQ" | |
| 305 | if _, err := s.CreatePasskey("alice", other, testCredential(t, "other"), "self-registration"); err == nil { | ||
| 305 | if _, err := s.CreatePasskey("alice", "", other, testCredential(t, "other"), "self-registration"); err == nil { | ||
| 306 | 306 | t.Fatal("duplicate name was accepted") | |
| 307 | 307 | } | |
| 308 | 308 | if _, err := s.Find(other); err == nil { | |