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/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/access/acl_test.go +15−9

@@ -103,16 +103,15 @@ func TestCapabilitiesFollowTheRole(t *testing.T) {
103 103
104 104 func TestContributorRefNamespace(t *testing.T) {
105 105 t.Parallel()
106 a := &ACL{Entries: map[string]string{"alice": "contributor"}}
107 106 own := "refs/heads/contrib/alice/topic"
108 if id, ok := ContributorRefAccount(own); !ok || id != "alice" {
109 t.Fatalf("ContributorRefAccount(%q)=%q,%v", own, id, ok)
107 if name, ok := ContributorRefName(own); !ok || name != "alice" {
108 t.Fatalf("ContributorRefName(%q)=%q,%v", own, name, ok)
110 109 }
111 if _, ok := ContributorRefAccount("refs/heads/contrib/alice"); ok {
110 if _, ok := ContributorRefName("refs/heads/contrib/alice"); ok {
112 111 t.Fatal("an empty contributor namespace parsed as a branch")
113 112 }
114 if !a.CanPushRef("alice", own) {
115 t.Fatalf("contributor cannot push %s", own)
113 if !IsContributorRef("alice", own) {
114 t.Fatalf("alice cannot push %s", own)
116 115 }
117 116 for _, ref := range []string{
118 117 "refs/heads/main",
@@ -121,13 +120,20 @@ func TestContributorRefNamespace(t *testing.T) {
121 120 "refs/tags/contrib/alice/v1",
122 121 "refs/heads/vole",
123 122 } {
124 if a.CanPushRef("alice", ref) {
125 t.Errorf("contributor was allowed to push %s", ref)
123 if IsContributorRef("alice", ref) {
124 t.Errorf("alice was allowed to push %s", ref)
126 125 }
127 126 }
128 if a.CanPushRef("bob", own) {
127 if IsContributorRef("bob", own) {
129 128 t.Fatal("a different account could push alice's namespace")
130 129 }
130 // A name is a whole path component: alice's namespace is not alice-b's.
131 if IsContributorRef("alice", "refs/heads/contrib/alice-b/topic") {
132 t.Fatal("the namespace prefix matched a longer account name")
133 }
134 if got := ContributorBranchPrefix("alice"); got != "refs/heads/contrib/alice/" {
135 t.Fatalf("ContributorBranchPrefix = %q", got)
136 }
131 137 }
132 138
133 139 func TestDefaultAppliesToUnknownUsers(t *testing.T) {