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.
Issues and requests are frontmatter, not JSON
307cbc2aa624ca9b76b79849e8ce8926414be435 · Keenan Nemetz · 17d ago · parent 427c4761
Issues and requests are frontmatter, not JSON
issues/<ulid>/issue.md and requests/<ulid>/request.md replace meta.json plus
description.md, and approvals/<account>.md replaces approvals/<account>.json.
Every file on refs/heads/vole is now frontmatter markdown read by one package, so
there is one reader and one writer for issues, requests, comments and approvals
instead of a JSON path and a hand-rolled key/value path.
The diff is the interface. A JSON array rewrites its neighbour to add a comma, so
one label cost two changed lines and removing one cost three; a block sequence
costs one. docs/spikes/issue-frontmatter measured the same six edits at 25 changed
lines as meta.json and 10 as frontmatter, and one file per entity means one path
to diff, blame and log.
Three rules make YAML safe to hand-edit rather than something to police:
- the writer always double-quotes free text, so no indicator, reserved word or
" # " can change what a title means
- a label is quoted only when the parser would not read it back bare ("null"),
which leaves ordinary labels as they were
- a reader refuses an unquoted title that the parser read differently: "title:
fix #12 crash" would otherwise be stored as "fix" and nobody would find out
Control characters are refused on both sides, so a tree the server accepts is a
tree it can always rewrite.
`updated` is gone from issues, requests and the index's issues table. It cost a
changed line on every edit — 11 of the 21 frontmatter lines in the spike's
sequence — and the commit that changed the file already carries the time.
Verified: TestRenderParseRoundTrip puts 13 titles that break naive YAML (hash,
colon, leading dash, null, quotes, backslash, unicode, 190 bytes) through the
writer and the reader; the server tests push a rendered issue over SSH and render
it, and the approval-ownership tests withdraw approvals/<account>.md. Then
scripts/ci.sh green (gofmt, vet, build, markdown links, go test -race ./...).
internal/server/integration_test.go +85−52
| @@ -26,6 +26,7 @@ import ( | |||
| 26 | 26 | ||
| 27 | 27 | "github.com/rodent-software/vole/internal/acl" | |
| 28 | 28 | "github.com/rodent-software/vole/internal/config" | |
| 29 | "github.com/rodent-software/vole/internal/frontmatter" | ||
| 29 | 30 | "github.com/rodent-software/vole/internal/identity" | |
| 30 | 31 | "github.com/rodent-software/vole/internal/index" | |
| 31 | 32 | "github.com/rodent-software/vole/internal/repo" | |
| @@ -516,9 +517,7 @@ func (f *fixture) issuesRepo(t *testing.T, files map[string]string) string { | |||
| 516 | 517 | ||
| 517 | 518 | func issueFiles(id, title, desc string) map[string]string { | |
| 518 | 519 | return map[string]string{ | |
| 519 | "issues/" + id + "/meta.json": fmt.Sprintf( | ||
| 520 | `{"title":%q,"state":"open","author":"keenan","created":"2026-10-22T21:00:00Z","labels":["bug"]}`, title), | ||
| 521 | "issues/" + id + "/description.md": desc, | ||
| 520 | "issues/" + id + "/issue.md": issueFile(title, "keenan", "open", "2026-10-22T21:00:00Z", []string{"bug"}, desc), | ||
| 522 | 521 | } | |
| 523 | 522 | } | |
| 524 | 523 | ||
| @@ -806,14 +805,14 @@ func TestRequestCLIResolvesNamesOverSSH(t *testing.T) { | |||
| 806 | 805 | // What landed on the server names the account id, so the name was resolved | |
| 807 | 806 | // from the roster rather than passed through. | |
| 808 | 807 | raw, _, err := runGit(t, filepath.Join(f.cfg.ReposDir(), f.repo+".git"), | |
| 809 | "show", "refs/heads/vole:requests/"+id+"/meta.json") | ||
| 808 | "show", "refs/heads/vole:requests/"+id+"/request.md") | ||
| 810 | 809 | if err != nil { | |
| 811 | 810 | t.Fatalf("read request from the server: %v\n%s", err, raw) | |
| 812 | 811 | } | |
| 813 | if !strings.Contains(raw, `"author": "keenan"`) { | ||
| 812 | if !strings.Contains(raw, `author: keenan`) { | ||
| 814 | 813 | t.Fatalf("request lost the author name:\n%s", raw) | |
| 815 | 814 | } | |
| 816 | if !strings.Contains(raw, `"author_id": "`+f.ownerID+`"`) { | ||
| 815 | if !strings.Contains(raw, `author_id: `+f.ownerID) { | ||
| 817 | 816 | t.Fatalf("request author_id is not the account id %s:\n%s", f.ownerID, raw) | |
| 818 | 817 | } | |
| 819 | 818 | } | |
| @@ -830,15 +829,12 @@ func TestIssueFiltersAndSearch(t *testing.T) { | |||
| 830 | 829 | f := newFixture(t) | |
| 831 | 830 | f.publish(f.repo) | |
| 832 | 831 | files := map[string]string{} | |
| 833 | add := func(id, title, state, body string, labels string) { | ||
| 834 | files["issues/"+id+"/meta.json"] = fmt.Sprintf( | ||
| 835 | `{"title":%q,"state":%q,"author":"keenan","created":"2026-10-22T21:00:00Z","labels":%s}`, | ||
| 836 | title, state, labels) | ||
| 837 | files["issues/"+id+"/description.md"] = body | ||
| 832 | add := func(id, title, state, body string, labels []string) { | ||
| 833 | files["issues/"+id+"/issue.md"] = issueFile(title, "keenan", state, "2026-10-22T21:00:00Z", labels, body) | ||
| 838 | 834 | } | |
| 839 | add("01M5JR2JK29KKMAA1T9MDNTPB1", "Segfault on startup", "open", "crash in parser", `["bug"]`) | ||
| 840 | add("01M5JR2JK29KKMAA1T9MDNTPB2", "Docs typo", "closed", "spelling", `["docs"]`) | ||
| 841 | add("01M5JR2JK29KKMAA1T9MDNTPB3", "Slow parser", "open", "throughput", `["bug","perf"]`) | ||
| 835 | add("01M5JR2JK29KKMAA1T9MDNTPB1", "Segfault on startup", "open", "crash in parser", []string{"bug"}) | ||
| 836 | add("01M5JR2JK29KKMAA1T9MDNTPB2", "Docs typo", "closed", "spelling", []string{"docs"}) | ||
| 837 | add("01M5JR2JK29KKMAA1T9MDNTPB3", "Slow parser", "open", "throughput", []string{"bug", "perf"}) | ||
| 842 | 838 | ||
| 843 | 839 | work := f.issuesRepo(t, files) | |
| 844 | 840 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "HEAD:refs/heads/vole"); err != nil { | |
| @@ -1099,11 +1095,8 @@ func writeIssueFile(t *testing.T, dir, id, title string) { | |||
| 1099 | 1095 | if err := os.MkdirAll(p, 0o755); err != nil { | |
| 1100 | 1096 | t.Fatal(err) | |
| 1101 | 1097 | } | |
| 1102 | meta := fmt.Sprintf(`{"title":%q,"state":"open","author":"keenan","created":"2026-10-22T21:00:00Z"}`, title) | ||
| 1103 | if err := os.WriteFile(filepath.Join(p, "meta.json"), []byte(meta), 0o644); err != nil { | ||
| 1104 | t.Fatal(err) | ||
| 1105 | } | ||
| 1106 | if err := os.WriteFile(filepath.Join(p, "description.md"), []byte("body\n"), 0o644); err != nil { | ||
| 1098 | raw := issueFile(title, "keenan", "open", "2026-10-22T21:00:00Z", nil, "body\n") | ||
| 1099 | if err := os.WriteFile(filepath.Join(p, "issue.md"), []byte(raw), 0o644); err != nil { | ||
| 1107 | 1100 | t.Fatal(err) | |
| 1108 | 1101 | } | |
| 1109 | 1102 | } | |
| @@ -1216,13 +1209,54 @@ func (f *fixture) cloneWithChange(t *testing.T, files map[string]string, msg str | |||
| 1216 | 1209 | ||
| 1217 | 1210 | const reqID = "01M5JR2JK29KKMAA1T9MDNTR01" | |
| 1218 | 1211 | ||
| 1212 | // requestFile renders requests/<id>/request.md. The body is the request's | ||
| 1213 | // description, so a fixture cannot drift from the format the reader expects. | ||
| 1214 | func requestFile(base, head, author, authorID, body string) string { | ||
| 1215 | d := frontmatter.New() | ||
| 1216 | d.Text("title", "Land the feature") | ||
| 1217 | d.Word("state", "open") | ||
| 1218 | d.Word("author", author) | ||
| 1219 | d.Word("author_id", authorID) | ||
| 1220 | d.Word("base", base) | ||
| 1221 | d.Word("head", head) | ||
| 1222 | d.Word("created", "2026-10-22T22:00:00Z") | ||
| 1223 | out, err := d.Bytes(body) | ||
| 1224 | if err != nil { | ||
| 1225 | panic(err) | ||
| 1226 | } | ||
| 1227 | return string(out) | ||
| 1228 | } | ||
| 1229 | |||
| 1230 | // requestMeta is requestFile with no description: the common fixture shape. | ||
| 1219 | 1231 | func requestMeta(base, head, author, authorID string) string { | |
| 1220 | return fmt.Sprintf(`{"title":"Land the feature","author":%q,"author_id":%q,"base":%q,"head":%q,"state":"open","created":"2026-10-22T22:00:00Z"}`+"\n", | ||
| 1221 | author, authorID, base, head) | ||
| 1232 | return requestFile(base, head, author, authorID, "") | ||
| 1222 | 1233 | } | |
| 1223 | 1234 | ||
| 1224 | 1235 | func approvalFile(base, head string) string { | |
| 1225 | return fmt.Sprintf(`{"base":%q,"head":%q,"created":"2026-10-22T22:10:00Z"}`+"\n", base, head) | ||
| 1236 | d := frontmatter.New() | ||
| 1237 | d.Word("base", base) | ||
| 1238 | d.Word("head", head) | ||
| 1239 | d.Word("created", "2026-10-22T22:10:00Z") | ||
| 1240 | out, err := d.Bytes("") | ||
| 1241 | if err != nil { | ||
| 1242 | panic(err) | ||
| 1243 | } | ||
| 1244 | return string(out) | ||
| 1245 | } | ||
| 1246 | |||
| 1247 | // issueFile renders issues/<id>/issue.md for fixtures. | ||
| 1248 | func issueFile(title, author, state, created string, labels []string, body string) string { | ||
| 1249 | d := frontmatter.New() | ||
| 1250 | d.Text("title", title) | ||
| 1251 | d.Word("state", state) | ||
| 1252 | d.Word("author", author) | ||
| 1253 | d.Word("created", created) | ||
| 1254 | d.List("labels", labels) | ||
| 1255 | out, err := d.Bytes(body) | ||
| 1256 | if err != nil { | ||
| 1257 | panic(err) | ||
| 1258 | } | ||
| 1259 | return string(out) | ||
| 1226 | 1260 | } | |
| 1227 | 1261 | ||
| 1228 | 1262 | // TestProtectedBranchRequiresApprovedRequest is the whole Phase 3 contract: a | |
| @@ -1265,8 +1299,7 @@ func TestProtectedBranchRequiresApprovedRequest(t *testing.T) { | |||
| 1265 | 1299 | // 2. Request open, no approvals: still refused. | |
| 1266 | 1300 | meta := f.newMetaRepo(t) | |
| 1267 | 1301 | if err := meta.apply(map[string]string{ | |
| 1268 | "requests/" + reqID + "/meta.json": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1269 | "requests/" + reqID + "/description.md": "please\n", | ||
| 1302 | "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1270 | 1303 | }, "request"); err != nil { | |
| 1271 | 1304 | t.Fatalf("request push: %v", err) | |
| 1272 | 1305 | } | |
| @@ -1279,7 +1312,7 @@ func TestProtectedBranchRequiresApprovedRequest(t *testing.T) { | |||
| 1279 | 1312 | // 3. Approval by another principal: allowed. | |
| 1280 | 1313 | approver := f.newMetaRepo(t) | |
| 1281 | 1314 | if err := approver.apply(map[string]string{ | |
| 1282 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".json": approvalFile("main", head), | ||
| 1315 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".md": approvalFile("main", head), | ||
| 1283 | 1316 | }, "approve"); err != nil { | |
| 1284 | 1317 | t.Fatalf("approval push: %v", err) | |
| 1285 | 1318 | } | |
| @@ -1317,13 +1350,13 @@ func TestApprovalIsBoundToTheCommit(t *testing.T) { | |||
| 1317 | 1350 | ||
| 1318 | 1351 | meta := f.newMetaRepo(t) | |
| 1319 | 1352 | if err := meta.apply(map[string]string{ | |
| 1320 | "requests/" + reqID + "/meta.json": requestMeta("main", approved, "wilbur", f.accountID("wilbur")), | ||
| 1353 | "requests/" + reqID + "/request.md": requestMeta("main", approved, "wilbur", f.accountID("wilbur")), | ||
| 1321 | 1354 | }, "request"); err != nil { | |
| 1322 | 1355 | t.Fatalf("request: %v", err) | |
| 1323 | 1356 | } | |
| 1324 | 1357 | approver := f.newMetaRepo(t) | |
| 1325 | 1358 | if err := approver.apply(map[string]string{ | |
| 1326 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".json": approvalFile("main", approved), | ||
| 1359 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".md": approvalFile("main", approved), | ||
| 1327 | 1360 | }, "approve"); err != nil { | |
| 1328 | 1361 | t.Fatalf("approve: %v", err) | |
| 1329 | 1362 | } | |
| @@ -1351,8 +1384,8 @@ func TestSelfApprovalRejected(t *testing.T) { | |||
| 1351 | 1384 | ||
| 1352 | 1385 | meta := f.newMetaRepo(t) | |
| 1353 | 1386 | err := meta.apply(map[string]string{ | |
| 1354 | "requests/" + reqID + "/meta.json": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1355 | "requests/" + reqID + "/approvals/" + f.accountID("wilbur") + ".json": approvalFile("main", head), | ||
| 1387 | "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1388 | "requests/" + reqID + "/approvals/" + f.accountID("wilbur") + ".md": approvalFile("main", head), | ||
| 1356 | 1389 | }, "self approve") | |
| 1357 | 1390 | if err == nil { | |
| 1358 | 1391 | t.Fatal("a self-approval was accepted") | |
| @@ -1387,13 +1420,13 @@ func TestMergeCommitShapeAllowed(t *testing.T) { | |||
| 1387 | 1420 | } | |
| 1388 | 1421 | meta := f.newMetaRepo(t) | |
| 1389 | 1422 | if err := meta.apply(map[string]string{ | |
| 1390 | "requests/" + reqID + "/meta.json": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1423 | "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1391 | 1424 | }, "request"); err != nil { | |
| 1392 | 1425 | t.Fatalf("request: %v", err) | |
| 1393 | 1426 | } | |
| 1394 | 1427 | approver := f.newMetaRepo(t) | |
| 1395 | 1428 | if err := approver.apply(map[string]string{ | |
| 1396 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".json": approvalFile("main", head), | ||
| 1429 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".md": approvalFile("main", head), | ||
| 1397 | 1430 | }, "approve"); err != nil { | |
| 1398 | 1431 | t.Fatalf("approve: %v", err) | |
| 1399 | 1432 | } | |
| @@ -1431,7 +1464,7 @@ func TestImpersonatedApprovalRejected(t *testing.T) { | |||
| 1431 | 1464 | ||
| 1432 | 1465 | meta := f.newMetaRepo(t) | |
| 1433 | 1466 | if err := meta.apply(map[string]string{ | |
| 1434 | "requests/" + reqID + "/meta.json": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1467 | "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1435 | 1468 | }, "request"); err != nil { | |
| 1436 | 1469 | t.Fatalf("request: %v", err) | |
| 1437 | 1470 | } | |
| @@ -1439,7 +1472,7 @@ func TestImpersonatedApprovalRejected(t *testing.T) { | |||
| 1439 | 1472 | impostor := f.newMetaRepo(t) | |
| 1440 | 1473 | impostor.key = writerKey | |
| 1441 | 1474 | err := impostor.apply(map[string]string{ | |
| 1442 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".json": approvalFile("main", head), | ||
| 1475 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".md": approvalFile("main", head), | ||
| 1443 | 1476 | }, "approve as someone else") | |
| 1444 | 1477 | if err == nil { | |
| 1445 | 1478 | t.Fatal("a writer approved in another principal's name") | |
| @@ -1473,13 +1506,13 @@ func TestApprovalIsBoundToTheBase(t *testing.T) { | |||
| 1473 | 1506 | feature, head := f.cloneWithChange(t, map[string]string{"a.md": "v2\n"}, "feature") | |
| 1474 | 1507 | meta := f.newMetaRepo(t) | |
| 1475 | 1508 | if err := meta.apply(map[string]string{ | |
| 1476 | "requests/" + reqID + "/meta.json": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1509 | "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1477 | 1510 | }, "request"); err != nil { | |
| 1478 | 1511 | t.Fatalf("request: %v", err) | |
| 1479 | 1512 | } | |
| 1480 | 1513 | approver := f.newMetaRepo(t) | |
| 1481 | 1514 | if err := approver.apply(map[string]string{ | |
| 1482 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".json": approvalFile("main", head), | ||
| 1515 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".md": approvalFile("main", head), | ||
| 1483 | 1516 | }, "approve"); err != nil { | |
| 1484 | 1517 | t.Fatalf("approve: %v", err) | |
| 1485 | 1518 | } | |
| @@ -1487,7 +1520,7 @@ func TestApprovalIsBoundToTheBase(t *testing.T) { | |||
| 1487 | 1520 | // Repoint the request at release. The approval named main. | |
| 1488 | 1521 | repoint := f.newMetaRepo(t) | |
| 1489 | 1522 | if err := repoint.apply(map[string]string{ | |
| 1490 | "requests/" + reqID + "/meta.json": requestMeta("release", head, "wilbur", f.accountID("wilbur")), | ||
| 1523 | "requests/" + reqID + "/request.md": requestMeta("release", head, "wilbur", f.accountID("wilbur")), | ||
| 1491 | 1524 | }, "repoint"); err != nil { | |
| 1492 | 1525 | t.Fatalf("repoint: %v", err) | |
| 1493 | 1526 | } | |
| @@ -1513,8 +1546,8 @@ func TestFirstPushCannotSeedForeignApprovals(t *testing.T) { | |||
| 1513 | 1546 | meta := f.newMetaRepo(t) | |
| 1514 | 1547 | meta.key = writerKey | |
| 1515 | 1548 | err := meta.apply(map[string]string{ | |
| 1516 | "requests/" + reqID + "/meta.json": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1517 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".json": approvalFile("main", head), | ||
| 1549 | "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1550 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".md": approvalFile("main", head), | ||
| 1518 | 1551 | }, "seed") | |
| 1519 | 1552 | if err == nil { | |
| 1520 | 1553 | t.Fatal("the first push seeded an approval in another principal's name") | |
| @@ -1548,13 +1581,13 @@ func TestModifiedApprovalCannotBeForged(t *testing.T) { | |||
| 1548 | 1581 | } | |
| 1549 | 1582 | meta := f.newMetaRepo(t) | |
| 1550 | 1583 | if err := meta.apply(map[string]string{ | |
| 1551 | "requests/" + reqID + "/meta.json": requestMeta("main", approvedHead, "wilbur", f.accountID("wilbur")), | ||
| 1584 | "requests/" + reqID + "/request.md": requestMeta("main", approvedHead, "wilbur", f.accountID("wilbur")), | ||
| 1552 | 1585 | }, "request"); err != nil { | |
| 1553 | 1586 | t.Fatalf("request: %v", err) | |
| 1554 | 1587 | } | |
| 1555 | 1588 | approver := f.newMetaRepo(t) | |
| 1556 | 1589 | if err := approver.apply(map[string]string{ | |
| 1557 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".json": approvalFile("main", approvedHead), | ||
| 1590 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".md": approvalFile("main", approvedHead), | ||
| 1558 | 1591 | }, "approve"); err != nil { | |
| 1559 | 1592 | t.Fatalf("approve: %v", err) | |
| 1560 | 1593 | } | |
| @@ -1570,8 +1603,8 @@ func TestModifiedApprovalCannotBeForged(t *testing.T) { | |||
| 1570 | 1603 | forger := f.newMetaRepo(t) | |
| 1571 | 1604 | forger.key = writerKey | |
| 1572 | 1605 | err := forger.apply(map[string]string{ | |
| 1573 | "requests/" + reqID + "/meta.json": requestMeta("main", sneaky, "wilbur", f.accountID("wilbur")), | ||
| 1574 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".json": approvalFile("main", sneaky), | ||
| 1606 | "requests/" + reqID + "/request.md": requestMeta("main", sneaky, "wilbur", f.accountID("wilbur")), | ||
| 1607 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".md": approvalFile("main", sneaky), | ||
| 1575 | 1608 | }, "repoint approval") | |
| 1576 | 1609 | if err == nil { | |
| 1577 | 1610 | t.Fatal("a writer rewrote another principal's approval and the push was accepted") | |
| @@ -1597,13 +1630,13 @@ func TestApprovalDeletionIsOwnerOrAdmin(t *testing.T) { | |||
| 1597 | 1630 | meta := f.newMetaRepo(t) | |
| 1598 | 1631 | meta.key = writerKey | |
| 1599 | 1632 | if err := meta.apply(map[string]string{ | |
| 1600 | "requests/" + reqID + "/meta.json": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1633 | "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur", f.accountID("wilbur")), | ||
| 1601 | 1634 | }, "request"); err != nil { | |
| 1602 | 1635 | t.Fatalf("setup: %v", err) | |
| 1603 | 1636 | } | |
| 1604 | 1637 | approver := f.newMetaRepo(t) | |
| 1605 | 1638 | if err := approver.apply(map[string]string{ | |
| 1606 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".json": approvalFile("main", head), | ||
| 1639 | "requests/" + reqID + "/approvals/" + f.accountID("keenan") + ".md": approvalFile("main", head), | ||
| 1607 | 1640 | }, "approve"); err != nil { | |
| 1608 | 1641 | t.Fatalf("approve: %v", err) | |
| 1609 | 1642 | } | |
| @@ -1612,7 +1645,7 @@ func TestApprovalDeletionIsOwnerOrAdmin(t *testing.T) { | |||
| 1612 | 1645 | deleter := f.newMetaRepo(t) | |
| 1613 | 1646 | deleter.key = writerKey | |
| 1614 | 1647 | deleter.sync() | |
| 1615 | if err := os.Remove(filepath.Join(deleter.dir, "requests", reqID, "approvals", f.accountID("keenan")+".json")); err != nil { | ||
| 1648 | if err := os.Remove(filepath.Join(deleter.dir, "requests", reqID, "approvals", f.accountID("keenan")+".md")); err != nil { | ||
| 1616 | 1649 | t.Fatal(err) | |
| 1617 | 1650 | } | |
| 1618 | 1651 | if _, errs, err := runGit(t, deleter.dir, "add", "-A"); err != nil { | |
| @@ -1632,7 +1665,7 @@ func TestApprovalDeletionIsOwnerOrAdmin(t *testing.T) { | |||
| 1632 | 1665 | // keenan withdraws his own. | |
| 1633 | 1666 | own := f.newMetaRepo(t) | |
| 1634 | 1667 | own.sync() | |
| 1635 | if err := os.Remove(filepath.Join(own.dir, "requests", reqID, "approvals", f.accountID("keenan")+".json")); err != nil { | ||
| 1668 | if err := os.Remove(filepath.Join(own.dir, "requests", reqID, "approvals", f.accountID("keenan")+".md")); err != nil { | ||
| 1636 | 1669 | t.Fatal(err) | |
| 1637 | 1670 | } | |
| 1638 | 1671 | if _, errs, cerr := runGit(t, own.dir, "add", "-A"); cerr != nil { | |
| @@ -1663,14 +1696,14 @@ func TestRenameKeepsAccessAndApprovalOwnership(t *testing.T) { | |||
| 1663 | 1696 | // keenan authors a request; wilbur (the writer) approves it. | |
| 1664 | 1697 | meta := f.newMetaRepo(t) | |
| 1665 | 1698 | if err := meta.apply(map[string]string{ | |
| 1666 | "requests/" + reqID + "/meta.json": requestMeta("main", head, "keenan", f.ownerID), | ||
| 1699 | "requests/" + reqID + "/request.md": requestMeta("main", head, "keenan", f.ownerID), | ||
| 1667 | 1700 | }, "request"); err != nil { | |
| 1668 | 1701 | t.Fatalf("request: %v", err) | |
| 1669 | 1702 | } | |
| 1670 | 1703 | approver := f.newMetaRepo(t) | |
| 1671 | 1704 | approver.key = writerKey | |
| 1672 | 1705 | if err := approver.apply(map[string]string{ | |
| 1673 | "requests/" + reqID + "/approvals/" + f.writerID + ".json": approvalFile("main", head), | ||
| 1706 | "requests/" + reqID + "/approvals/" + f.writerID + ".md": approvalFile("main", head), | ||
| 1674 | 1707 | }, "approve"); err != nil { | |
| 1675 | 1708 | t.Fatalf("approve: %v", err) | |
| 1676 | 1709 | } | |
| @@ -1687,7 +1720,7 @@ func TestRenameKeepsAccessAndApprovalOwnership(t *testing.T) { | |||
| 1687 | 1720 | issue := f.newMetaRepo(t) | |
| 1688 | 1721 | issue.key = writerKey | |
| 1689 | 1722 | if err := issue.apply(map[string]string{ | |
| 1690 | "issues/01M5JR2JK29KKMAA1T9MDNTZ01/meta.json": `{"title":"after rename","state":"open","author":"robert","created":"2026-10-22T22:00:00Z"}` + "\n", | ||
| 1723 | "issues/01M5JR2JK29KKMAA1T9MDNTZ01/issue.md": issueFile("after rename", "robert", "open", "2026-10-22T22:00:00Z", nil, ""), | ||
| 1691 | 1724 | }, "issue after rename"); err != nil { | |
| 1692 | 1725 | t.Fatalf("the renamed account lost its access: %v", err) | |
| 1693 | 1726 | } | |
| @@ -1697,7 +1730,7 @@ func TestRenameKeepsAccessAndApprovalOwnership(t *testing.T) { | |||
| 1697 | 1730 | withdraw := f.newMetaRepo(t) | |
| 1698 | 1731 | withdraw.key = writerKey | |
| 1699 | 1732 | withdraw.sync() | |
| 1700 | if err := os.Remove(filepath.Join(withdraw.dir, "requests", reqID, "approvals", f.writerID+".json")); err != nil { | ||
| 1733 | if err := os.Remove(filepath.Join(withdraw.dir, "requests", reqID, "approvals", f.writerID+".md")); err != nil { | ||
| 1701 | 1734 | t.Fatal(err) | |
| 1702 | 1735 | } | |
| 1703 | 1736 | if _, errs, err := runGit(t, withdraw.dir, "add", "-A"); err != nil { | |
| @@ -1987,13 +2020,13 @@ func (f *fixture) openAndApprove(t *testing.T, head, author string) { | |||
| 1987 | 2020 | t.Helper() | |
| 1988 | 2021 | meta := f.newMetaRepo(t) | |
| 1989 | 2022 | if err := meta.apply(map[string]string{ | |
| 1990 | "requests/" + reqID + "/meta.json": requestMeta("main", head, author, f.accountID(author)), | ||
| 2023 | "requests/" + reqID + "/request.md": requestMeta("main", head, author, f.accountID(author)), | ||
| 1991 | 2024 | }, "request"); err != nil { | |
| 1992 | 2025 | t.Fatalf("request: %v", err) | |
| 1993 | 2026 | } | |
| 1994 | 2027 | approver := f.newMetaRepo(t) | |
| 1995 | 2028 | if err := approver.apply(map[string]string{ | |
| 1996 | "requests/" + reqID + "/approvals/" + f.ownerID + ".json": approvalFile("main", head), | ||
| 2029 | "requests/" + reqID + "/approvals/" + f.ownerID + ".md": approvalFile("main", head), | ||
| 1997 | 2030 | }, "approve"); err != nil { | |
| 1998 | 2031 | t.Fatalf("approve: %v", err) | |
| 1999 | 2032 | } | |