vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Let the browser edit issue and change details
a4e7a99a9ecf697586abdb884351a722fc25a864 · Keenan Nemetz · 6d ago · parent de84c32b
Let the browser edit issue and change details An issue's title and description could only be corrected with the CLI, and a change request's prose could not be corrected at all after it was opened. Add an edit page for each, offered to the author and to a maintainer. A change edit writes through a new metadata writer so the record is still validated, and neither edit touches the discussion, labels, or review state.
metadata/requests_test.go +47
| @@ -881,6 +881,53 @@ func TestSetRequestClosesEditsAnOpenRequest(t *testing.T) { | |||
| 881 | 881 | } | |
| 882 | 882 | } | |
| 883 | 883 | ||
| 884 | // The prose is not what a review names, so correcting it does not start the | ||
| 885 | // review over and is not limited to an open request. The writer still validates the | ||
| 886 | // record it is about to store. | ||
| 887 | func TestSetRequestDetailsEditsTitleAndBody(t *testing.T) { | ||
| 888 | t.Parallel() | ||
| 889 | g := newRequestRepo(t) | ||
| 890 | now := time.Date(2026, 10, 27, 12, 0, 0, 0, time.UTC) | ||
| 891 | req, err := NewRequest("First draft", "why\n", "keenan", "main", headSHA, now) | ||
| 892 | if err != nil { | ||
| 893 | t.Fatal(err) | ||
| 894 | } | ||
| 895 | if err := CreateRequest(g, req, "keenan"); err != nil { | ||
| 896 | t.Fatal(err) | ||
| 897 | } | ||
| 898 | |||
| 899 | got, err := SetRequestDetails(g, req.ID, " Better title ", "clearer why\n", "keenan") | ||
| 900 | if err != nil { | ||
| 901 | t.Fatalf("edit: %v", err) | ||
| 902 | } | ||
| 903 | if got.Title != "Better title" || got.Description != "clearer why\n" { | ||
| 904 | t.Fatalf("edited request = %q / %q", got.Title, got.Description) | ||
| 905 | } | ||
| 906 | stored, err := GetRequest(g, req.ID) | ||
| 907 | if err != nil { | ||
| 908 | t.Fatal(err) | ||
| 909 | } | ||
| 910 | if stored.Title != "Better title" || strings.TrimSpace(stored.Description) != "clearer why" { | ||
| 911 | t.Fatalf("stored request = %q / %q", stored.Title, stored.Description) | ||
| 912 | } | ||
| 913 | |||
| 914 | // An edit that changes nothing is not a commit. | ||
| 915 | tip, err := g.RevParse(Ref) | ||
| 916 | if err != nil { | ||
| 917 | t.Fatal(err) | ||
| 918 | } | ||
| 919 | if _, err := SetRequestDetails(g, req.ID, "Better title", "clearer why\n", "keenan"); err != nil { | ||
| 920 | t.Fatalf("repeat edit: %v", err) | ||
| 921 | } | ||
| 922 | if next, _ := g.RevParse(Ref); next != tip { | ||
| 923 | t.Fatal("an edit that changed nothing moved the branch") | ||
| 924 | } | ||
| 925 | |||
| 926 | if _, err := SetRequestDetails(g, req.ID, " ", "clearer why\n", "keenan"); err == nil { | ||
| 927 | t.Fatal("a blank title was accepted") | ||
| 928 | } | ||
| 929 | } | ||
| 930 | |||
| 884 | 931 | func mustTree(t *testing.T, g *git.Repo) string { | |
| 885 | 932 | t.Helper() | |
| 886 | 933 | tree, err := g.MkTree(nil) | |