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 | } | |