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.
Store changes under changes/<ulid>/change.md
b492b557f5cc5633b3dcb21fdbb5e2ad98d0bc16 · Keenan Nemetz · 3d ago · parent 9bce3723
Store changes under changes/<ulid>/change.md The web, the CLI, and the docs call these items changes; only the metadata tree still said requests. Rename the directory and the record file so the format and the product use one word. The tree is a contract, so this is a format change rather than a display change: the top-level layout stays closed, and a request path no longer validates. An existing repository is migrated by rewriting its vole branch, which is an audited admin force-push; the index rebuilds from the new tip and the #N aliases keep their ULIDs because the rewrite preserves commit order. Refs #202 (01M6S3JC3Y2KA6EFJK6APX7A1T).
metadata/requests_test.go +20−20
| @@ -41,10 +41,10 @@ const ( | |||
| 41 | 41 | func TestParseReadsTheDiscussion(t *testing.T) { | |
| 42 | 42 | t.Parallel() | |
| 43 | 43 | list, err := ParseRequests(map[string]string{ | |
| 44 | RequestsDir + "/" + reqIDA + "/request.md": requestBody(headSHA), | ||
| 45 | RequestsDir + "/" + reqIDA + "/comments/" + reqIDB + ".md": "---\nauthor: wilbur\ncreated: 2026-10-22T23:00:00Z\n---\nOne question.\n", | ||
| 46 | RequestsDir + "/" + reqIDA + "/approvals/wilbur.md": "---\nbase: main\nhead: " + headSHA + "\ncreated: 2026-10-22T23:10:00Z\n---\n", | ||
| 47 | RequestsDir + "/" + reqIDA + "/comments/01M5JR2JK29KKMAA1T9MDNTRBZ.md": "---\nauthor: keenan\ncreated: 2026-10-22T23:05:00Z\n---\nEarlier.\n", | ||
| 44 | ChangesDir + "/" + reqIDA + "/change.md": requestBody(headSHA), | ||
| 45 | ChangesDir + "/" + reqIDA + "/comments/" + reqIDB + ".md": "---\nauthor: wilbur\ncreated: 2026-10-22T23:00:00Z\n---\nOne question.\n", | ||
| 46 | ChangesDir + "/" + reqIDA + "/approvals/wilbur.md": "---\nbase: main\nhead: " + headSHA + "\ncreated: 2026-10-22T23:10:00Z\n---\n", | ||
| 47 | ChangesDir + "/" + reqIDA + "/comments/01M5JR2JK29KKMAA1T9MDNTRBZ.md": "---\nauthor: keenan\ncreated: 2026-10-22T23:05:00Z\n---\nEarlier.\n", | ||
| 48 | 48 | }) | |
| 49 | 49 | if err != nil { | |
| 50 | 50 | t.Fatalf("unexpected error: %v", err) | |
| @@ -79,8 +79,8 @@ func TestCommentNeedsAnAuthorAndATime(t *testing.T) { | |||
| 79 | 79 | } | |
| 80 | 80 | for _, c := range cases { | |
| 81 | 81 | _, err := ParseRequests(map[string]string{ | |
| 82 | RequestsDir + "/" + reqIDA + "/request.md": requestBody(headSHA), | ||
| 83 | RequestsDir + "/" + reqIDA + "/comments/" + reqIDB + ".md": c.body, | ||
| 82 | ChangesDir + "/" + reqIDA + "/change.md": requestBody(headSHA), | ||
| 83 | ChangesDir + "/" + reqIDA + "/comments/" + reqIDB + ".md": c.body, | ||
| 84 | 84 | }) | |
| 85 | 85 | ve, ok := err.(*ValidationError) | |
| 86 | 86 | if !ok { | |
| @@ -99,15 +99,15 @@ func TestCommentNeedsAnAuthorAndATime(t *testing.T) { | |||
| 99 | 99 | func TestUnknownFileInARequestIsRefused(t *testing.T) { | |
| 100 | 100 | t.Parallel() | |
| 101 | 101 | _, err := ParseRequests(map[string]string{ | |
| 102 | RequestsDir + "/" + reqIDA + "/request.md": requestBody(headSHA), | ||
| 103 | RequestsDir + "/" + reqIDA + "/review.md": "---\nauthor: wilbur\ncreated: 2026-10-22T23:00:00Z\n---\nnote\n", | ||
| 102 | ChangesDir + "/" + reqIDA + "/change.md": requestBody(headSHA), | ||
| 103 | ChangesDir + "/" + reqIDA + "/review.md": "---\nauthor: wilbur\ncreated: 2026-10-22T23:00:00Z\n---\nnote\n", | ||
| 104 | 104 | }) | |
| 105 | 105 | ve, ok := err.(*ValidationError) | |
| 106 | 106 | if !ok { | |
| 107 | 107 | t.Fatalf("accepted: %v", err) | |
| 108 | 108 | } | |
| 109 | 109 | joined := strings.Join(ve.Problems, "; ") | |
| 110 | for _, want := range []string{"review.md", "request.md", "approvals/", "comments/"} { | ||
| 110 | for _, want := range []string{"review.md", "change.md", "approvals/", "comments/"} { | ||
| 111 | 111 | if !strings.Contains(joined, want) { | |
| 112 | 112 | t.Fatalf("refusal does not mention %q: %v", want, ve.Problems) | |
| 113 | 113 | } | |
| @@ -117,15 +117,15 @@ func TestUnknownFileInARequestIsRefused(t *testing.T) { | |||
| 117 | 117 | func TestCommentPaths(t *testing.T) { | |
| 118 | 118 | t.Parallel() | |
| 119 | 119 | path := RequestCommentPath(reqIDA, reqIDB) | |
| 120 | if path != "requests/"+reqIDA+"/comments/"+reqIDB+".md" { | ||
| 120 | if path != "changes/"+reqIDA+"/comments/"+reqIDB+".md" { | ||
| 121 | 121 | t.Fatalf("path = %q", path) | |
| 122 | 122 | } | |
| 123 | 123 | if !IsRequestCommentPath(path) { | |
| 124 | 124 | t.Fatal("a comment path was not recognised") | |
| 125 | 125 | } | |
| 126 | 126 | for _, other := range []string{ | |
| 127 | "requests/" + reqIDA + "/request.md", | ||
| 128 | "requests/" + reqIDA + "/approvals/" + reqIDB + ".md", | ||
| 127 | "changes/" + reqIDA + "/change.md", | ||
| 128 | "changes/" + reqIDA + "/approvals/" + reqIDB + ".md", | ||
| 129 | 129 | "issues/" + reqIDA + "/comments/" + reqIDB + ".md", | |
| 130 | 130 | } { | |
| 131 | 131 | if IsRequestCommentPath(other) { | |
| @@ -141,9 +141,9 @@ func TestRepliesAllowAnyCommentAsParent(t *testing.T) { | |||
| 141 | 141 | replyID = "01M5JR2JK29KKMAA1T9MDNTRC2" | |
| 142 | 142 | deeperID = "01M5JR2JK29KKMAA1T9MDNTRC3" | |
| 143 | 143 | ) | |
| 144 | requestPath := RequestsDir + "/" + reqIDA + "/request.md" | ||
| 144 | requestPath := ChangesDir + "/" + reqIDA + "/change.md" | ||
| 145 | 145 | commentPath := func(id string) string { | |
| 146 | return RequestsDir + "/" + reqIDA + "/comments/" + id + ".md" | ||
| 146 | return ChangesDir + "/" + reqIDA + "/comments/" + id + ".md" | ||
| 147 | 147 | } | |
| 148 | 148 | root := "---\nauthor: wilbur\ncreated: 2026-10-22T23:00:00Z\n---\nRoot.\n" | |
| 149 | 149 | reply := "---\nauthor: keenan\ncreated: 2026-10-22T23:01:00Z\nreply_to: " + rootID + "\n---\nReply.\n" | |
| @@ -188,7 +188,7 @@ func TestRepliesAllowAnyCommentAsParent(t *testing.T) { | |||
| 188 | 188 | func TestMergedStateNamesTheCommitThatLanded(t *testing.T) { | |
| 189 | 189 | t.Parallel() | |
| 190 | 190 | base := map[string]string{ | |
| 191 | RequestsDir + "/" + reqIDA + "/request.md": "", | ||
| 191 | ChangesDir + "/" + reqIDA + "/change.md": "", | ||
| 192 | 192 | } | |
| 193 | 193 | merged := func(extra string) map[string]string { | |
| 194 | 194 | m := map[string]string{} | |
| @@ -240,7 +240,7 @@ func TestMergedStateNamesTheCommitThatLanded(t *testing.T) { | |||
| 240 | 240 | "merged_base": "merged_base: " + sideSHA + "\n", | |
| 241 | 241 | } { | |
| 242 | 242 | pending := map[string]string{ | |
| 243 | RequestsDir + "/" + reqIDA + "/request.md": "---\n" + | ||
| 243 | ChangesDir + "/" + reqIDA + "/change.md": "---\n" + | ||
| 244 | 244 | `title: "Land the retry"` + "\n" + | |
| 245 | 245 | "state: open\n" + | |
| 246 | 246 | "author: keenan\n" + | |
| @@ -265,7 +265,7 @@ func TestHeadRefIsAHeadingNotAnApproval(t *testing.T) { | |||
| 265 | 265 | t.Parallel() | |
| 266 | 266 | body := func(extra string) map[string]string { | |
| 267 | 267 | return map[string]string{ | |
| 268 | RequestsDir + "/" + reqIDA + "/request.md": "---\n" + | ||
| 268 | ChangesDir + "/" + reqIDA + "/change.md": "---\n" + | ||
| 269 | 269 | `title: "Land the retry"` + "\n" + | |
| 270 | 270 | "state: open\n" + | |
| 271 | 271 | "author: keenan\n" + | |
| @@ -292,7 +292,7 @@ func TestHeadRefIsAHeadingNotAnApproval(t *testing.T) { | |||
| 292 | 292 | } | |
| 293 | 293 | } | |
| 294 | 294 | noHead := map[string]string{ | |
| 295 | RequestsDir + "/" + reqIDA + "/request.md": "---\n" + | ||
| 295 | ChangesDir + "/" + reqIDA + "/change.md": "---\n" + | ||
| 296 | 296 | `title: "Land the retry"` + "\n" + | |
| 297 | 297 | "state: open\n" + | |
| 298 | 298 | "author: keenan\n" + | |
| @@ -316,7 +316,7 @@ func TestHeadRefIsAHeadingNotAnApproval(t *testing.T) { | |||
| 316 | 316 | if err != nil { | |
| 317 | 317 | t.Fatal(err) | |
| 318 | 318 | } | |
| 319 | again, err := ParseRequests(map[string]string{RequestsDir + "/" + req.ID + "/request.md": string(raw)}) | ||
| 319 | again, err := ParseRequests(map[string]string{ChangesDir + "/" + req.ID + "/change.md": string(raw)}) | ||
| 320 | 320 | if err != nil { | |
| 321 | 321 | t.Fatalf("the writer produced a request the reader refuses: %v", err) | |
| 322 | 322 | } | |
| @@ -364,7 +364,7 @@ func TestClosesIsAFullIdList(t *testing.T) { | |||
| 364 | 364 | t.Parallel() | |
| 365 | 365 | body := func(closes string) map[string]string { | |
| 366 | 366 | return map[string]string{ | |
| 367 | RequestsDir + "/" + reqIDA + "/request.md": "---\n" + | ||
| 367 | ChangesDir + "/" + reqIDA + "/change.md": "---\n" + | ||
| 368 | 368 | `title: "Land the retry"` + "\n" + | |
| 369 | 369 | "state: open\n" + | |
| 370 | 370 | "author: keenan\n" + | |