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.
Show which branch a change request came from
8e1005eb2c104934adef950c5afc090d4025ce06 · Keenan Nemetz · 13d ago · parent b7ac1a00
Show which branch a change request came from A request records the exact commit under review, and the page showed only that: `main ← 43f98bcd`. The branch the work is on is how people talk about a change, fetch it, and find it after it lands, and it was being thrown away when the request was opened — the web form resolved the chosen branch to a commit and kept only the commit. `request.md` now records `head_ref:` beside `head:`. It is a display hint and nothing else: approvals, checks, the diff and the landing gate still name the commit, and a test pins that a branch name cannot authorise a different tip. The request page, the request list and `vole change show` show it, and the page marks the branch as moved or deleted when it no longer points at the reviewed commit, so the name reads as provenance rather than as a claim about what is being reviewed. It is optional on read: records written before it existed still load, and a request opened from a bare commit id records no branch.
metadata/requests.go +37−5
| @@ -46,11 +46,17 @@ var ( | |||
| 46 | 46 | ) | |
| 47 | 47 | ||
| 48 | 48 | type Request struct { | |
| 49 | ID string | ||
| 50 | Title string | ||
| 51 | Author string // account name: what the approval gate compares against | ||
| 52 | Base string // branch name, e.g. "main" | ||
| 53 | Head string // commit sha under review | ||
| 49 | ID string | ||
| 50 | Title string | ||
| 51 | Author string // account name: what the approval gate compares against | ||
| 52 | Base string // branch name, e.g. "main" | ||
| 53 | Head string // commit sha under review | ||
| 54 | // HeadRef is the branch the head was taken from when the request was opened, | ||
| 55 | // e.g. "contrib/alice/fix". It is a display hint, not part of the review: the | ||
| 56 | // commit in Head is what approvals, checks, the diff and the landing gate name. | ||
| 57 | // It is empty for requests opened from a bare commit, for older records, and it | ||
| 58 | // may no longer point at Head if the branch has moved or been deleted. | ||
| 59 | HeadRef string | ||
| 54 | 60 | State string // open | merged | closed | |
| 55 | 61 | Created time.Time | |
| 56 | 62 | Description string | |
| @@ -109,6 +115,7 @@ type requestMeta struct { | |||
| 109 | 115 | Author string `yaml:"author"` | |
| 110 | 116 | Base string `yaml:"base"` | |
| 111 | 117 | Head string `yaml:"head"` | |
| 118 | HeadRef string `yaml:"head_ref"` | ||
| 112 | 119 | State string `yaml:"state"` | |
| 113 | 120 | Closes []string `yaml:"closes"` | |
| 114 | 121 | Created string `yaml:"created"` | |
| @@ -256,6 +263,19 @@ func applyRequest(req *Request, raw, path string, problems *[]string) { | |||
| 256 | 263 | if req.Head != "" && !shaRE.MatchString(req.Head) { | |
| 257 | 264 | *problems = append(*problems, fmt.Sprintf("%s: head must be a full 40-character commit id, got %q", path, req.Head)) | |
| 258 | 265 | } | |
| 266 | // The branch is a display hint, and a branch with no commit behind it says | ||
| 267 | // nothing: the commit is what the request is about. | ||
| 268 | req.HeadRef = strings.TrimSpace(m.HeadRef) | ||
| 269 | if req.HeadRef != "" { | ||
| 270 | switch { | ||
| 271 | case !validHeadRef(req.HeadRef): | ||
| 272 | *problems = append(*problems, fmt.Sprintf( | ||
| 273 | "%s: head_ref must be a branch name, got %q", path, req.HeadRef)) | ||
| 274 | case req.Head == "": | ||
| 275 | *problems = append(*problems, fmt.Sprintf( | ||
| 276 | "%s: head_ref names a branch, so head must name the commit it pointed at", path)) | ||
| 277 | } | ||
| 278 | } | ||
| 259 | 279 | if m.State == "" { | |
| 260 | 280 | m.State = "open" | |
| 261 | 281 | } | |
| @@ -318,6 +338,18 @@ func applyRequest(req *Request, raw, path string, problems *[]string) { | |||
| 318 | 338 | req.Description = body | |
| 319 | 339 | } | |
| 320 | 340 | ||
| 341 | // validHeadRef is the file-format check for head_ref: one line, no spaces, and not a | ||
| 342 | // full ref. It is deliberately looser than git's ref rules because the branch the | ||
| 343 | // request names may not exist yet on the reader's side, and because git is the | ||
| 344 | // authority where the branch does exist (Repo.ValidBranch at the writer). A reader | ||
| 345 | // that refused a name git accepts would reject a request that was written correctly. | ||
| 346 | func validHeadRef(name string) bool { | ||
| 347 | if name == "" || strings.HasPrefix(name, "refs/") || strings.ContainsAny(name, " \t") { | ||
| 348 | return false | ||
| 349 | } | ||
| 350 | return ValidText(name) == nil | ||
| 351 | } | ||
| 352 | |||
| 321 | 353 | // parseCloses validates and normalises the issues a request closes: full ids, no | |
| 322 | 354 | // duplicates, sorted, so two writers that link the same issues render the same file. | |
| 323 | 355 | func parseCloses(raw []string, path string) ([]string, []string) { | |