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.
Build out change review and threaded discussions
f6f4b1db24292d1df877680714952a9c568e2b78 · Keenan Nemetz · 15d ago · parent 564acd4b
Build out change review and threaded discussions - Add Conversation, Files and Runs tabs to change requests. Files uses the code-browser shape with a changed-file tree and one selected-file diff; Runs lists the head commit's workflow results. - Show anchored review comments beneath the line, with a collapsed excerpt read from the exact blob that was reviewed, and return posted anchors to their line note. - Add comment threads: any comment can be replied to, the UI flattens the descendants under one root, reply context links back to the parent, and reply targets feed a single composer at the foot of the discussion. - Collapse reply groups while keeping the root visible, and scroll to the composer when Reply is selected. - Add comment context menus with Reply, Copy text, Copy link, Edit and Delete. Add web edit/delete routes with author or admin checks; deletion leaves a tombstone so replies keep their thread. - Support --reply-to in the issue and change CLI commands, and treat a missing parent as a tombstone rather than an invalid tree. - Fix the diff comment gutter, action button colours, reply labels, and keep the Files tree and diff pane aligned. Update metadata validation, web templates, CSS, JavaScript, tests, and the CLI/change-request/issue docs.
server/auth_test.go +15−6
| @@ -453,14 +453,14 @@ jobs: | |||
| 453 | 453 | ||
| 454 | 454 | // The Runs tab is history only; the approval belongs with the code under review. | |
| 455 | 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") { | ||
| 456 | if resp.StatusCode != http.StatusOK || strings.Contains(body, "waiting for a maintainer") || strings.Contains(body, "Allow CI</button>") { | ||
| 457 | 457 | t.Fatalf("runs tab still owns CI approval: status=%d body=%s", resp.StatusCode, body) | |
| 458 | 458 | } | |
| 459 | 459 | resp, body = f.browserGet(t, requestURL, contribCookie) | |
| 460 | 460 | if resp.StatusCode != http.StatusOK || !strings.Contains(body, "waiting for a maintainer") { | |
| 461 | 461 | t.Fatalf("contributor request CI status: status=%d body=%s", resp.StatusCode, body) | |
| 462 | 462 | } | |
| 463 | if strings.Contains(body, "Allow CI for this tip") { | ||
| 463 | if strings.Contains(body, "Allow CI</button>") { | ||
| 464 | 464 | t.Fatalf("contributor saw the Allow CI control: body=%s", body) | |
| 465 | 465 | } | |
| 466 | 466 | resp, _ = f.browserPost(t, "/r/"+f.repo+"/ci/allow", contribCookie, url.Values{ | |
| @@ -472,15 +472,17 @@ jobs: | |||
| 472 | 472 | ||
| 473 | 473 | ownerSess, ownerCookie := f.browserSession(t, f.ownerID) | |
| 474 | 474 | resp, body = f.browserGet(t, requestURL, ownerCookie) | |
| 475 | if resp.StatusCode != http.StatusOK || !strings.Contains(body, "Allow CI for this tip") { | ||
| 476 | t.Fatalf("maintainer CI control missing from request: status=%d body=%s", resp.StatusCode, body) | ||
| 475 | if resp.StatusCode != http.StatusOK || !strings.Contains(body, "Allow CI</button>") || | ||
| 476 | !strings.Contains(body, `class="warn"`) || !strings.Contains(body, "Approve</button>") || | ||
| 477 | !strings.Contains(body, `class="success"`) || !strings.Contains(body, "View runs") { | ||
| 478 | t.Fatalf("maintainer actions missing from request: status=%d body=%s", resp.StatusCode, body) | ||
| 477 | 479 | } | |
| 478 | 480 | 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") { | ||
| 481 | if resp.StatusCode != http.StatusOK || strings.Contains(runsPage, "Allow CI</button>") || strings.Contains(runsPage, "Awaiting CI approval") { | ||
| 480 | 482 | t.Fatalf("runs tab should not show contributor CI approval: status=%d body=%s", resp.StatusCode, runsPage) | |
| 481 | 483 | } | |
| 482 | 484 | resp, codePage := f.browserGet(t, "/r/"+f.repo+"/code", ownerCookie) | |
| 483 | if resp.StatusCode != http.StatusOK || strings.Contains(codePage, "Allow CI for this tip") { | ||
| 485 | if resp.StatusCode != http.StatusOK || strings.Contains(codePage, "Allow CI</button>") { | ||
| 484 | 486 | t.Fatalf("contributor CI control should not be in the code browser: status=%d body=%s", resp.StatusCode, codePage) | |
| 485 | 487 | } | |
| 486 | 488 | resp, body = f.browserPost(t, "/r/"+f.repo+"/ci/allow", ownerCookie, url.Values{ | |
| @@ -496,6 +498,13 @@ jobs: | |||
| 496 | 498 | if run.Ref != "refs/heads/"+ref || run.Status != runs.StatusQueued { | |
| 497 | 499 | t.Fatalf("queued run = %+v", run) | |
| 498 | 500 | } | |
| 501 | |||
| 502 | resp, body = f.browserGet(t, requestURL+"/runs", ownerCookie) | ||
| 503 | if resp.StatusCode != http.StatusOK || !strings.Contains(body, "/runs/"+run.ID) || | ||
| 504 | !strings.Contains(body, "queued") || !strings.Contains(body, "Workflow results") || | ||
| 505 | !strings.Contains(body, "View runs") || !strings.Contains(body, "</html>") { | ||
| 506 | t.Fatalf("request runs tab: status=%d body=%s", resp.StatusCode, body) | ||
| 507 | } | ||
| 499 | 508 | } | |
| 500 | 509 | ||
| 501 | 510 | func TestOpenRegistrationPagesAndBegin(t *testing.T) { | |