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/integration_test.go +22−20
| @@ -110,7 +110,7 @@ func newFixtureWith(t *testing.T, tweak func(*config.Config)) *fixture { | |||
| 110 | 110 | if err != nil { | |
| 111 | 111 | t.Fatal(err) | |
| 112 | 112 | } | |
| 113 | owner, err := ident.Create("keenan", pub, "test") | ||
| 113 | owner, err := ident.Create("keenan", "", pub, "test") | ||
| 114 | 114 | if err != nil { | |
| 115 | 115 | t.Fatal(err) | |
| 116 | 116 | } | |
| @@ -171,7 +171,7 @@ func keypair(t *testing.T, path string) (pubLine, privPath string) { | |||
| 171 | 171 | func (f *fixture) addWriter(t *testing.T) string { | |
| 172 | 172 | t.Helper() | |
| 173 | 173 | pub, priv := keypair(t, filepath.Join(f.cfg.DataDir, "id_writer")) | |
| 174 | writer, err := f.srv.Identity.Create("wilbur", pub, "test") | ||
| 174 | writer, err := f.srv.Identity.Create("wilbur", "", pub, "test") | ||
| 175 | 175 | if err != nil { | |
| 176 | 176 | t.Fatal(err) | |
| 177 | 177 | } | |
| @@ -2482,11 +2482,11 @@ func TestApprovalDeletionIsOwnerOrAdmin(t *testing.T) { | |||
| 2482 | 2482 | } | |
| 2483 | 2483 | } | |
| 2484 | 2484 | ||
| 2485 | // TestRenameKeepsAccessAndApprovalOwnership is the whole point of putting a | ||
| 2486 | // stable id on the account: ACL entries and approval files hold the id, so | ||
| 2487 | // changing a person's name must not change what they may do, nor who owns their | ||
| 2488 | // past approvals. | ||
| 2489 | func TestRenameKeepsAccessAndApprovalOwnership(t *testing.T) { | ||
| 2485 | // TestDisplayNameChangeKeepsAccessAndApprovalOwnership: ACL entries and approval | ||
| 2486 | // files hold the account id, metadata authors hold the fixed account name, and a | ||
| 2487 | // display name is only a label — so relabelling a person must not change what | ||
| 2488 | // they may do, who owns their past approvals, or whose name their comments carry. | ||
| 2489 | func TestDisplayNameChangeKeepsAccessAndApprovalOwnership(t *testing.T) { | ||
| 2490 | 2490 | t.Parallel() | |
| 2491 | 2491 | f := newFixture(t) | |
| 2492 | 2492 | writerKey := f.addWriter(t) | |
| @@ -2512,25 +2512,27 @@ func TestRenameKeepsAccessAndApprovalOwnership(t *testing.T) { | |||
| 2512 | 2512 | t.Fatalf("approve: %v", err) | |
| 2513 | 2513 | } | |
| 2514 | 2514 | ||
| 2515 | renamed, err := f.srv.Identity.Rename("wilbur", "robert", "test") | ||
| 2516 | if err != nil { | ||
| 2515 | if _, err := f.srv.Identity.SetDisplay("wilbur", "Wilbur Wright", "test"); err != nil { | ||
| 2517 | 2516 | t.Fatal(err) | |
| 2518 | 2517 | } | |
| 2519 | if renamed.ID != f.writerID { | ||
| 2520 | t.Fatalf("rename changed the account id: %s -> %s", f.writerID, renamed.ID) | ||
| 2518 | if got := f.srv.Identity.Label(f.writerID); got != "Wilbur Wright" { | ||
| 2519 | t.Fatalf("label = %q, want the display name", got) | ||
| 2520 | } | ||
| 2521 | if got := f.srv.Identity.Name(f.writerID); got != "wilbur" { | ||
| 2522 | t.Fatalf("name changed to %q; the name is not a display label", got) | ||
| 2521 | 2523 | } | |
| 2522 | 2524 | ||
| 2523 | // Access follows the id: the same key still has write, with a new name. | ||
| 2525 | // Access follows the id: the same key still has write, under the same name. | ||
| 2524 | 2526 | issue := f.newMetaRepo(t) | |
| 2525 | 2527 | issue.key = writerKey | |
| 2526 | 2528 | if err := issue.apply(map[string]string{ | |
| 2527 | "issues/01M5JR2JK29KKMAA1T9MDNTZ01/issue.md": issueFile("after rename", "robert", "open", "2026-10-22T22:00:00Z", nil, ""), | ||
| 2528 | }, "issue after rename"); err != nil { | ||
| 2529 | t.Fatalf("the renamed account lost its access: %v", err) | ||
| 2529 | "issues/01M5JR2JK29KKMAA1T9MDNTZ01/issue.md": issueFile("after display change", "wilbur", "open", "2026-10-22T22:00:00Z", nil, ""), | ||
| 2530 | }, "issue after display change"); err != nil { | ||
| 2531 | t.Fatalf("the account lost its access: %v", err) | ||
| 2530 | 2532 | } | |
| 2531 | 2533 | ||
| 2532 | // Ownership follows the id too: robert may withdraw the approval he wrote as | ||
| 2533 | // wilbur, because the file is named after the account, not the person. | ||
| 2534 | // Ownership follows the id too: wilbur may withdraw the approval they wrote, | ||
| 2535 | // because the file is named after the account, not the display name. | ||
| 2534 | 2536 | withdraw := f.newMetaRepo(t) | |
| 2535 | 2537 | withdraw.key = writerKey | |
| 2536 | 2538 | withdraw.sync() | |
| @@ -2540,11 +2542,11 @@ func TestRenameKeepsAccessAndApprovalOwnership(t *testing.T) { | |||
| 2540 | 2542 | if _, errs, err := runGit(t, withdraw.dir, "add", "-A"); err != nil { | |
| 2541 | 2543 | t.Fatalf("add: %v: %s", err, errs) | |
| 2542 | 2544 | } | |
| 2543 | if _, errs, err := runGit(t, withdraw.dir, "commit", "-qm", "withdraw after rename"); err != nil { | ||
| 2545 | if _, errs, err := runGit(t, withdraw.dir, "commit", "-qm", "withdraw after display change"); err != nil { | ||
| 2544 | 2546 | t.Fatalf("commit: %v: %s", err, errs) | |
| 2545 | 2547 | } | |
| 2546 | 2548 | if _, errs, err := fgit(t, writerKey, withdraw.dir, "push", "origin", "HEAD:refs/heads/vole"); err != nil { | |
| 2547 | t.Fatalf("the renamed account could not withdraw its own approval: %s", errs) | ||
| 2549 | t.Fatalf("the account could not withdraw its own approval: %s", errs) | ||
| 2548 | 2550 | } | |
| 2549 | 2551 | } | |
| 2550 | 2552 | ||
| @@ -2564,7 +2566,7 @@ func TestRetiredNameCannotBeReissued(t *testing.T) { | |||
| 2564 | 2566 | t.Fatal(err) | |
| 2565 | 2567 | } | |
| 2566 | 2568 | reissue, _ := keypair(t, filepath.Join(f.cfg.DataDir, "id_reissued")) | |
| 2567 | if _, err := f.srv.Identity.Create("wilbur", reissue, "test"); err == nil { | ||
| 2569 | if _, err := f.srv.Identity.Create("wilbur", "", reissue, "test"); err == nil { | ||
| 2568 | 2570 | t.Fatal("the retired name was reissued") | |
| 2569 | 2571 | } | |
| 2570 | 2572 | // And the retired key no longer authenticates. | |