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.
Create and edit releases from the browser
ec34301375902f6b08c0171bf91b98cbcc55c63c · Keenan Nemetz · 15d ago · parent 09b15120
Create and edit releases from the browser Maintainers get a New release form that writes the same annotated tag the CLI writes: tag name, target commit, and notes become the tag message, with the account as tagger. Edit notes rewrites the tag message only while no artifact is attached, matching the release-immutability rule; lightweight tags and releases with files stay locked. Tag names are validated, existing tags are refused, reserved page names are rejected, and both actions are audited.
server/release_web_test.go +91
| @@ -0,0 +1,91 @@ | |||
| 1 | package server_test | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "net/url" | ||
| 5 | "strings" | ||
| 6 | "testing" | ||
| 7 | ) | ||
| 8 | |||
| 9 | func TestReleaseCreateAndEditFromTheBrowser(t *testing.T) { | ||
| 10 | t.Parallel() | ||
| 11 | f := newFixture(t) | ||
| 12 | f.publish(f.repo) | ||
| 13 | work := workRepo(t, map[string]string{"readme.md": "hello\n"}) | ||
| 14 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil { | ||
| 15 | t.Fatalf("push: %s", errs) | ||
| 16 | } | ||
| 17 | sess, cookie := f.browserSession(t, f.ownerID) | ||
| 18 | |||
| 19 | resp, body := f.browserGet(t, "/r/"+f.repo+"/releases/new", cookie) | ||
| 20 | if resp.StatusCode != 200 || !strings.Contains(body, "New release") { | ||
| 21 | t.Fatalf("new release page = %d:\n%s", resp.StatusCode, body) | ||
| 22 | } | ||
| 23 | form := url.Values{ | ||
| 24 | "csrf": {sess.CSRF}, "tag": {"v1.0.0"}, "target": {"main"}, | ||
| 25 | "notes": {"## v1.0.0\n\nFirst release.\n"}, | ||
| 26 | } | ||
| 27 | resp, _ = f.browserPost(t, "/r/"+f.repo+"/releases", cookie, form) | ||
| 28 | if resp.StatusCode != 303 || resp.Header.Get("Location") != "/r/"+f.repo+"/releases/v1.0.0" { | ||
| 29 | t.Fatalf("create release = %d %q", resp.StatusCode, resp.Header.Get("Location")) | ||
| 30 | } | ||
| 31 | m, err := f.srv.Repos.Open(f.repo) | ||
| 32 | if err != nil { | ||
| 33 | t.Fatal(err) | ||
| 34 | } | ||
| 35 | tag, err := m.Git().Tag("v1.0.0") | ||
| 36 | if err != nil { | ||
| 37 | t.Fatal(err) | ||
| 38 | } | ||
| 39 | if !tag.Annotated || !strings.Contains(tag.Message, "First release.") || tag.Tagger != "keenan" { | ||
| 40 | t.Fatalf("created tag = %+v", tag) | ||
| 41 | } | ||
| 42 | resp, body = f.browserGet(t, "/r/"+f.repo+"/releases/v1.0.0", cookie) | ||
| 43 | if resp.StatusCode != 200 || !strings.Contains(body, "First release") || !strings.Contains(body, "Edit notes") { | ||
| 44 | t.Fatalf("release page = %d:\n%s", resp.StatusCode, body) | ||
| 45 | } | ||
| 46 | |||
| 47 | resp, body = f.browserGet(t, "/r/"+f.repo+"/releases/edit?tag=v1.0.0", cookie) | ||
| 48 | if resp.StatusCode != 200 || !strings.Contains(body, "First release") { | ||
| 49 | t.Fatalf("edit release page = %d:\n%s", resp.StatusCode, body) | ||
| 50 | } | ||
| 51 | form.Set("notes", "## v1.0.0\n\nRevised notes.\n") | ||
| 52 | resp, _ = f.browserPost(t, "/r/"+f.repo+"/releases/edit", cookie, form) | ||
| 53 | if resp.StatusCode != 303 { | ||
| 54 | t.Fatalf("edit release = %d", resp.StatusCode) | ||
| 55 | } | ||
| 56 | tag, err = m.Git().Tag("v1.0.0") | ||
| 57 | if err != nil { | ||
| 58 | t.Fatal(err) | ||
| 59 | } | ||
| 60 | if !strings.Contains(tag.Message, "Revised notes.") || strings.Contains(tag.Message, "First release.") { | ||
| 61 | t.Fatalf("edited tag message = %q", tag.Message) | ||
| 62 | } | ||
| 63 | |||
| 64 | form.Set("tag", "v1.0.0") | ||
| 65 | form.Set("notes", "duplicate") | ||
| 66 | if resp, body := f.browserPost(t, "/r/"+f.repo+"/releases", cookie, form); resp.StatusCode != 400 || !strings.Contains(body, "already exists") { | ||
| 67 | t.Fatalf("duplicate release = %d:\n%s", resp.StatusCode, body) | ||
| 68 | } | ||
| 69 | form.Set("tag", "bad tag") | ||
| 70 | if resp, _ := f.browserPost(t, "/r/"+f.repo+"/releases", cookie, form); resp.StatusCode != 400 { | ||
| 71 | t.Fatalf("invalid tag = %d, want 400", resp.StatusCode) | ||
| 72 | } | ||
| 73 | form.Set("tag", "v1.1.0") | ||
| 74 | form.Set("notes", " ") | ||
| 75 | if resp, body := f.browserPost(t, "/r/"+f.repo+"/releases", cookie, form); resp.StatusCode != 400 || !strings.Contains(body, "needs notes") { | ||
| 76 | t.Fatalf("empty notes = %d:\n%s", resp.StatusCode, body) | ||
| 77 | } | ||
| 78 | |||
| 79 | commit, _ := m.Git().RevParse("main") | ||
| 80 | if err := m.Git().UpdateRef("refs/tags/light", commit); err != nil { | ||
| 81 | t.Fatal(err) | ||
| 82 | } | ||
| 83 | if resp, body := f.browserGet(t, "/r/"+f.repo+"/releases/edit?tag=light", cookie); resp.StatusCode != 200 || !strings.Contains(body, "Lightweight tags have no notes") { | ||
| 84 | t.Fatalf("lightweight edit page = %d:\n%s", resp.StatusCode, body) | ||
| 85 | } | ||
| 86 | form.Set("tag", "light") | ||
| 87 | form.Set("notes", "cannot") | ||
| 88 | if resp, _ := f.browserPost(t, "/r/"+f.repo+"/releases/edit", cookie, form); resp.StatusCode != 400 { | ||
| 89 | t.Fatalf("lightweight edit = %d, want 400", resp.StatusCode) | ||
| 90 | } | ||
| 91 | } | ||