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.
Identify accounts by their registered name
62576fa8157d431dcf2d360f3ebe481de77bda03 · Keenan Nemetz · 14d ago · parent 642e5994
Identify accounts by their registered name Drop the internal account id and display name. Users, keys, passkeys, invites, sessions, ACL entries, approvals, locks and CI records now all use the registered name as the single identity. The roster migrates in place: old id-keyed tables are rebuilt around the name, and access-rule/lock references are rewritten through the same mapping. Display-name editing and whoami --id are removed.
metadata/requests.go +13−18
| @@ -27,7 +27,6 @@ import ( | |||
| 27 | 27 | "time" | |
| 28 | 28 | ||
| 29 | 29 | "github.com/rodent-software/vole/core/git" | |
| 30 | "github.com/rodent-software/vole/core/id" | ||
| 31 | 30 | ) | |
| 32 | 31 | ||
| 33 | 32 | const ( | |
| @@ -39,15 +38,14 @@ const ( | |||
| 39 | 38 | ||
| 40 | 39 | var ( | |
| 41 | 40 | shaRE = regexp.MustCompile(`^[0-9a-f]{40}$`) | |
| 42 | requestPathRE = regexp.MustCompile(`^requests/([0-9A-HJKMNP-TV-Z]{26})(?:/(request\.md)|/approvals/([0-9A-HJKMNP-TV-Z]{26})\.md|/comments/([0-9A-HJKMNP-TV-Z]{26})\.md)$`) | ||
| 41 | requestPathRE = regexp.MustCompile(`^requests/([0-9A-HJKMNP-TV-Z]{26})(?:/(request\.md)|/approvals/([a-z0-9][a-z0-9_-]{1,31})\.md|/comments/([0-9A-HJKMNP-TV-Z]{26})\.md)$`) | ||
| 43 | 42 | branchNameRE = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,62}$`) | |
| 44 | 43 | ) | |
| 45 | 44 | ||
| 46 | 45 | type Request struct { | |
| 47 | 46 | ID string | |
| 48 | 47 | Title string | |
| 49 | Author string // account name at the time of writing; labels are resolved for display | ||
| 50 | AuthorID string // account id: what the approval gate compares against | ||
| 48 | Author string // account name: what the approval gate compares against | ||
| 51 | 49 | Base string // branch name, e.g. "main" | |
| 52 | 50 | Head string // commit sha under review | |
| 53 | 51 | State string // open | merged | closed | |
| @@ -86,13 +84,12 @@ func (r *Request) ApprovedBy(principal string) bool { | |||
| 86 | 84 | // meta is the frontmatter head of request.md. Field names are part of the | |
| 87 | 85 | // public format. There is no updated field: the commit carries the time. | |
| 88 | 86 | type requestMeta struct { | |
| 89 | Title string `yaml:"title"` | ||
| 90 | Author string `yaml:"author"` | ||
| 91 | AuthorID string `yaml:"author_id"` | ||
| 92 | Base string `yaml:"base"` | ||
| 93 | Head string `yaml:"head"` | ||
| 94 | State string `yaml:"state"` | ||
| 95 | Created string `yaml:"created"` | ||
| 87 | Title string `yaml:"title"` | ||
| 88 | Author string `yaml:"author"` | ||
| 89 | Base string `yaml:"base"` | ||
| 90 | Head string `yaml:"head"` | ||
| 91 | State string `yaml:"state"` | ||
| 92 | Created string `yaml:"created"` | ||
| 96 | 93 | } | |
| 97 | 94 | ||
| 98 | 95 | // Load reads and validates every request on the metadata branch. | |
| @@ -179,7 +176,7 @@ func ParseRequests(contents map[string]string) ([]*Request, error) { | |||
| 179 | 176 | // Self-approval is not an approval. Catching it here rather than at the | |
| 180 | 177 | // gate means the author learns immediately instead of after a rejection. | |
| 181 | 178 | for _, a := range req.Approvals { | |
| 182 | if a.Principal == req.AuthorID { | ||
| 179 | if a.Principal == req.Author { | ||
| 183 | 180 | problems = append(problems, fmt.Sprintf( | |
| 184 | 181 | "%s/%s/%s/%s.md: the author cannot approve their own request", RequestsDir, req.ID, approvalDir, a.Principal)) | |
| 185 | 182 | } | |
| @@ -221,11 +218,9 @@ func applyRequest(req *Request, raw, path string, problems *[]string) { | |||
| 221 | 218 | req.Author = strings.TrimSpace(m.Author) | |
| 222 | 219 | if req.Author == "" { | |
| 223 | 220 | *problems = append(*problems, fmt.Sprintf("%s: author is required", path)) | |
| 224 | } | ||
| 225 | req.AuthorID = strings.TrimSpace(m.AuthorID) | ||
| 226 | if !id.Valid(req.AuthorID) { | ||
| 221 | } else if !assigneeRE.MatchString(req.Author) { | ||
| 227 | 222 | *problems = append(*problems, fmt.Sprintf( | |
| 228 | "%s: author_id must be the author's account id (a 26-character ULID), not their name; it is what blocks self-approval", path)) | ||
| 223 | "%s: author %q is not an account name", path, req.Author)) | ||
| 229 | 224 | } | |
| 230 | 225 | req.Base = strings.TrimSpace(m.Base) | |
| 231 | 226 | if !branchNameRE.MatchString(req.Base) { | |
| @@ -256,8 +251,8 @@ func applyRequest(req *Request, raw, path string, problems *[]string) { | |||
| 256 | 251 | ||
| 257 | 252 | func parseApproval(principal, raw, path string) (Approval, string) { | |
| 258 | 253 | a := Approval{Principal: principal} | |
| 259 | if !id.Valid(principal) { | ||
| 260 | return a, fmt.Sprintf("%s: the file name must be the approver's account id (a 26-character ULID)", path) | ||
| 254 | if !assigneeRE.MatchString(principal) { | ||
| 255 | return a, fmt.Sprintf("%s: the file name must be the approver's account name", path) | ||
| 261 | 256 | } | |
| 262 | 257 | head, _, err := Split([]byte(raw)) | |
| 263 | 258 | if err != nil { | |