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.

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)