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/ci.go +3−3
| @@ -69,7 +69,7 @@ func (s *Server) enqueue(ev runs.Event) { | |||
| 69 | 69 | // A contributor branch is untrusted code. It does not reach a runner until a | |
| 70 | 70 | // writer has allowed this exact tip; the approval is keyed by SHA, so a new | |
| 71 | 71 | // push is a new decision. This check covers both the spool and reconciliation. | |
| 72 | if _, contributorBranch := access.ContributorRefAccount(ev.Ref); contributorBranch { | ||
| 72 | if _, contributorBranch := access.ContributorRefName(ev.Ref); contributorBranch { | ||
| 73 | 73 | allowed, err := s.Runs.CIAllowed(ev.Repo, ev.Ref, ev.New) | |
| 74 | 74 | if err != nil { | |
| 75 | 75 | s.Log.Warn("ci: cannot read contributor approval", "repo", ev.Repo, "ref", ev.Ref, "err", err) | |
| @@ -244,7 +244,7 @@ func (s *Server) reconcileRefs() { | |||
| 244 | 244 | // A contributor tip without an approval is not owed. Leave the cursor | |
| 245 | 245 | // untouched: the allow action queues the tip, and if that enqueue hits a | |
| 246 | 246 | // transient error the next reconciliation can retry it. | |
| 247 | if _, contributorBranch := access.ContributorRefAccount(ref); contributorBranch { | ||
| 247 | if _, contributorBranch := access.ContributorRefName(ref); contributorBranch { | ||
| 248 | 248 | allowed, err := s.Runs.CIAllowed(m.Name, ref, tip) | |
| 249 | 249 | if err != nil { | |
| 250 | 250 | s.Log.Warn("ci: cannot read contributor approval", "repo", m.Name, "ref", ref, "err", err) | |
| @@ -357,7 +357,7 @@ func (s *Server) watchLeases(ctx context.Context) { | |||
| 357 | 357 | // still the contributor branch tip that was approved, records the decision, and | |
| 358 | 358 | // queues it. The approval is keyed by SHA, so the next push waits again. | |
| 359 | 359 | func (s *Server) allowContributorCI(repo, ref, sha, approver string) error { | |
| 360 | if _, ok := access.ContributorRefAccount(ref); !ok { | ||
| 360 | if _, ok := access.ContributorRefName(ref); !ok { | ||
| 361 | 361 | return fmt.Errorf("ci: %s is not a contributor branch", ref) | |
| 362 | 362 | } | |
| 363 | 363 | m, err := s.Repos.Open(repo) | |