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.

Say what each change is waiting for in the list

7cc36ba0f4ba96d3f4938b13758b2cc20c22375a · Keenan Nemetz · 4d ago · parent 946d2fd4

Say what each change is waiting for in the list

A change spends most of its life in states "open" cannot describe: held for a
maintainer to allow CI, running its checks, failed, waiting for a reviewer,
conflicting with its base. The list now says which of those it is, in one
column, with a tooltip that explains the label.

The answer comes from facts a list can afford: the approvals it already counts,
one batched run-queue query for every head on the page, the contributor
allowance (one ref listing, not one per row), and — only for a change that would
otherwise read as ready — whether it still merges into its base. The full
landing plan stays on the detail page.

The rest of the row follows the issue list: the author and opening moved under
the title, the comment count sits at the end of that line, and the last column
is "Updated", the newest commit over the request's directory. The list sorts by
it. The separate Author, Approvals and Age columns are gone.

Closes #194

server/issues_changes_test.go +72

@@ -1482,3 +1482,75 @@ func TestIssueListShowsActivityColumnsAndSortsByUpdated(t *testing.T) {
1482 1482 t.Fatalf("issue list is not ordered by updated: edited at %d, other at %d\n%s", editedAt, otherAt, body)
1483 1483 }
1484 1484 }
1485
1486 // TestChangeListShowsOneStatus: the list answers "where is this change?" in one
1487 // column, from the approvals and checks the repository already has, and carries
1488 // the author and opening under the title like the issue list does.
1489 func TestChangeListShowsOneStatus(t *testing.T) {
1490 t.Parallel()
1491 f := newFixture(t)
1492 f.addWriter(t) // wilbur, so the approved change is not approved by its author
1493 f.publish(f.repo)
1494 base := workRepo(t, map[string]string{"readme.md": "v1\n"})
1495 if _, errs, err := f.git(base, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil {
1496 t.Fatalf("main push: %v\n%s", err, errs)
1497 }
1498 m := mustRepo(t, f)
1499 a, _ := m.ACL()
1500 a.Protected = []string{"refs/heads/main"}
1501 if err := m.SetACL(a, "test"); err != nil {
1502 t.Fatal(err)
1503 }
1504
1505 first, firstHead := f.cloneWithChange(t, map[string]string{"a.txt": "1\n"}, "change one")
1506 if _, errs, err := f.git(first, "push", f.sshURL(f.repo), "HEAD:refs/heads/feature-one"); err != nil {
1507 t.Fatalf("feature one push: %v\n%s", err, errs)
1508 }
1509 second, secondHead := f.cloneWithChange(t, map[string]string{"b.txt": "1\n"}, "change two")
1510 if _, errs, err := f.git(second, "push", f.sshURL(f.repo), "HEAD:refs/heads/feature-two"); err != nil {
1511 t.Fatalf("feature two push: %v\n%s", err, errs)
1512 }
1513
1514 const reviewID = "01M5JR2JK29KKMAA1T9MDNTR02"
1515 const approvedID = "01M5JR2JK29KKMAA1T9MDNTR03"
1516 const mergedID = "01M5JR2JK29KKMAA1T9MDNTR04"
1517 const closedID = "01M5JR2JK29KKMAA1T9MDNTR05"
1518 meta := f.newMetaRepo(t)
1519 if err := meta.apply(map[string]string{
1520 "requests/" + reviewID + "/request.md": requestFileTitled("Needs a review", "main", firstHead, "keenan", "body\n"),
1521 "requests/" + approvedID + "/request.md": requestFileTitled("Approved change", "main", secondHead, "wilbur", "body\n"),
1522 "requests/" + approvedID + "/approvals/" + f.accountName("keenan") + ".md": approvalFile("main", secondHead),
1523 "requests/" + mergedID + "/request.md": requestFileMerged("Landed change", "main", secondHead, "wilbur", "keenan", nil),
1524 "requests/" + closedID + "/request.md": requestFileState("Dropped change", "main", firstHead, "wilbur", "closed", "body\n"),
1525 }, "changes"); err != nil {
1526 t.Fatalf("changes push: %v", err)
1527 }
1528
1529 code, body := f.get("/r/" + f.repo + "/requests?state=all")
1530 if code != http.StatusOK {
1531 t.Fatalf("change list: %d\n%s", code, body)
1532 }
1533 for _, want := range []string{
1534 "<th>Status</th><th>Change</th><th>Base ← Head</th><th>Updated</th>",
1535 `class="change-status review"`,
1536 `title="0 of 1 required approvals."`,
1537 "Waiting for review",
1538 `class="change-status ready"`,
1539 "Ready to merge",
1540 `class="change-status merged"`,
1541 "Merged",
1542 `class="change-status closed"`,
1543 "Closed",
1544 `opened by <a class="user" href="/u/keenan">keenan</a>`,
1545 `opened by <a class="user" href="/u/wilbur">wilbur</a>`,
1546 } {
1547 if !strings.Contains(body, want) {
1548 t.Fatalf("change list is missing %q:\n%s", want, body)
1549 }
1550 }
1551 for _, gone := range []string{"<th>Author</th>", "<th>Approvals</th>", "<th>Age</th>", "<th>Replies</th>"} {
1552 if strings.Contains(body, gone) {
1553 t.Fatalf("change list still has %s:\n%s", gone, body)
1554 }
1555 }
1556 }