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/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.