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.

Serve changes at /r/<repo>/changes

8fb7aabf534adf208cd795724b1e3d287dc62f5a · Keenan Nemetz · 3d ago · parent b492b557

Serve changes at /r/<repo>/changes

The metadata tree and the interface say changes; the browser was the last
place spelling the route requests. Rename every method on that subtree and
the URLs the server builds: redirects, activity and search results, the
number alias, code links, templates, and the CLI's page links.

The old route is gone rather than aliased. Nothing has been publicly
released, so there is no bookmark or client to keep working, and a redirect
would leave two spellings of the same page in the code.

Refs #204 (01M6S6685JFG8S64ZFF452217W).

server/issues_changes_test.go +76−76

@@ -172,18 +172,18 @@ func TestIssueAndChangeDetailsCanBeEditedInTheBrowser(t *testing.T) {
172 172 }
173 173
174 174 // A change: same controls, and the same writer.
175 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests", cookie, url.Values{
175 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes", cookie, url.Values{
176 176 "csrf": {sess.CSRF}, "title": {"First change draft"}, "body": {"original change body\n"},
177 177 "base": {"main"}, "head": {"feature"},
178 178 })
179 179 if resp.StatusCode != http.StatusSeeOther {
180 180 t.Fatalf("create change: %d\n%s", resp.StatusCode, body)
181 181 }
182 changeID := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/requests/")
182 changeID := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/changes/")
183 183 if changeID == "" {
184 184 t.Fatalf("no change id in %q", resp.Header.Get("Location"))
185 185 }
186 _, page = f.browserGet(t, "/r/"+f.repo+"/requests/"+changeID, cookie)
186 _, page = f.browserGet(t, "/r/"+f.repo+"/changes/"+changeID, cookie)
187 187 if !strings.Contains(page, "data-title-edit") || !strings.Contains(page, "data-title-edit-form") ||
188 188 !strings.Contains(page, `data-comment-menu`) || !strings.Contains(page, "Copy text") ||
189 189 !strings.Contains(page, "data-comment-edit") || !strings.Contains(page, `name="field" value="title"`) ||
@@ -192,16 +192,16 @@ func TestIssueAndChangeDetailsCanBeEditedInTheBrowser(t *testing.T) {
192 192 }
193 193 // The Files tab reuses the change head but has no description form there, so
194 194 // its title pencil must keep the ordinary link to the standalone page.
195 if _, files := f.browserGet(t, "/r/"+f.repo+"/requests/"+changeID+"/files", cookie); !strings.Contains(files, "/requests/"+changeID+"/edit") ||
195 if _, files := f.browserGet(t, "/r/"+f.repo+"/changes/"+changeID+"/files", cookie); !strings.Contains(files, "/changes/"+changeID+"/edit") ||
196 196 strings.Contains(files, "data-title-edit") {
197 197 t.Fatalf("the Files tab does not link to the edit page:\n%s", files)
198 198 }
199 resp, form = f.browserGet(t, "/r/"+f.repo+"/requests/"+changeID+"/edit", cookie)
200 if resp.StatusCode != http.StatusOK || !strings.Contains(form, `action="/r/`+f.repo+`/requests/`+changeID+`/edit"`) ||
199 resp, form = f.browserGet(t, "/r/"+f.repo+"/changes/"+changeID+"/edit", cookie)
200 if resp.StatusCode != http.StatusOK || !strings.Contains(form, `action="/r/`+f.repo+`/changes/`+changeID+`/edit"`) ||
201 201 !strings.Contains(form, `value="First change draft"`) || !strings.Contains(form, "original change body") {
202 202 t.Fatalf("change edit page = %d:\n%s", resp.StatusCode, form)
203 203 }
204 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests/"+changeID+"/edit", cookie, url.Values{
204 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+changeID+"/edit", cookie, url.Values{
205 205 "csrf": {sess.CSRF}, "title": {"Corrected change"}, "body": {"corrected change body\n"},
206 206 })
207 207 if resp.StatusCode != http.StatusSeeOther {
@@ -214,12 +214,12 @@ func TestIssueAndChangeDetailsCanBeEditedInTheBrowser(t *testing.T) {
214 214 if req.Title != "Corrected change" || strings.TrimSpace(req.Description) != "corrected change body" {
215 215 t.Fatalf("edited change = %q / %q", req.Title, req.Description)
216 216 }
217 if _, page := f.browserGet(t, "/r/"+f.repo+"/requests/"+changeID, cookie); !strings.Contains(page, "Corrected change") ||
217 if _, page := f.browserGet(t, "/r/"+f.repo+"/changes/"+changeID, cookie); !strings.Contains(page, "Corrected change") ||
218 218 !strings.Contains(page, "corrected change body") {
219 219 t.Fatalf("the change page does not show the edit:\n%s", page)
220 220 }
221 221
222 resp, body = f.htmxPost(t, "/r/"+f.repo+"/requests/"+changeID+"/edit", cookie, url.Values{
222 resp, body = f.htmxPost(t, "/r/"+f.repo+"/changes/"+changeID+"/edit", cookie, url.Values{
223 223 "csrf": {sess.CSRF}, "field": {"title"}, "title": {"In-place change"},
224 224 })
225 225 if resp.StatusCode != http.StatusOK || strings.Contains(body, "<!doctype html>") ||
@@ -231,7 +231,7 @@ func TestIssueAndChangeDetailsCanBeEditedInTheBrowser(t *testing.T) {
231 231 req.Title != "In-place change" || strings.TrimSpace(req.Description) != "corrected change body" {
232 232 t.Fatalf("inline change title edit = %+v, %v", req, err)
233 233 }
234 resp, body = f.htmxPost(t, "/r/"+f.repo+"/requests/"+changeID+"/edit", cookie, url.Values{
234 resp, body = f.htmxPost(t, "/r/"+f.repo+"/changes/"+changeID+"/edit", cookie, url.Values{
235 235 "csrf": {sess.CSRF}, "field": {"body"}, "body": {"in-place change body\n"},
236 236 })
237 237 if resp.StatusCode != http.StatusOK || strings.Contains(body, "<!doctype html>") ||
@@ -243,7 +243,7 @@ func TestIssueAndChangeDetailsCanBeEditedInTheBrowser(t *testing.T) {
243 243 req.Title != "In-place change" || strings.TrimSpace(req.Description) != "in-place change body" {
244 244 t.Fatalf("inline change description edit = %+v, %v", req, err)
245 245 }
246 resp, body = f.htmxPost(t, "/r/"+f.repo+"/requests/"+changeID+"/edit", cookie, url.Values{
246 resp, body = f.htmxPost(t, "/r/"+f.repo+"/changes/"+changeID+"/edit", cookie, url.Values{
247 247 "csrf": {sess.CSRF}, "field": {"title"}, "title": {""},
248 248 })
249 249 if resp.StatusCode != http.StatusOK || strings.Contains(body, "<!doctype html>") ||
@@ -262,7 +262,7 @@ func TestIssueAndChangeDetailsCanBeEditedInTheBrowser(t *testing.T) {
262 262 strangerSess, strangerCookie := f.browserSession(t, stranger.Name)
263 263 for _, path := range []string{
264 264 "/r/" + f.repo + "/issues/" + issueID + "/edit",
265 "/r/" + f.repo + "/requests/" + changeID + "/edit",
265 "/r/" + f.repo + "/changes/" + changeID + "/edit",
266 266 } {
267 267 if resp, _ := f.browserGet(t, path, strangerCookie); resp.StatusCode != http.StatusForbidden {
268 268 t.Fatalf("stranger got %d from %s, want 403", resp.StatusCode, path)
@@ -270,7 +270,7 @@ func TestIssueAndChangeDetailsCanBeEditedInTheBrowser(t *testing.T) {
270 270 }
271 271 for _, path := range []string{
272 272 "/r/" + f.repo + "/issues/" + issueID + "/edit",
273 "/r/" + f.repo + "/requests/" + changeID + "/edit",
273 "/r/" + f.repo + "/changes/" + changeID + "/edit",
274 274 } {
275 275 resp, _ := f.browserPost(t, path, strangerCookie, url.Values{
276 276 "csrf": {strangerSess.CSRF}, "title": {"Not mine"}, "body": {"not mine\n"},
@@ -296,52 +296,52 @@ func TestChangeCanBeDiscussedAndClosedInTheBrowser(t *testing.T) {
296 296 }
297 297
298 298 sess, cookie := f.browserSession(t, f.ownerID)
299 resp, body := f.browserPost(t, "/r/"+f.repo+"/requests", cookie, url.Values{
299 resp, body := f.browserPost(t, "/r/"+f.repo+"/changes", cookie, url.Values{
300 300 "csrf": {sess.CSRF}, "title": {"Browser change"}, "body": {"please review\n"},
301 301 "base": {"main"}, "head": {"feature"},
302 302 })
303 303 if resp.StatusCode != http.StatusSeeOther {
304 304 t.Fatalf("create change: %d\n%s", resp.StatusCode, body)
305 305 }
306 id := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/requests/")
306 id := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/changes/")
307 307 if id == "" {
308 308 t.Fatalf("no change id in %q", resp.Header.Get("Location"))
309 309 }
310 310
311 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests/"+id+"/comments", cookie, url.Values{
311 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+id+"/comments", cookie, url.Values{
312 312 "csrf": {sess.CSRF}, "body": {"Looks good from the browser.\n"},
313 313 })
314 314 if resp.StatusCode != http.StatusSeeOther {
315 315 t.Fatalf("comment: %d\n%s", resp.StatusCode, body)
316 316 }
317 _, page := f.browserGet(t, "/r/"+f.repo+"/requests/"+id, cookie)
317 _, page := f.browserGet(t, "/r/"+f.repo+"/changes/"+id, cookie)
318 318 if !strings.Contains(page, "Looks good from the browser.") {
319 319 t.Fatalf("the browser comment is not on the page:\n%s", page)
320 320 }
321 321
322 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests/"+id+"/state", cookie, url.Values{
322 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+id+"/state", cookie, url.Values{
323 323 "csrf": {sess.CSRF}, "state": {"closed"},
324 324 })
325 325 if resp.StatusCode != http.StatusSeeOther {
326 326 t.Fatalf("close change: %d\n%s", resp.StatusCode, body)
327 327 }
328 if _, list := f.browserGet(t, "/r/"+f.repo+"/requests", cookie); strings.Contains(list, "Browser change") {
328 if _, list := f.browserGet(t, "/r/"+f.repo+"/changes", cookie); strings.Contains(list, "Browser change") {
329 329 t.Fatalf("closed change is still in the open list:\n%s", list)
330 330 }
331 if _, list := f.browserGet(t, "/r/"+f.repo+"/requests?state=all", cookie); !strings.Contains(list, "Browser change") {
331 if _, list := f.browserGet(t, "/r/"+f.repo+"/changes?state=all", cookie); !strings.Contains(list, "Browser change") {
332 332 t.Fatalf("closed change is missing from the All list:\n%s", list)
333 333 }
334 if _, page := f.browserGet(t, "/r/"+f.repo+"/requests/"+id, cookie); !strings.Contains(page, "Reopen</button>") {
334 if _, page := f.browserGet(t, "/r/"+f.repo+"/changes/"+id, cookie); !strings.Contains(page, "Reopen</button>") {
335 335 t.Fatalf("closed change does not offer a reopen:\n%s", page)
336 336 }
337 337
338 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests/"+id+"/state", cookie, url.Values{
338 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+id+"/state", cookie, url.Values{
339 339 "csrf": {sess.CSRF}, "state": {"open"},
340 340 })
341 341 if resp.StatusCode != http.StatusSeeOther {
342 342 t.Fatalf("reopen change: %d\n%s", resp.StatusCode, body)
343 343 }
344 if _, page := f.browserGet(t, "/r/"+f.repo+"/requests/"+id, cookie); !strings.Contains(page, "Close</button>") ||
344 if _, page := f.browserGet(t, "/r/"+f.repo+"/changes/"+id, cookie); !strings.Contains(page, "Close</button>") ||
345 345 !strings.Contains(page, `class="danger"`) {
346 346 t.Fatalf("reopened change does not offer a close:\n%s", page)
347 347 }
@@ -370,8 +370,8 @@ func TestNewIssueAndChangePages(t *testing.T) {
370 370 t.Fatal("new issue page has no CSRF token")
371 371 }
372 372
373 resp, body = f.browserGet(t, "/r/"+f.repo+"/requests/new", cookie)
374 if resp.StatusCode != http.StatusOK || !strings.Contains(body, `action="/r/`+f.repo+`/requests"`) ||
373 resp, body = f.browserGet(t, "/r/"+f.repo+"/changes/new", cookie)
374 if resp.StatusCode != http.StatusOK || !strings.Contains(body, `action="/r/`+f.repo+`/changes"`) ||
375 375 !strings.Contains(body, "feature") || !strings.Contains(body, "Open change") {
376 376 t.Fatalf("new change page = %d:\n%s", resp.StatusCode, body)
377 377 }
@@ -380,7 +380,7 @@ func TestNewIssueAndChangePages(t *testing.T) {
380 380 if _, page := f.browserGet(t, "/r/"+f.repo+"/issues", cookie); !strings.Contains(page, "/issues/new") {
381 381 t.Fatal("issues list does not link to the new issue page")
382 382 }
383 if _, page := f.browserGet(t, "/r/"+f.repo+"/requests", cookie); !strings.Contains(page, "/requests/new") {
383 if _, page := f.browserGet(t, "/r/"+f.repo+"/changes", cookie); !strings.Contains(page, "/changes/new") {
384 384 t.Fatal("changes list does not link to the new change page")
385 385 }
386 386 }
@@ -538,25 +538,25 @@ func TestChangeLinksIssuesFromTheIssuesSection(t *testing.T) {
538 538 first := createIssue("First linked issue")
539 539 second := createIssue("Second linked issue")
540 540
541 resp, body := f.browserPost(t, "/r/"+f.repo+"/requests", cookie, url.Values{
541 resp, body := f.browserPost(t, "/r/"+f.repo+"/changes", cookie, url.Values{
542 542 "csrf": {sess.CSRF}, "title": {"Linked issues"}, "body": {"body\n"},
543 543 "base": {"main"}, "head": {"feature"}, "closes": {first},
544 544 })
545 545 if resp.StatusCode != http.StatusSeeOther {
546 546 t.Fatalf("create change: %d\n%s", resp.StatusCode, body)
547 547 }
548 id := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/requests/")
548 id := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/changes/")
549 549 if id == "" {
550 550 t.Fatalf("no change id in %q", resp.Header.Get("Location"))
551 551 }
552 552
553 553 // The Issues section uses the overview's issue rows and offers the link
554 554 // controls instead of the old one-line "Closes ... when merged".
555 _, page := f.browserGet(t, "/r/"+f.repo+"/requests/"+id, cookie)
555 _, page := f.browserGet(t, "/r/"+f.repo+"/changes/"+id, cookie)
556 556 for _, want := range []string{
557 557 `id="request-issues"`, "First linked issue", `value="` + first + `"`,
558 558 `value="remove"`, `request-unlink`, `class="request-issue-search"`,
559 `hx-get="/r/` + f.repo + `/requests/` + id + `/live"`,
559 `hx-get="/r/` + f.repo + `/changes/` + id + `/live"`,
560 560 `hx-trigger="every 20s [document.visibilityState === 'visible']"`, `hx-swap="none"`,
561 561 } {
562 562 if !strings.Contains(page, want) {
@@ -582,7 +582,7 @@ func TestChangeLinksIssuesFromTheIssuesSection(t *testing.T) {
582 582 t.Fatalf("the second issue has no number")
583 583 }
584 584 for _, query := range []string{"Second", "#" + strconv.Itoa(number), second[:8]} {
585 resp, results := f.htmxGet(t, "/r/"+f.repo+"/requests/"+id+"/issues/search?q="+url.QueryEscape(query), cookie)
585 resp, results := f.htmxGet(t, "/r/"+f.repo+"/changes/"+id+"/issues/search?q="+url.QueryEscape(query), cookie)
586 586 if resp.StatusCode != http.StatusOK || !strings.Contains(results, "Second linked issue") ||
587 587 !strings.Contains(results, `value="`+second+`"`) || !strings.Contains(results, `value="add"`) ||
588 588 !strings.Contains(results, `hx-target="#request-issues"`) ||
@@ -592,19 +592,19 @@ func TestChangeLinksIssuesFromTheIssuesSection(t *testing.T) {
592 592 }
593 593 // An issue already linked is not offered, so a shared title cannot hide the
594 594 // rows that are still available.
595 resp, results := f.htmxGet(t, "/r/"+f.repo+"/requests/"+id+"/issues/search?q=First", cookie)
595 resp, results := f.htmxGet(t, "/r/"+f.repo+"/changes/"+id+"/issues/search?q=First", cookie)
596 596 if resp.StatusCode != http.StatusOK || strings.Contains(results, "First linked issue") ||
597 597 !strings.Contains(results, "No issues match") {
598 598 t.Fatalf("a linked issue is still in the search results (%d):\n%s", resp.StatusCode, results)
599 599 }
600 600 // Clearing the field removes the popover rather than leaving a blank panel.
601 resp, results = f.htmxGet(t, "/r/"+f.repo+"/requests/"+id+"/issues/search?q=", cookie)
601 resp, results = f.htmxGet(t, "/r/"+f.repo+"/changes/"+id+"/issues/search?q=", cookie)
602 602 if resp.StatusCode != http.StatusOK || strings.TrimSpace(results) != "" {
603 603 t.Fatalf("an empty search left a panel (%d):\n%s", resp.StatusCode, results)
604 604 }
605 605
606 606 // Clicking a result posts its id, and the section comes back with both links.
607 resp, body = f.htmxPost(t, "/r/"+f.repo+"/requests/"+id+"/issues", cookie, url.Values{
607 resp, body = f.htmxPost(t, "/r/"+f.repo+"/changes/"+id+"/issues", cookie, url.Values{
608 608 "csrf": {sess.CSRF}, "action": {"add"}, "issue": {second},
609 609 })
610 610 if resp.StatusCode != http.StatusOK || !strings.Contains(body, `id="request-issues"`) ||
@@ -616,7 +616,7 @@ func TestChangeLinksIssuesFromTheIssuesSection(t *testing.T) {
616 616 }
617 617
618 618 // Removing a link is the same write, and the row disappears.
619 resp, body = f.htmxPost(t, "/r/"+f.repo+"/requests/"+id+"/issues", cookie, url.Values{
619 resp, body = f.htmxPost(t, "/r/"+f.repo+"/changes/"+id+"/issues", cookie, url.Values{
620 620 "csrf": {sess.CSRF}, "action": {"remove"}, "issue": {first},
621 621 })
622 622 if resp.StatusCode != http.StatusOK || !strings.Contains(body, `id="request-issues"`) ||
@@ -629,7 +629,7 @@ func TestChangeLinksIssuesFromTheIssuesSection(t *testing.T) {
629 629 }
630 630
631 631 // A duplicate says so beside the control instead of rewriting the list.
632 resp, body = f.htmxPost(t, "/r/"+f.repo+"/requests/"+id+"/issues", cookie, url.Values{
632 resp, body = f.htmxPost(t, "/r/"+f.repo+"/changes/"+id+"/issues", cookie, url.Values{
633 633 "csrf": {sess.CSRF}, "action": {"add"}, "issue": {second},
634 634 })
635 635 if resp.StatusCode != http.StatusOK || !strings.Contains(body, "already linked") {
@@ -638,18 +638,18 @@ func TestChangeLinksIssuesFromTheIssuesSection(t *testing.T) {
638 638
639 639 // A closed request keeps its record but offers no edits, and the writer
640 640 // refuses one posted anyway.
641 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests/"+id+"/state", cookie, url.Values{
641 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+id+"/state", cookie, url.Values{
642 642 "csrf": {sess.CSRF}, "state": {"closed"},
643 643 })
644 644 if resp.StatusCode != http.StatusSeeOther {
645 645 t.Fatalf("close change: %d\n%s", resp.StatusCode, body)
646 646 }
647 _, page = f.browserGet(t, "/r/"+f.repo+"/requests/"+id, cookie)
647 _, page = f.browserGet(t, "/r/"+f.repo+"/changes/"+id, cookie)
648 648 if !strings.Contains(page, "Second linked issue") || strings.Contains(page, `request-unlink`) ||
649 649 strings.Contains(page, `class="request-issue-search"`) {
650 650 t.Fatalf("a closed request does not show the right Issues section:\n%s", page)
651 651 }
652 resp, _ = f.htmxPost(t, "/r/"+f.repo+"/requests/"+id+"/issues", cookie, url.Values{
652 resp, _ = f.htmxPost(t, "/r/"+f.repo+"/changes/"+id+"/issues", cookie, url.Values{
653 653 "csrf": {sess.CSRF}, "action": {"remove"}, "issue": {second},
654 654 })
655 655 if resp.StatusCode != http.StatusForbidden {
@@ -682,7 +682,7 @@ func TestRepositoryCanRequireALinkedIssueForNewChanges(t *testing.T) {
682 682 }
683 683
684 684 sess, cookie := f.browserSession(t, f.ownerID)
685 resp, body := f.browserGet(t, "/r/"+f.repo+"/requests/new", cookie)
685 resp, body := f.browserGet(t, "/r/"+f.repo+"/changes/new", cookie)
686 686 if resp.StatusCode != http.StatusOK || !strings.Contains(body, "requires a linked issue") ||
687 687 !strings.Contains(body, "required") {
688 688 t.Fatalf("new change page does not explain the rule (%d):\n%s", resp.StatusCode, body)
@@ -692,7 +692,7 @@ func TestRepositoryCanRequireALinkedIssueForNewChanges(t *testing.T) {
692 692 "csrf": {sess.CSRF}, "title": {"Unlinked change"}, "body": {"body\n"},
693 693 "base": {"main"}, "head": {"feature"},
694 694 }
695 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests", cookie, form)
695 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes", cookie, form)
696 696 if resp.StatusCode != http.StatusBadRequest || !strings.Contains(body, "requires at least one linked issue") {
697 697 t.Fatalf("an unlinked change was accepted (%d):\n%s", resp.StatusCode, body)
698 698 }
@@ -710,11 +710,11 @@ func TestRepositoryCanRequireALinkedIssueForNewChanges(t *testing.T) {
710 710
711 711 form.Set("title", "Linked change")
712 712 form.Set("closes", issueID)
713 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests", cookie, form)
713 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes", cookie, form)
714 714 if resp.StatusCode != http.StatusSeeOther {
715 715 t.Fatalf("a linked change was rejected (%d):\n%s", resp.StatusCode, body)
716 716 }
717 id := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/requests/")
717 id := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/changes/")
718 718 got, err := metadata.GetRequest(m.Git(), id)
719 719 if err != nil {
720 720 t.Fatal(err)
@@ -805,14 +805,14 @@ func TestRequestSidebarAndDiscussionUpdateWithHtmx(t *testing.T) {
805 805 }
806 806
807 807 ownerSess, ownerCookie := f.browserSession(t, f.ownerID)
808 resp, body := f.browserPost(t, "/r/"+f.repo+"/requests", ownerCookie, url.Values{
808 resp, body := f.browserPost(t, "/r/"+f.repo+"/changes", ownerCookie, url.Values{
809 809 "csrf": {ownerSess.CSRF}, "title": {"HTMX change"}, "body": {"body\n"},
810 810 "base": {"main"}, "head": {"feature"},
811 811 })
812 812 if resp.StatusCode != http.StatusSeeOther {
813 813 t.Fatalf("create change: %d\n%s", resp.StatusCode, body)
814 814 }
815 id := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/requests/")
815 id := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/changes/")
816 816
817 817 // A maintainer approves; the data-only regions and the action buttons come
818 818 // back as out-of-band updates, so the Issues section and any open search are
@@ -822,7 +822,7 @@ func TestRequestSidebarAndDiscussionUpdateWithHtmx(t *testing.T) {
822 822 if err != nil {
823 823 t.Fatal(err)
824 824 }
825 resp, body = f.htmxPost(t, "/r/"+f.repo+"/requests/"+id+"/approve", writerCookie, url.Values{
825 resp, body = f.htmxPost(t, "/r/"+f.repo+"/changes/"+id+"/approve", writerCookie, url.Values{
826 826 "csrf": {writerSess.CSRF}, "base": {request.Base}, "head": {request.Head},
827 827 })
828 828 if resp.StatusCode != http.StatusOK || !strings.Contains(body, `hx-swap-oob="outerHTML"`) ||
@@ -838,7 +838,7 @@ func TestRequestSidebarAndDiscussionUpdateWithHtmx(t *testing.T) {
838 838
839 839 // The page's poll returns only out-of-band data regions. It has no sidebar
840 840 // wrapper and no form, so it cannot close a search or replace a picker.
841 resp, body = f.htmxGet(t, "/r/"+f.repo+"/requests/"+id+"/live", writerCookie)
841 resp, body = f.htmxGet(t, "/r/"+f.repo+"/changes/"+id+"/live", writerCookie)
842 842 if resp.StatusCode != http.StatusOK || strings.Contains(body, "<!doctype html>") ||
843 843 strings.Contains(body, "<form") || strings.Contains(body, `id="request-aside"`) ||
844 844 strings.Contains(body, `id="request-sidebar"`) || strings.Contains(body, `request-issues`) {
@@ -856,7 +856,7 @@ func TestRequestSidebarAndDiscussionUpdateWithHtmx(t *testing.T) {
856 856 }
857 857
858 858 // Posting a comment replaces only the discussion fragment.
859 resp, body = f.htmxPost(t, "/r/"+f.repo+"/requests/"+id+"/comments", ownerCookie, url.Values{
859 resp, body = f.htmxPost(t, "/r/"+f.repo+"/changes/"+id+"/comments", ownerCookie, url.Values{
860 860 "csrf": {ownerSess.CSRF}, "body": {"Review note over HTMX.\n"},
861 861 })
862 862 if resp.StatusCode != http.StatusOK || !strings.Contains(body, `id="request-discussion"`) ||
@@ -912,7 +912,7 @@ func TestIssueAndChangeListsReturnFragmentsForHtmx(t *testing.T) {
912 912 t.Fatal("issues fragment lost its HTMX target")
913 913 }
914 914
915 resp, body = f.htmxGet(t, "/r/"+f.repo+"/requests?state=all", "")
915 resp, body = f.htmxGet(t, "/r/"+f.repo+"/changes?state=all", "")
916 916 if resp.StatusCode != http.StatusOK || !strings.Contains(body, `id="change-list"`) ||
917 917 strings.Contains(body, "<!doctype html>") {
918 918 t.Fatalf("changes fragment = %d:\n%s", resp.StatusCode, body)
@@ -952,16 +952,16 @@ func TestRequestFilesListsEveryChangedFile(t *testing.T) {
952 952 t.Fatalf("feature push: %s", errs)
953 953 }
954 954 sess, cookie := f.browserSession(t, f.ownerID)
955 resp, body := f.browserPost(t, "/r/"+f.repo+"/requests", cookie, url.Values{
955 resp, body := f.browserPost(t, "/r/"+f.repo+"/changes", cookie, url.Values{
956 956 "csrf": {sess.CSRF}, "title": {"Every file"}, "body": {"body\n"},
957 957 "base": {"main"}, "head": {"feature"},
958 958 })
959 959 if resp.StatusCode != http.StatusSeeOther {
960 960 t.Fatalf("create change: %d\n%s", resp.StatusCode, body)
961 961 }
962 id := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/requests/")
962 id := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/changes/")
963 963
964 filesURL := "/r/" + f.repo + "/requests/" + id + "/files"
964 filesURL := "/r/" + f.repo + "/changes/" + id + "/files"
965 965 _, page := f.browserGet(t, filesURL, cookie)
966 966 if !strings.Contains(page, "5 changed") {
967 967 t.Errorf("the tree does not count every file:\n%.2000s", page)
@@ -1000,17 +1000,17 @@ func TestLineAnchoredReviewCommentFromTheWeb(t *testing.T) {
1000 1000 t.Fatalf("feature push: %s", errs)
1001 1001 }
1002 1002 sess, cookie := f.browserSession(t, f.ownerID)
1003 resp, body := f.browserPost(t, "/r/"+f.repo+"/requests", cookie, url.Values{
1003 resp, body := f.browserPost(t, "/r/"+f.repo+"/changes", cookie, url.Values{
1004 1004 "csrf": {sess.CSRF}, "title": {"Line comments"}, "body": {"body\n"},
1005 1005 "base": {"main"}, "head": {"feature"},
1006 1006 })
1007 1007 if resp.StatusCode != http.StatusSeeOther {
1008 1008 t.Fatalf("create change: %d\n%s", resp.StatusCode, body)
1009 1009 }
1010 id := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/requests/")
1010 id := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/changes/")
1011 1011
1012 1012 // The Files tab brings the changed file and its per-line comment button.
1013 filesURL := "/r/" + f.repo + "/requests/" + id + "/files"
1013 filesURL := "/r/" + f.repo + "/changes/" + id + "/files"
1014 1014 _, page := f.browserGet(t, filesURL, cookie)
1015 1015 if !strings.Contains(page, `id="review-tree"`) || !strings.Contains(page, `data-status="M"`) ||
1016 1016 !strings.Contains(page, filesURL+"/readme.md") {
@@ -1029,7 +1029,7 @@ func TestLineAnchoredReviewCommentFromTheWeb(t *testing.T) {
1029 1029 }
1030 1030
1031 1031 // HTMX inserts the composer under the line, with the anchor already filled in.
1032 resp, form := f.htmxGet(t, "/r/"+f.repo+"/requests/"+id+"/comment-form?path=readme.md&line=2&side=new", cookie)
1032 resp, form := f.htmxGet(t, "/r/"+f.repo+"/changes/"+id+"/comment-form?path=readme.md&line=2&side=new", cookie)
1033 1033 if resp.StatusCode != http.StatusOK || strings.Contains(form, "<!doctype html>") ||
1034 1034 !strings.Contains(form, `class="inline-comment"`) ||
1035 1035 !strings.Contains(form, `name="path" value="readme.md"`) ||
@@ -1039,7 +1039,7 @@ func TestLineAnchoredReviewCommentFromTheWeb(t *testing.T) {
1039 1039 }
1040 1040
1041 1041 // The old full-page fallback still opens the composer with the anchor filled in.
1042 _, page = f.browserGet(t, "/r/"+f.repo+"/requests/"+id+"?path=readme.md&line=2&side=new", cookie)
1042 _, page = f.browserGet(t, "/r/"+f.repo+"/changes/"+id+"?path=readme.md&line=2&side=new", cookie)
1043 1043 if !strings.Contains(page, "Commenting on") || !strings.Contains(page, `name="path" value="readme.md"`) ||
1044 1044 !strings.Contains(page, `name="line" value="2"`) {
1045 1045 t.Fatalf("anchored composer is not pre-filled:\n%s", page)
@@ -1048,7 +1048,7 @@ func TestLineAnchoredReviewCommentFromTheWeb(t *testing.T) {
1048 1048 // A rejected anchored comment re-renders the discussion with the anchor still
1049 1049 // chosen, so the reader keeps the line they picked instead of being sent back to
1050 1050 // the Files tab to choose it again.
1051 resp, body = f.htmxPost(t, "/r/"+f.repo+"/requests/"+id+"/comments", cookie, url.Values{
1051 resp, body = f.htmxPost(t, "/r/"+f.repo+"/changes/"+id+"/comments", cookie, url.Values{
1052 1052 "csrf": {sess.CSRF}, "body": {""},
1053 1053 "path": {"readme.md"}, "line": {"2"}, "side": {"new"},
1054 1054 })
@@ -1059,7 +1059,7 @@ func TestLineAnchoredReviewCommentFromTheWeb(t *testing.T) {
1059 1059 }
1060 1060
1061 1061 // Posting it through HTMX stores the anchor and returns the discussion.
1062 resp, body = f.htmxPost(t, "/r/"+f.repo+"/requests/"+id+"/comments", cookie, url.Values{
1062 resp, body = f.htmxPost(t, "/r/"+f.repo+"/changes/"+id+"/comments", cookie, url.Values{
1063 1063 "csrf": {sess.CSRF}, "body": {"This line needs a second look."},
1064 1064 "path": {"readme.md"}, "line": {"2"}, "side": {"new"},
1065 1065 })
@@ -1087,7 +1087,7 @@ func TestLineAnchoredReviewCommentFromTheWeb(t *testing.T) {
1087 1087 }
1088 1088
1089 1089 // A normal form post from the Files tab returns to that file's line note.
1090 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests/"+id+"/comments", cookie, url.Values{
1090 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+id+"/comments", cookie, url.Values{
1091 1091 "csrf": {sess.CSRF}, "body": {"Another line note."},
1092 1092 "path": {"readme.md"}, "line": {"2"}, "side": {"new"},
1093 1093 })
@@ -1171,29 +1171,29 @@ func TestIssueAndChangeCommentsThreadOneLevel(t *testing.T) {
1171 1171 if _, errs, err := f.git(feature, "push", f.sshURL(f.repo), "HEAD:refs/heads/feature"); err != nil {
1172 1172 t.Fatalf("feature push: %s", errs)
1173 1173 }
1174 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests", cookie, url.Values{
1174 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes", cookie, url.Values{
1175 1175 "csrf": {sess.CSRF}, "title": {"Threaded change"}, "body": {"body\n"},
1176 1176 "base": {"main"}, "head": {"feature"},
1177 1177 })
1178 1178 if resp.StatusCode != http.StatusSeeOther {
1179 1179 t.Fatalf("create change: %d\n%s", resp.StatusCode, body)
1180 1180 }
1181 requestID := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/requests/")
1182 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests/"+requestID+"/comments", cookie, url.Values{
1181 requestID := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/changes/")
1182 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+requestID+"/comments", cookie, url.Values{
1183 1183 "csrf": {sess.CSRF}, "body": {"Change root."},
1184 1184 })
1185 1185 rootID = commentID(resp)
1186 1186 if resp.StatusCode != http.StatusSeeOther || rootID == "" {
1187 1187 t.Fatalf("change root comment: %d %q\n%s", resp.StatusCode, rootID, body)
1188 1188 }
1189 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests/"+requestID+"/comments", cookie, url.Values{
1189 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+requestID+"/comments", cookie, url.Values{
1190 1190 "csrf": {sess.CSRF}, "body": {"Change reply."}, "reply_to": {rootID},
1191 1191 })
1192 1192 replyID = commentID(resp)
1193 1193 if resp.StatusCode != http.StatusSeeOther || replyID == "" {
1194 1194 t.Fatalf("change reply: %d %q\n%s", resp.StatusCode, replyID, body)
1195 1195 }
1196 _, page = f.browserGet(t, "/r/"+f.repo+"/requests/"+requestID, cookie)
1196 _, page = f.browserGet(t, "/r/"+f.repo+"/changes/"+requestID, cookie)
1197 1197 if !strings.Contains(page, `class="comment-replies"`) || !strings.Contains(page, `id="c-`+replyID+`"`) ||
1198 1198 !strings.Contains(page, "Change reply.") || !strings.Contains(page, `data-reply-to="`+replyID+`"`) ||
1199 1199 !strings.Contains(page, `class="reply-context"`) || !strings.Contains(page, `href="#c-`+rootID+`"`) ||
@@ -1283,41 +1283,41 @@ func TestCommentEditAndDeleteFromTheWeb(t *testing.T) {
1283 1283 if _, errs, err := f.git(feature, "push", f.sshURL(f.repo), "HEAD:refs/heads/feature"); err != nil {
1284 1284 t.Fatalf("feature push: %s", errs)
1285 1285 }
1286 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests", cookie, url.Values{
1286 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes", cookie, url.Values{
1287 1287 "csrf": {sess.CSRF}, "title": {"Editable change"}, "body": {"body\n"},
1288 1288 "base": {"main"}, "head": {"feature"},
1289 1289 })
1290 1290 if resp.StatusCode != http.StatusSeeOther {
1291 1291 t.Fatalf("create change: %d\n%s", resp.StatusCode, body)
1292 1292 }
1293 requestID := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/requests/")
1294 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests/"+requestID+"/comments", cookie, url.Values{
1293 requestID := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/changes/")
1294 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+requestID+"/comments", cookie, url.Values{
1295 1295 "csrf": {sess.CSRF}, "body": {"Original change root."},
1296 1296 })
1297 1297 rootID = commentID(resp)
1298 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests/"+requestID+"/comments", cookie, url.Values{
1298 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+requestID+"/comments", cookie, url.Values{
1299 1299 "csrf": {sess.CSRF}, "body": {"Change reply survives."}, "reply_to": {rootID},
1300 1300 })
1301 1301 if resp.StatusCode != http.StatusSeeOther {
1302 1302 t.Fatalf("change reply: %d\n%s", resp.StatusCode, body)
1303 1303 }
1304 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests/"+requestID+"/comments/"+rootID+"/edit", cookie, url.Values{
1304 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+requestID+"/comments/"+rootID+"/edit", cookie, url.Values{
1305 1305 "csrf": {sess.CSRF}, "body": {"Edited change root."},
1306 1306 })
1307 1307 if resp.StatusCode != http.StatusSeeOther {
1308 1308 t.Fatalf("change edit: %d\n%s", resp.StatusCode, body)
1309 1309 }
1310 _, page = f.browserGet(t, "/r/"+f.repo+"/requests/"+requestID, cookie)
1310 _, page = f.browserGet(t, "/r/"+f.repo+"/changes/"+requestID, cookie)
1311 1311 if !strings.Contains(page, "Edited change root.") || strings.Contains(page, "Original change root.") {
1312 1312 t.Fatalf("change edit did not replace the comment:\n%s", page)
1313 1313 }
1314 resp, body = f.browserPost(t, "/r/"+f.repo+"/requests/"+requestID+"/comments/"+rootID+"/delete", cookie, url.Values{
1314 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+requestID+"/comments/"+rootID+"/delete", cookie, url.Values{
1315 1315 "csrf": {sess.CSRF},
1316 1316 })
1317 1317 if resp.StatusCode != http.StatusSeeOther {
1318 1318 t.Fatalf("change delete: %d\n%s", resp.StatusCode, body)
1319 1319 }
1320 _, page = f.browserGet(t, "/r/"+f.repo+"/requests/"+requestID, cookie)
1320 _, page = f.browserGet(t, "/r/"+f.repo+"/changes/"+requestID, cookie)
1321 1321 if !strings.Contains(page, "Comment deleted") || !strings.Contains(page, "Change reply survives.") {
1322 1322 t.Fatalf("deleted change root did not become a tombstone:\n%s", page)
1323 1323 }
@@ -1346,12 +1346,12 @@ func TestLandingRedirectsToTheRequestPage(t *testing.T) {
1346 1346 }
1347 1347
1348 1348 sess, cookie := f.browserSession(t, f.ownerID)
1349 resp, body := f.browserPost(t, "/r/"+f.repo+"/requests/"+reqID+"/merge", cookie,
1349 resp, body := f.browserPost(t, "/r/"+f.repo+"/changes/"+reqID+"/merge", cookie,
1350 1350 url.Values{"csrf": {sess.CSRF}})
1351 1351 if resp.StatusCode != http.StatusSeeOther {
1352 1352 t.Fatalf("merge = %d, want 303:\n%s", resp.StatusCode, body)
1353 1353 }
1354 want := "/r/" + f.repo + "/requests/" + reqID
1354 want := "/r/" + f.repo + "/changes/" + reqID
1355 1355 if got := resp.Header.Get("Location"); got != want {
1356 1356 t.Fatalf("merge redirect = %q, want %q", got, want)
1357 1357 }
@@ -1535,7 +1535,7 @@ func TestChangeListShowsOneStatus(t *testing.T) {
1535 1535 t.Fatalf("changes push: %v", err)
1536 1536 }
1537 1537
1538 code, body := f.get("/r/" + f.repo + "/requests?state=all")
1538 code, body := f.get("/r/" + f.repo + "/changes?state=all")
1539 1539 if code != http.StatusOK {
1540 1540 t.Fatalf("change list: %d\n%s", code, body)
1541 1541 }