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.
Tighten up the issue and change review pages
564acd4b6de233740f3c19e297c4655e8952d40c · Keenan Nemetz · 15d ago · parent a464f892
Tighten up the issue and change review pages - Move contributor-branch CI approval from the Runs tab to the change request header, and return the allow form to that request. - Put issue and change actions in the page header, right aligned with the title, instead of duplicating them in the sidebar. - Drop sidebar details that repeated the header: state, author, age and reference. Keep labels, assignees, review base/head, approvals and checks. - Consolidate approval status into one section and remove the empty-state line now covered by the count. - Fix diff row spacing: keep pre-wrap on the code column only, so template whitespace in the gutter cells no longer creates blank lines. Update the contributor CI test, the HTMX fragment expectations and the documentation that named the Runs tab as the approval location.
server/auth_test.go +39−6
| @@ -400,6 +400,10 @@ func TestContributorCIIsHeldUntilWriterAllows(t *testing.T) { | |||
| 400 | 400 | t.Fatal(err) | |
| 401 | 401 | } | |
| 402 | 402 | ||
| 403 | base := workRepo(t, map[string]string{"readme.md": "base\n"}) | ||
| 404 | if _, errs, err := f.git(base, "push", f.sshURL(f.repo), "HEAD:refs/heads/main"); err != nil { | ||
| 405 | t.Fatalf("owner base push: %v\n%s", err, errs) | ||
| 406 | } | ||
| 403 | 407 | work := workRepo(t, workflowFile(`version: 1 | |
| 404 | 408 | jobs: | |
| 405 | 409 | test: | |
| @@ -433,32 +437,61 @@ jobs: | |||
| 433 | 437 | } | |
| 434 | 438 | ||
| 435 | 439 | contribSess, contribCookie := f.browserSession(t, contrib.ID) | |
| 436 | resp, body := f.browserGet(t, "/r/"+f.repo+"/runs", contribCookie) | ||
| 440 | resp, body := f.browserPost(t, "/r/"+f.repo+"/requests", contribCookie, url.Values{ | ||
| 441 | "csrf": {contribSess.CSRF}, "title": {"CI gate"}, "body": {"please review"}, | ||
| 442 | "base": {"main"}, "head": {ref}, | ||
| 443 | }) | ||
| 444 | if resp.StatusCode != http.StatusSeeOther { | ||
| 445 | t.Fatalf("contributor request: status=%d body=%s", resp.StatusCode, body) | ||
| 446 | } | ||
| 447 | list, err := metadata.LoadRequests(m.Git(), metadata.Ref) | ||
| 448 | if err != nil || len(list) != 1 { | ||
| 449 | t.Fatalf("requests = %v, err = %v", list, err) | ||
| 450 | } | ||
| 451 | requestID := list[0].ID | ||
| 452 | requestURL := "/r/" + f.repo + "/requests/" + requestID | ||
| 453 | |||
| 454 | // The Runs tab is history only; the approval belongs with the code under review. | ||
| 455 | resp, body = f.browserGet(t, "/r/"+f.repo+"/runs", contribCookie) | ||
| 456 | if resp.StatusCode != http.StatusOK || strings.Contains(body, "waiting for a maintainer") || strings.Contains(body, "Allow CI for this tip") { | ||
| 457 | t.Fatalf("runs tab still owns CI approval: status=%d body=%s", resp.StatusCode, body) | ||
| 458 | } | ||
| 459 | resp, body = f.browserGet(t, requestURL, contribCookie) | ||
| 437 | 460 | if resp.StatusCode != http.StatusOK || !strings.Contains(body, "waiting for a maintainer") { | |
| 438 | t.Fatalf("contributor branch status: status=%d body=%s", resp.StatusCode, body) | ||
| 461 | t.Fatalf("contributor request CI status: status=%d body=%s", resp.StatusCode, body) | ||
| 462 | } | ||
| 463 | if strings.Contains(body, "Allow CI for this tip") { | ||
| 464 | t.Fatalf("contributor saw the Allow CI control: body=%s", body) | ||
| 439 | 465 | } | |
| 440 | 466 | resp, _ = f.browserPost(t, "/r/"+f.repo+"/ci/allow", contribCookie, url.Values{ | |
| 441 | "csrf": {contribSess.CSRF}, "ref": {"refs/heads/" + ref}, | ||
| 467 | "csrf": {contribSess.CSRF}, "ref": {"refs/heads/" + ref}, "request": {requestID}, | ||
| 442 | 468 | }) | |
| 443 | 469 | if resp.StatusCode != http.StatusForbidden { | |
| 444 | 470 | t.Fatalf("contributor allowed their own CI: status=%d", resp.StatusCode) | |
| 445 | 471 | } | |
| 446 | 472 | ||
| 447 | 473 | ownerSess, ownerCookie := f.browserSession(t, f.ownerID) | |
| 448 | resp, body = f.browserGet(t, "/r/"+f.repo+"/runs", ownerCookie) | ||
| 474 | resp, body = f.browserGet(t, requestURL, ownerCookie) | ||
| 449 | 475 | if resp.StatusCode != http.StatusOK || !strings.Contains(body, "Allow CI for this tip") { | |
| 450 | t.Fatalf("maintainer CI control missing: status=%d body=%s", resp.StatusCode, body) | ||
| 476 | t.Fatalf("maintainer CI control missing from request: status=%d body=%s", resp.StatusCode, body) | ||
| 477 | } | ||
| 478 | resp, runsPage := f.browserGet(t, "/r/"+f.repo+"/runs", ownerCookie) | ||
| 479 | if resp.StatusCode != http.StatusOK || strings.Contains(runsPage, "Allow CI for this tip") || strings.Contains(runsPage, "Awaiting CI approval") { | ||
| 480 | t.Fatalf("runs tab should not show contributor CI approval: status=%d body=%s", resp.StatusCode, runsPage) | ||
| 451 | 481 | } | |
| 452 | 482 | resp, codePage := f.browserGet(t, "/r/"+f.repo+"/code", ownerCookie) | |
| 453 | 483 | if resp.StatusCode != http.StatusOK || strings.Contains(codePage, "Allow CI for this tip") { | |
| 454 | 484 | t.Fatalf("contributor CI control should not be in the code browser: status=%d body=%s", resp.StatusCode, codePage) | |
| 455 | 485 | } | |
| 456 | 486 | resp, body = f.browserPost(t, "/r/"+f.repo+"/ci/allow", ownerCookie, url.Values{ | |
| 457 | "csrf": {ownerSess.CSRF}, "ref": {"refs/heads/" + ref}, | ||
| 487 | "csrf": {ownerSess.CSRF}, "ref": {"refs/heads/" + ref}, "request": {requestID}, | ||
| 458 | 488 | }) | |
| 459 | 489 | if resp.StatusCode != http.StatusSeeOther { | |
| 460 | 490 | t.Fatalf("maintainer allow: status=%d body=%s", resp.StatusCode, body) | |
| 461 | 491 | } | |
| 492 | if loc := resp.Header.Get("Location"); loc != requestURL { | ||
| 493 | t.Fatalf("allow redirect = %q, want %q", loc, requestURL) | ||
| 494 | } | ||
| 462 | 495 | run := f.waitForRun(t, 5*time.Second, func(r *runs.Run) bool { return r.SHA == head }) | |
| 463 | 496 | if run.Ref != "refs/heads/"+ref || run.Status != runs.StatusQueued { | |
| 464 | 497 | t.Fatalf("queued run = %+v", run) | |