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).

server/integration_test.go +38−38

@@ -1260,7 +1260,7 @@ func TestRequestAuthorComesFromTheKey(t *testing.T) {
1260 1260 t.Fatalf("no request id printed: %q", out)
1261 1261 }
1262 1262 raw, _, err := runGit(t, filepath.Join(f.cfg.ReposDir(), f.repo+".git"),
1263 "show", "refs/heads/vole:requests/"+fields[0]+"/request.md")
1263 "show", "refs/heads/vole:changes/"+fields[0]+"/change.md")
1264 1264 if err != nil {
1265 1265 t.Fatalf("read request from the server: %v\n%s", err, raw)
1266 1266 }
@@ -1365,7 +1365,7 @@ func TestRequestPageSaysWhenItWouldConflict(t *testing.T) {
1365 1365
1366 1366 meta := f.newMetaRepo(t)
1367 1367 if err := meta.apply(map[string]string{
1368 "requests/" + reqID + "/request.md": requestFile("main", head, "keenan", "why this change\n"),
1368 "changes/" + reqID + "/change.md": requestFile("main", head, "keenan", "why this change\n"),
1369 1369 }, "request"); err != nil {
1370 1370 t.Fatalf("request push: %v", err)
1371 1371 }
@@ -1389,7 +1389,7 @@ func TestRequestPageSaysWhenItWouldConflict(t *testing.T) {
1389 1389 }
1390 1390 const cleanID = "01M5JR2JK29KKMAA1T9MDNTR02"
1391 1391 if err := meta.apply(map[string]string{
1392 "requests/" + cleanID + "/request.md": requestFile("main", cleanHead, "keenan", "clean change\n"),
1392 "changes/" + cleanID + "/change.md": requestFile("main", cleanHead, "keenan", "clean change\n"),
1393 1393 }, "clean request"); err != nil {
1394 1394 t.Fatalf("clean request push: %v", err)
1395 1395 }
@@ -1432,7 +1432,7 @@ func TestRequestPageSaysWhenItWouldConflict(t *testing.T) {
1432 1432 // about: the commit is not in the repository, so there is no merge to report.
1433 1433 const ghostID = "01M5JR2JK29KKMAA1T9MDNTR03"
1434 1434 if err := meta.apply(map[string]string{
1435 "requests/" + ghostID + "/request.md": requestFile("main",
1435 "changes/" + ghostID + "/change.md": requestFile("main",
1436 1436 "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "keenan", "ghost head\n"),
1437 1437 }, "ghost request"); err != nil {
1438 1438 t.Fatalf("ghost request push: %v", err)
@@ -1463,7 +1463,7 @@ func TestRequestDiscussionIsPushedAndOwned(t *testing.T) {
1463 1463 }
1464 1464 meta := f.newMetaRepo(t)
1465 1465 if err := meta.apply(map[string]string{
1466 "requests/" + reqID + "/request.md": requestFile("main", head, "keenan", "rationale\n"),
1466 "changes/" + reqID + "/change.md": requestFile("main", head, "keenan", "rationale\n"),
1467 1467 }, "request"); err != nil {
1468 1468 t.Fatalf("request push: %v", err)
1469 1469 }
@@ -1514,7 +1514,7 @@ func TestRequestDiscussionIsPushedAndOwned(t *testing.T) {
1514 1514 t.Fatalf("the CLI request reply is not nested:\n%s", body)
1515 1515 }
1516 1516
1517 commentPath := func(id string) string { return "requests/" + reqID + "/comments/" + id + ".md" }
1517 commentPath := func(id string) string { return "changes/" + reqID + "/comments/" + id + ".md" }
1518 1518 const created = "2026-10-22T23:00:00Z"
1519 1519
1520 1520 // Someone else may not rewrite it, adopt it, or delete it.
@@ -1584,7 +1584,7 @@ func TestReviewCommentAnchorsALine(t *testing.T) {
1584 1584 }
1585 1585 meta := f.newMetaRepo(t)
1586 1586 if err := meta.apply(map[string]string{
1587 "requests/" + reqID + "/request.md": requestFile("main", head, "keenan", "rationale\n"),
1587 "changes/" + reqID + "/change.md": requestFile("main", head, "keenan", "rationale\n"),
1588 1588 }, "request"); err != nil {
1589 1589 t.Fatalf("request push: %v", err)
1590 1590 }
@@ -1606,7 +1606,7 @@ func TestReviewCommentAnchorsALine(t *testing.T) {
1606 1606 }
1607 1607 wantBlob, _, _ := runGit(t, filepath.Join(f.cfg.ReposDir(), f.repo+".git"), "rev-parse", head+":code.txt")
1608 1608 wantBlob = strings.TrimSpace(wantBlob)
1609 got := serverFile("requests/" + reqID + "/comments/" + newCm + ".md")
1609 got := serverFile("changes/" + reqID + "/comments/" + newCm + ".md")
1610 1610 for _, want := range []string{"path: code.txt", "side: new", "line: 2", "blob: " + wantBlob} {
1611 1611 if !strings.Contains(got, want) {
1612 1612 t.Fatalf("the anchor is missing %q:\n%s", want, got)
@@ -1636,7 +1636,7 @@ func TestReviewCommentAnchorsALine(t *testing.T) {
1636 1636 mergeBase, _, _ := runGit(t, filepath.Join(f.cfg.ReposDir(), f.repo+".git"), "merge-base", "refs/heads/main", head)
1637 1637 oldBlob, _, _ := runGit(t, filepath.Join(f.cfg.ReposDir(), f.repo+".git"),
1638 1638 "rev-parse", strings.TrimSpace(mergeBase)+":code.txt")
1639 if got := serverFile("requests/" + reqID + "/comments/" + oldCm + ".md"); !strings.Contains(got, "blob: "+strings.TrimSpace(oldBlob)) {
1639 if got := serverFile("changes/" + reqID + "/comments/" + oldCm + ".md"); !strings.Contains(got, "blob: "+strings.TrimSpace(oldBlob)) {
1640 1640 t.Fatalf("the old side anchor does not name the merge-base blob:\n%s", got)
1641 1641 }
1642 1642
@@ -2283,7 +2283,7 @@ func (f *fixture) cloneWithChange(t *testing.T, files map[string]string, msg str
2283 2283
2284 2284 const reqID = "01M5JR2JK29KKMAA1T9MDNTR01"
2285 2285
2286 // requestFile renders requests/<id>/request.md. The body is the request's
2286 // requestFile renders changes/<id>/change.md. The body is the request's
2287 2287 // description, so a fixture cannot drift from the format the reader expects.
2288 2288 func requestFile(base, head, author, body string) string {
2289 2289 return requestFileTitled("Land the feature", base, head, author, body)
@@ -2427,7 +2427,7 @@ func TestProtectedBranchRequiresApprovedRequest(t *testing.T) {
2427 2427 // 2. Request open, no approvals: still refused.
2428 2428 meta := f.newMetaRepo(t)
2429 2429 if err := meta.apply(map[string]string{
2430 "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur"),
2430 "changes/" + reqID + "/change.md": requestMeta("main", head, "wilbur"),
2431 2431 }, "request"); err != nil {
2432 2432 t.Fatalf("request push: %v", err)
2433 2433 }
@@ -2440,7 +2440,7 @@ func TestProtectedBranchRequiresApprovedRequest(t *testing.T) {
2440 2440 // 3. Approval by another principal: allowed.
2441 2441 approver := f.newMetaRepo(t)
2442 2442 if err := approver.apply(map[string]string{
2443 "requests/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", head),
2443 "changes/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", head),
2444 2444 }, "approve"); err != nil {
2445 2445 t.Fatalf("approval push: %v", err)
2446 2446 }
@@ -2479,13 +2479,13 @@ func TestApprovalIsBoundToTheCommit(t *testing.T) {
2479 2479
2480 2480 meta := f.newMetaRepo(t)
2481 2481 if err := meta.apply(map[string]string{
2482 "requests/" + reqID + "/request.md": requestMeta("main", approved, "wilbur"),
2482 "changes/" + reqID + "/change.md": requestMeta("main", approved, "wilbur"),
2483 2483 }, "request"); err != nil {
2484 2484 t.Fatalf("request: %v", err)
2485 2485 }
2486 2486 approver := f.newMetaRepo(t)
2487 2487 if err := approver.apply(map[string]string{
2488 "requests/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", approved),
2488 "changes/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", approved),
2489 2489 }, "approve"); err != nil {
2490 2490 t.Fatalf("approve: %v", err)
2491 2491 }
@@ -2514,8 +2514,8 @@ func TestSelfApprovalRejected(t *testing.T) {
2514 2514
2515 2515 meta := f.newMetaRepo(t)
2516 2516 err := meta.apply(map[string]string{
2517 "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur"),
2518 "requests/" + reqID + "/approvals/" + f.accountName("wilbur") + ".md": approvalFile("main", head),
2517 "changes/" + reqID + "/change.md": requestMeta("main", head, "wilbur"),
2518 "changes/" + reqID + "/approvals/" + f.accountName("wilbur") + ".md": approvalFile("main", head),
2519 2519 }, "self approve")
2520 2520 if err == nil {
2521 2521 t.Fatal("a self-approval was accepted")
@@ -2551,13 +2551,13 @@ func TestMergeCommitShapeAllowed(t *testing.T) {
2551 2551 }
2552 2552 meta := f.newMetaRepo(t)
2553 2553 if err := meta.apply(map[string]string{
2554 "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur"),
2554 "changes/" + reqID + "/change.md": requestMeta("main", head, "wilbur"),
2555 2555 }, "request"); err != nil {
2556 2556 t.Fatalf("request: %v", err)
2557 2557 }
2558 2558 approver := f.newMetaRepo(t)
2559 2559 if err := approver.apply(map[string]string{
2560 "requests/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", head),
2560 "changes/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", head),
2561 2561 }, "approve"); err != nil {
2562 2562 t.Fatalf("approve: %v", err)
2563 2563 }
@@ -2596,7 +2596,7 @@ func TestImpersonatedApprovalRejected(t *testing.T) {
2596 2596
2597 2597 meta := f.newMetaRepo(t)
2598 2598 if err := meta.apply(map[string]string{
2599 "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur"),
2599 "changes/" + reqID + "/change.md": requestMeta("main", head, "wilbur"),
2600 2600 }, "request"); err != nil {
2601 2601 t.Fatalf("request: %v", err)
2602 2602 }
@@ -2604,7 +2604,7 @@ func TestImpersonatedApprovalRejected(t *testing.T) {
2604 2604 impostor := f.newMetaRepo(t)
2605 2605 impostor.key = writerKey
2606 2606 err := impostor.apply(map[string]string{
2607 "requests/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", head),
2607 "changes/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", head),
2608 2608 }, "approve as someone else")
2609 2609 if err == nil {
2610 2610 t.Fatal("a writer approved in another principal's name")
@@ -2639,13 +2639,13 @@ func TestApprovalIsBoundToTheBase(t *testing.T) {
2639 2639 feature, head := f.cloneWithChange(t, map[string]string{"a.md": "v2\n"}, "feature")
2640 2640 meta := f.newMetaRepo(t)
2641 2641 if err := meta.apply(map[string]string{
2642 "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur"),
2642 "changes/" + reqID + "/change.md": requestMeta("main", head, "wilbur"),
2643 2643 }, "request"); err != nil {
2644 2644 t.Fatalf("request: %v", err)
2645 2645 }
2646 2646 approver := f.newMetaRepo(t)
2647 2647 if err := approver.apply(map[string]string{
2648 "requests/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", head),
2648 "changes/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", head),
2649 2649 }, "approve"); err != nil {
2650 2650 t.Fatalf("approve: %v", err)
2651 2651 }
@@ -2653,7 +2653,7 @@ func TestApprovalIsBoundToTheBase(t *testing.T) {
2653 2653 // Repoint the request at release. The approval named main.
2654 2654 repoint := f.newMetaRepo(t)
2655 2655 if err := repoint.apply(map[string]string{
2656 "requests/" + reqID + "/request.md": requestMeta("release", head, "wilbur"),
2656 "changes/" + reqID + "/change.md": requestMeta("release", head, "wilbur"),
2657 2657 }, "repoint"); err != nil {
2658 2658 t.Fatalf("repoint: %v", err)
2659 2659 }
@@ -2680,8 +2680,8 @@ func TestFirstPushCannotSeedForeignApprovals(t *testing.T) {
2680 2680 meta := f.newMetaRepo(t)
2681 2681 meta.key = writerKey
2682 2682 err := meta.apply(map[string]string{
2683 "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur"),
2684 "requests/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", head),
2683 "changes/" + reqID + "/change.md": requestMeta("main", head, "wilbur"),
2684 "changes/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", head),
2685 2685 }, "seed")
2686 2686 if err == nil {
2687 2687 t.Fatal("the first push seeded an approval in another principal's name")
@@ -2716,13 +2716,13 @@ func TestModifiedApprovalCannotBeForged(t *testing.T) {
2716 2716 }
2717 2717 meta := f.newMetaRepo(t)
2718 2718 if err := meta.apply(map[string]string{
2719 "requests/" + reqID + "/request.md": requestMeta("main", approvedHead, "wilbur"),
2719 "changes/" + reqID + "/change.md": requestMeta("main", approvedHead, "wilbur"),
2720 2720 }, "request"); err != nil {
2721 2721 t.Fatalf("request: %v", err)
2722 2722 }
2723 2723 approver := f.newMetaRepo(t)
2724 2724 if err := approver.apply(map[string]string{
2725 "requests/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", approvedHead),
2725 "changes/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", approvedHead),
2726 2726 }, "approve"); err != nil {
2727 2727 t.Fatalf("approve: %v", err)
2728 2728 }
@@ -2738,8 +2738,8 @@ func TestModifiedApprovalCannotBeForged(t *testing.T) {
2738 2738 forger := f.newMetaRepo(t)
2739 2739 forger.key = writerKey
2740 2740 err := forger.apply(map[string]string{
2741 "requests/" + reqID + "/request.md": requestMeta("main", sneaky, "wilbur"),
2742 "requests/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", sneaky),
2741 "changes/" + reqID + "/change.md": requestMeta("main", sneaky, "wilbur"),
2742 "changes/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", sneaky),
2743 2743 }, "repoint approval")
2744 2744 if err == nil {
2745 2745 t.Fatal("a writer rewrote another principal's approval and the push was accepted")
@@ -2766,13 +2766,13 @@ func TestApprovalDeletionIsOwnerOrAdmin(t *testing.T) {
2766 2766 meta := f.newMetaRepo(t)
2767 2767 meta.key = writerKey
2768 2768 if err := meta.apply(map[string]string{
2769 "requests/" + reqID + "/request.md": requestMeta("main", head, "wilbur"),
2769 "changes/" + reqID + "/change.md": requestMeta("main", head, "wilbur"),
2770 2770 }, "request"); err != nil {
2771 2771 t.Fatalf("setup: %v", err)
2772 2772 }
2773 2773 approver := f.newMetaRepo(t)
2774 2774 if err := approver.apply(map[string]string{
2775 "requests/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", head),
2775 "changes/" + reqID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", head),
2776 2776 }, "approve"); err != nil {
2777 2777 t.Fatalf("approve: %v", err)
2778 2778 }
@@ -2781,7 +2781,7 @@ func TestApprovalDeletionIsOwnerOrAdmin(t *testing.T) {
2781 2781 deleter := f.newMetaRepo(t)
2782 2782 deleter.key = writerKey
2783 2783 deleter.sync()
2784 if err := os.Remove(filepath.Join(deleter.dir, "requests", reqID, "approvals", f.accountName("keenan")+".md")); err != nil {
2784 if err := os.Remove(filepath.Join(deleter.dir, "changes", reqID, "approvals", f.accountName("keenan")+".md")); err != nil {
2785 2785 t.Fatal(err)
2786 2786 }
2787 2787 if _, errs, err := runGit(t, deleter.dir, "add", "-A"); err != nil {
@@ -2801,7 +2801,7 @@ func TestApprovalDeletionIsOwnerOrAdmin(t *testing.T) {
2801 2801 // keenan withdraws his own.
2802 2802 own := f.newMetaRepo(t)
2803 2803 own.sync()
2804 if err := os.Remove(filepath.Join(own.dir, "requests", reqID, "approvals", f.accountName("keenan")+".md")); err != nil {
2804 if err := os.Remove(filepath.Join(own.dir, "changes", reqID, "approvals", f.accountName("keenan")+".md")); err != nil {
2805 2805 t.Fatal(err)
2806 2806 }
2807 2807 if _, errs, cerr := runGit(t, own.dir, "add", "-A"); cerr != nil {
@@ -2831,14 +2831,14 @@ func TestAccessAndApprovalOwnershipFollowTheName(t *testing.T) {
2831 2831 // keenan authors a request; wilbur (the writer) approves it.
2832 2832 meta := f.newMetaRepo(t)
2833 2833 if err := meta.apply(map[string]string{
2834 "requests/" + reqID + "/request.md": requestMeta("main", head, "keenan"),
2834 "changes/" + reqID + "/change.md": requestMeta("main", head, "keenan"),
2835 2835 }, "request"); err != nil {
2836 2836 t.Fatalf("request: %v", err)
2837 2837 }
2838 2838 approver := f.newMetaRepo(t)
2839 2839 approver.key = writerKey
2840 2840 if err := approver.apply(map[string]string{
2841 "requests/" + reqID + "/approvals/wilbur.md": approvalFile("main", head),
2841 "changes/" + reqID + "/approvals/wilbur.md": approvalFile("main", head),
2842 2842 }, "approve"); err != nil {
2843 2843 t.Fatalf("approve: %v", err)
2844 2844 }
@@ -2856,7 +2856,7 @@ func TestAccessAndApprovalOwnershipFollowTheName(t *testing.T) {
2856 2856 withdraw := f.newMetaRepo(t)
2857 2857 withdraw.key = writerKey
2858 2858 withdraw.sync()
2859 if err := os.Remove(filepath.Join(withdraw.dir, "requests", reqID, "approvals", "wilbur.md")); err != nil {
2859 if err := os.Remove(filepath.Join(withdraw.dir, "changes", reqID, "approvals", "wilbur.md")); err != nil {
2860 2860 t.Fatal(err)
2861 2861 }
2862 2862 if _, errs, err := runGit(t, withdraw.dir, "add", "-A"); err != nil {
@@ -3504,13 +3504,13 @@ func (f *fixture) openAndApprove(t *testing.T, head, author string) {
3504 3504 t.Helper()
3505 3505 meta := f.newMetaRepo(t)
3506 3506 if err := meta.apply(map[string]string{
3507 "requests/" + reqID + "/request.md": requestMeta("main", head, author),
3507 "changes/" + reqID + "/change.md": requestMeta("main", head, author),
3508 3508 }, "request"); err != nil {
3509 3509 t.Fatalf("request: %v", err)
3510 3510 }
3511 3511 approver := f.newMetaRepo(t)
3512 3512 if err := approver.apply(map[string]string{
3513 "requests/" + reqID + "/approvals/" + f.ownerID + ".md": approvalFile("main", head),
3513 "changes/" + reqID + "/approvals/" + f.ownerID + ".md": approvalFile("main", head),
3514 3514 }, "approve"); err != nil {
3515 3515 t.Fatalf("approve: %v", err)
3516 3516 }