Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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" +