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/ssh/admin.go +37−20

@@ -136,7 +136,11 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
136 136 if a.Admin {
137 137 role = " admin"
138 138 }
139 fmt.Fprintf(out, "%-16s %s %-28s%s keys=%d\n", a.Name, a.ID, a.Status, role, len(a.Keys))
139 display := a.Display
140 if display == "" {
141 display = "-"
142 }
143 fmt.Fprintf(out, "%-16s %s %-24s %-8s%s keys=%d\n", a.Name, a.ID, display, a.Status, role, len(a.Keys))
140 144 for _, k := range a.Keys {
141 145 fmt.Fprintf(out, " %s %s %s\n", k.Fingerprint, k.Type, k.Comment)
142 146 }
@@ -152,23 +156,28 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
152 156 return nil, nil, err
153 157 }
154 158 return userData(a), func(out, _ io.Writer) {
155 fmt.Fprintf(out, "name: %s\nid: %s\nstatus: %s\nadmin: %t\ncreated: %s\n",
156 a.Name, a.ID, a.Status, a.Admin, a.Created.Format(time.RFC3339))
159 display := a.Display
160 if display == "" {
161 display = "-"
162 }
163 fmt.Fprintf(out, "name: %s\ndisplay: %s\nid: %s\nstatus: %s\nadmin: %t\ncreated: %s\n",
164 a.Name, display, a.ID, a.Status, a.Admin, a.Created.Format(time.RFC3339))
157 165 for _, k := range a.Keys {
158 166 fmt.Fprintf(out, "key: %s %s %s\n", k.Fingerprint, k.Type, k.Comment)
159 167 }
160 168 }, nil
161 169
162 170 case "create":
163 f, err := parseAdminFlags(args, adminFlagSpec{long: "no-key", boolean: true})
171 f, err := parseAdminFlags(args, adminFlagSpec{long: "no-key", boolean: true}, adminFlagSpec{long: "display"})
164 172 if err != nil {
165 173 return nil, nil, err
166 174 }
167 175 if len(f.rest) != 1 {
168 return nil, nil, &adminUsageError{msg: "usage: vole-admin user create [--no-key] <name> (public key on stdin unless --no-key)"}
176 return nil, nil, &adminUsageError{msg: "usage: vole-admin user create [--no-key] [--display <label>] <name> (public key on stdin unless --no-key)"}
169 177 }
178 display := f.one("display")
170 179 if f.has("no-key") {
171 a, inv, err := s.cfg.Identity.CreateWithInvite(f.rest[0], actor, 0)
180 a, inv, err := s.cfg.Identity.CreateWithInvite(f.rest[0], display, actor, 0)
172 181 if err != nil {
173 182 return nil, nil, err
174 183 }
@@ -182,7 +191,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
182 191 if err != nil {
183 192 return nil, nil, err
184 193 }
185 a, err := s.cfg.Identity.Create(f.rest[0], key, actor)
194 a, err := s.cfg.Identity.Create(f.rest[0], display, key, actor)
186 195 if err != nil {
187 196 return nil, nil, err
188 197 }
@@ -341,16 +350,22 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
341 350 }, nil
342 351
343 352 case "rename":
353 return nil, nil, &adminUsageError{msg: "account names are permanent; use vole-admin user display <name|id> <display-name>"}
354
355 case "display":
344 356 if len(args) != 2 {
345 return nil, nil, &adminUsageError{msg: "usage: vole-admin user rename <name|id> <new-name>"}
357 return nil, nil, &adminUsageError{msg: "usage: vole-admin user display <name|id> <display-name>"}
346 358 }
347 a, err := s.cfg.Identity.Rename(args[0], args[1], actor)
359 a, err := s.cfg.Identity.SetDisplay(args[0], args[1], actor)
348 360 if err != nil {
349 361 return nil, nil, err
350 362 }
351 return userData(a), func(out, errOut io.Writer) {
352 fmt.Fprintf(out, "%s\t%s\n", a.Name, a.ID)
353 fmt.Fprintln(errOut, "ACL entries and approvals refer to this id and are unaffected")
363 return userData(a), func(out, _ io.Writer) {
364 if a.Display == "" {
365 fmt.Fprintf(out, "cleared the display name for %s; showing %s\n", a.Name, a.Name)
366 } else {
367 fmt.Fprintf(out, "%s now shows as %s\n", a.Name, a.Display)
368 }
354 369 }, nil
355 370
356 371 case "retire", "rm", "delete":
@@ -685,16 +700,18 @@ func (s *Server) aclResult(m *repo.Meta, a *access.ACL) (any, adminRender, error
685 700 entries := make([]map[string]string, 0, len(a.Entries))
686 701 for _, principal := range a.Principals() {
687 702 entries = append(entries, map[string]string{
688 "id": principal,
689 "name": s.cfg.Identity.Name(principal),
690 "role": a.Entries[principal],
703 "id": principal,
704 "name": s.cfg.Identity.Name(principal),
705 "display": s.cfg.Identity.Label(principal),
706 "role": a.Entries[principal],
691 707 })
692 708 }
693 709 runners := make([]map[string]string, 0, len(a.Runners))
694 710 for _, runner := range a.Runners {
695 711 runners = append(runners, map[string]string{
696 "id": runner,
697 "name": s.cfg.Identity.Name(runner),
712 "id": runner,
713 "name": s.cfg.Identity.Name(runner),
714 "display": s.cfg.Identity.Label(runner),
698 715 })
699 716 }
700 717 data := map[string]any{
@@ -715,7 +732,7 @@ func (s *Server) aclResult(m *repo.Meta, a *access.ACL) (any, adminRender, error
715 732 fmt.Fprintf(out, "repo: %s (%s)\n", m.Name, vis)
716 733 fmt.Fprintf(out, "default: %s\n", a.Default)
717 734 for _, e := range entries {
718 fmt.Fprintf(out, " %-16s %-28s %s\n", e["name"], e["id"], e["role"])
735 fmt.Fprintf(out, " %-16s %-28s %s\n", e["display"], e["id"], e["role"])
719 736 }
720 737 if len(a.Protected) > 0 {
721 738 fmt.Fprintf(out, "protected: %s\n", strings.Join(a.Protected, ", "))
@@ -731,7 +748,7 @@ func (s *Server) aclResult(m *repo.Meta, a *access.ACL) (any, adminRender, error
731 748 return
732 749 }
733 750 for _, r := range runners {
734 fmt.Fprintf(out, "runner %-28s %s\n", r["id"], r["name"])
751 fmt.Fprintf(out, "runner %-28s %s\n", r["id"], r["display"])
735 752 }
736 753 fmt.Fprintln(out, "(admins may also report CI results)")
737 754 }, nil
@@ -775,9 +792,9 @@ func userData(a *identity.Account) map[string]any {
775 792 return map[string]any{
776 793 "id": a.ID,
777 794 "name": a.Name,
795 "display": a.Display,
778 796 "status": a.Status,
779 797 "admin": a.Admin,
780 "former": a.Former,
781 798 "keys": a.Keys,
782 799 "created": a.Created,
783 800 }