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.
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.
server/web_test.go +103
| @@ -196,6 +196,109 @@ func TestMergedRequestStillShowsWhatLanded(t *testing.T) { | |||
| 196 | 196 | } | |
| 197 | 197 | } | |
| 198 | 198 | ||
| 199 | // TestRequestRecordsTheBranchItCameFrom: a request names the exact commit under | ||
| 200 | // review, and the page shows the commit. The branch it was opened from is worth | ||
| 201 | // showing too — it is how people find and fetch the change — as long as the page | ||
| 202 | // says so when the branch has moved past that commit. A request opened from a bare | ||
| 203 | // commit records no branch, and one written before the field existed still loads. | ||
| 204 | func TestRequestRecordsTheBranchItCameFrom(t *testing.T) { | ||
| 205 | t.Parallel() | ||
| 206 | f := newFixture(t) | ||
| 207 | f.publish(f.repo) | ||
| 208 | |||
| 209 | base := workRepo(t, map[string]string{"readme.md": "v1\n"}) | ||
| 210 | if _, errs, err := f.git(base, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil { | ||
| 211 | t.Fatalf("main push: %s", errs) | ||
| 212 | } | ||
| 213 | feature, head := f.cloneWithChange(t, map[string]string{"readme.md": "v1\nfeature line\n"}, "feature") | ||
| 214 | // The clone works on the default branch; name the pushed branch locally too, so | ||
| 215 | // --head can name a branch the way a contributor would. | ||
| 216 | if _, errs, err := runGit(t, feature, "branch", "feature"); err != nil { | ||
| 217 | t.Fatalf("branch: %s", errs) | ||
| 218 | } | ||
| 219 | if _, errs, err := f.git(feature, "push", f.sshURL(f.repo), "HEAD:refs/heads/feature"); err != nil { | ||
| 220 | t.Fatalf("feature push: %s", errs) | ||
| 221 | } | ||
| 222 | |||
| 223 | out, errs, err := f.cli(feature, "change", "new", "--title", "Land the feature", | ||
| 224 | "--base", "main", "--head", "feature", "-m", "why this change") | ||
| 225 | if err != nil { | ||
| 226 | t.Fatalf("change new: %v\n%s", err, errs) | ||
| 227 | } | ||
| 228 | id := strings.Fields(out)[0] | ||
| 229 | recordPath := "refs/heads/vole:requests/" + id + "/request.md" | ||
| 230 | record, _, err := runGit(t, filepath.Join(f.cfg.ReposDir(), f.repo+".git"), "show", recordPath) | ||
| 231 | if err != nil { | ||
| 232 | t.Fatalf("read the request: %v", err) | ||
| 233 | } | ||
| 234 | for _, want := range []string{"head: " + head, "head_ref: feature"} { | ||
| 235 | if !strings.Contains(record, want) { | ||
| 236 | t.Fatalf("the request is missing %q:\n%s", want, record) | ||
| 237 | } | ||
| 238 | } | ||
| 239 | |||
| 240 | code, body := f.get("/r/" + f.repo + "/requests/" + id) | ||
| 241 | if code != 200 { | ||
| 242 | t.Fatalf("request page = %d", code) | ||
| 243 | } | ||
| 244 | if !strings.Contains(body, ">feature</code>") { | ||
| 245 | t.Fatalf("the request page does not name the branch it came from:\n%s", body) | ||
| 246 | } | ||
| 247 | _, body = f.get("/r/" + f.repo + "/requests") | ||
| 248 | if !strings.Contains(body, ">feature</code>") { | ||
| 249 | t.Fatalf("the request list does not name the branch it came from:\n%s", body) | ||
| 250 | } | ||
| 251 | out, _, err = f.cli(feature, "change", "show", id) | ||
| 252 | if err != nil { | ||
| 253 | t.Fatalf("change show: %v", err) | ||
| 254 | } | ||
| 255 | if !strings.Contains(out, "branch: feature") { | ||
| 256 | t.Fatalf("change show does not name the branch:\n%s", out) | ||
| 257 | } | ||
| 258 | |||
| 259 | // A branch that moves past the reviewed commit is still the branch the request | ||
| 260 | // came from, but the page must not present it as what is being reviewed. | ||
| 261 | if err := os.WriteFile(filepath.Join(feature, "readme.md"), []byte("v1\nfeature line\nand more\n"), 0o644); err != nil { | ||
| 262 | t.Fatal(err) | ||
| 263 | } | ||
| 264 | if _, errs, err := runGit(t, feature, "commit", "-qam", "fixup"); err != nil { | ||
| 265 | t.Fatalf("commit: %s", errs) | ||
| 266 | } | ||
| 267 | if _, errs, err := f.git(feature, "push", f.sshURL(f.repo), "HEAD:refs/heads/feature"); err != nil { | ||
| 268 | t.Fatalf("fixup push: %s", errs) | ||
| 269 | } | ||
| 270 | _, body = f.get("/r/" + f.repo + "/requests/" + id) | ||
| 271 | if !strings.Contains(body, "branch moved") || !strings.Contains(body, "/commit/"+head) { | ||
| 272 | t.Fatalf("a moved branch is not labelled, or the reviewed commit is gone:\n%s", body) | ||
| 273 | } | ||
| 274 | |||
| 275 | // A branch that is gone is still the branch the request came from, and the page | ||
| 276 | // says so instead of dropping the name or pretending it can check it. | ||
| 277 | if _, errs, err := f.git(feature, "push", f.sshURL(f.repo), ":refs/heads/feature"); err != nil { | ||
| 278 | t.Fatalf("delete branch: %s", errs) | ||
| 279 | } | ||
| 280 | _, body = f.get("/r/" + f.repo + "/requests/" + id) | ||
| 281 | if !strings.Contains(body, "branch deleted") || !strings.Contains(body, ">feature</code>") { | ||
| 282 | t.Fatalf("a deleted branch is not labelled, or its name is gone:\n%s", body) | ||
| 283 | } | ||
| 284 | |||
| 285 | // A raw commit is a head with no branch behind it. | ||
| 286 | out, errs, err = f.cli(feature, "change", "new", "--title", "From a commit", | ||
| 287 | "--base", "main", "--head", head, "-m", "why this change") | ||
| 288 | if err != nil { | ||
| 289 | t.Fatalf("change new from a commit: %v\n%s", err, errs) | ||
| 290 | } | ||
| 291 | commitID := strings.Fields(out)[0] | ||
| 292 | record, _, err = runGit(t, filepath.Join(f.cfg.ReposDir(), f.repo+".git"), | ||
| 293 | "show", "refs/heads/vole:requests/"+commitID+"/request.md") | ||
| 294 | if err != nil { | ||
| 295 | t.Fatalf("read the commit request: %v", err) | ||
| 296 | } | ||
| 297 | if strings.Contains(record, "head_ref:") { | ||
| 298 | t.Fatalf("a request opened from a bare commit recorded a branch:\n%s", record) | ||
| 299 | } | ||
| 300 | } | ||
| 301 | |||
| 199 | 302 | // TestCommitPageEscapesFileContent matters because the web renders other people's | |
| 200 | 303 | // commits: a diff that executes a tag is an XSS, not a syntax error. | |
| 201 | 304 | func TestCommitPageEscapesFileContent(t *testing.T) { | |