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.
Show open work on repository tabs and overview
a196e404c86a5b9607bd226c7229ffc89a37d47f · Keenan Nemetz · 11d ago · parent 0e35a6a0
Show open work on repository tabs and overview The repository header now counts open issues and change requests, and the overview sidebar lists the newest open issues and change requests with links to their full lists. Closed and merged records stay out of both: the counts and the overview describe what is still being worked on. Issue counts come from the derived index; change requests are read from the metadata branch, where their state lives. A failed read leaves a zero or an empty section rather than turning the overview into an error page.
server/ui_test.go +107−2
| @@ -41,7 +41,7 @@ func TestRepositoryPagesShareTheTabHeader(t *testing.T) { | |||
| 41 | 41 | if code != 200 { | |
| 42 | 42 | t.Fatalf("%s = %d", path, code) | |
| 43 | 43 | } | |
| 44 | for _, want := range []string{`class="repotab`, ">Activity<", ">Code<", ">Issues<", ">Change requests<", ">Releases<", ">Runs<", ">Locks<"} { | ||
| 44 | for _, want := range []string{`class="repotab`, ">Activity<", ">Code<", ">Issues <span class=\"chip-count\">0</span><", ">Change requests <span class=\"chip-count\">0</span><", ">Releases<", ">Runs<", ">Locks<"} { | ||
| 45 | 45 | if !strings.Contains(body, want) { | |
| 46 | 46 | t.Fatalf("%s is missing the %s tab:\n%s", path, want, body) | |
| 47 | 47 | } | |
| @@ -52,6 +52,52 @@ func TestRepositoryPagesShareTheTabHeader(t *testing.T) { | |||
| 52 | 52 | } | |
| 53 | 53 | } | |
| 54 | 54 | ||
| 55 | func TestRepoTabsCountOpenIssuesAndRequests(t *testing.T) { | ||
| 56 | t.Parallel() | ||
| 57 | f := newFixture(t) | ||
| 58 | f.publish(f.repo) | ||
| 59 | |||
| 60 | // Two issues, one closed: the badge counts what the Issues tab lists by | ||
| 61 | // default, not every record on the metadata branch. | ||
| 62 | work := f.issuesRepo(t, map[string]string{ | ||
| 63 | "issues/01M5JR2JK29KKMAA1T9MDNTQ01/issue.md": issueFile( | ||
| 64 | "Still open", "keenan", "open", "2026-10-22T21:00:00Z", nil, ""), | ||
| 65 | "issues/01M5JR2JK29KKMAA1T9MDNTQ02/issue.md": issueFile( | ||
| 66 | "Already closed", "keenan", "closed", "2026-10-22T21:00:00Z", nil, ""), | ||
| 67 | }) | ||
| 68 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "HEAD:refs/heads/vole"); err != nil { | ||
| 69 | t.Fatalf("push issues: %v\n%s", err, errs) | ||
| 70 | } | ||
| 71 | |||
| 72 | // The request only has to exist and be open for the header to count it; the | ||
| 73 | // tab count does not read the branch or the diff. | ||
| 74 | head := strings.Repeat("a", 40) | ||
| 75 | meta := f.newMetaRepo(t) | ||
| 76 | if err := meta.apply(map[string]string{ | ||
| 77 | "requests/01M5JR2JK29KKMAA1T9MDNTR01/request.md": requestFileTitled( | ||
| 78 | "Still open", "main", head, "keenan", "open\n"), | ||
| 79 | "requests/01M5JR2JK29KKMAA1T9MDNTR02/request.md": requestFileState( | ||
| 80 | "Already closed", "main", head, "keenan", "closed", "closed\n"), | ||
| 81 | }, "requests"); err != nil { | ||
| 82 | t.Fatalf("requests push: %v", err) | ||
| 83 | } | ||
| 84 | |||
| 85 | // The header is shared, so a page that shows nothing but code must carry the | ||
| 86 | // same counts as the lists themselves. | ||
| 87 | code, body := f.get("/r/" + f.repo + "/code") | ||
| 88 | if code != 200 { | ||
| 89 | t.Fatalf("code page = %d", code) | ||
| 90 | } | ||
| 91 | for _, want := range []string{ | ||
| 92 | `>Issues <span class="chip-count">1</span>`, | ||
| 93 | `>Change requests <span class="chip-count">1</span>`, | ||
| 94 | } { | ||
| 95 | if !strings.Contains(body, want) { | ||
| 96 | t.Fatalf("the repository tabs do not show %q:\n%s", want, body) | ||
| 97 | } | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 55 | 101 | func TestRepoTabShowsNameAndVisibility(t *testing.T) { | |
| 56 | 102 | t.Parallel() | |
| 57 | 103 | f := newFixture(t) | |
| @@ -450,7 +496,7 @@ func TestOverviewAndActivityPages(t *testing.T) { | |||
| 450 | 496 | if code != 200 { | |
| 451 | 497 | t.Fatalf("overview = %d", code) | |
| 452 | 498 | } | |
| 453 | for _, want := range []string{"Overview project", "Recent commits", "Recent runs", "contributors", "Languages", "Go"} { | ||
| 499 | for _, want := range []string{"Overview project", "Recent commits", "Recent issues", "Recent change requests", "No open issues.", "No open change requests.", "Recent runs", "contributors", "Languages", "Go"} { | ||
| 454 | 500 | if !strings.Contains(body, want) { | |
| 455 | 501 | t.Fatalf("overview page does not show %q:\n%s", want, body) | |
| 456 | 502 | } | |
| @@ -467,6 +513,65 @@ func TestOverviewAndActivityPages(t *testing.T) { | |||
| 467 | 513 | } | |
| 468 | 514 | } | |
| 469 | 515 | ||
| 516 | func TestOverviewShowsRecentOpenIssuesAndChanges(t *testing.T) { | ||
| 517 | t.Parallel() | ||
| 518 | f := newFixture(t) | ||
| 519 | f.publish(f.repo) | ||
| 520 | work := workRepo(t, map[string]string{"README.md": "# Overview project\n"}) | ||
| 521 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil { | ||
| 522 | t.Fatalf("push: %s", errs) | ||
| 523 | } | ||
| 524 | |||
| 525 | const ( | ||
| 526 | openIssueID = "01M5JR2JK29KKMAA1T9MDNTP01" | ||
| 527 | closedIssueID = "01M5JR2JK29KKMAA1T9MDNTP02" | ||
| 528 | ) | ||
| 529 | issues := f.issuesRepo(t, map[string]string{ | ||
| 530 | "issues/" + openIssueID + "/issue.md": issueFile( | ||
| 531 | "Overview open issue", "keenan", "open", "2026-10-22T21:00:00Z", nil, ""), | ||
| 532 | "issues/" + closedIssueID + "/issue.md": issueFile( | ||
| 533 | "Overview closed issue", "keenan", "closed", "2026-10-23T21:00:00Z", nil, ""), | ||
| 534 | }) | ||
| 535 | if _, errs, err := f.git(issues, "push", f.sshURL(f.repo), "HEAD:refs/heads/vole"); err != nil { | ||
| 536 | t.Fatalf("push issues: %v\n%s", err, errs) | ||
| 537 | } | ||
| 538 | |||
| 539 | const ( | ||
| 540 | openChangeID = "01M5JR2JK29KKMAA1T9MDNTR01" | ||
| 541 | closedChangeID = "01M5JR2JK29KKMAA1T9MDNTR02" | ||
| 542 | ) | ||
| 543 | head := strings.Repeat("a", 40) | ||
| 544 | meta := f.newMetaRepo(t) | ||
| 545 | if err := meta.apply(map[string]string{ | ||
| 546 | "requests/" + openChangeID + "/request.md": requestFileState( | ||
| 547 | "Overview open change", "main", head, "keenan", "open", "open\n"), | ||
| 548 | "requests/" + closedChangeID + "/request.md": requestFileState( | ||
| 549 | "Overview closed change", "main", head, "keenan", "closed", "closed\n"), | ||
| 550 | }, "requests"); err != nil { | ||
| 551 | t.Fatalf("requests push: %v", err) | ||
| 552 | } | ||
| 553 | |||
| 554 | code, body := f.get("/r/" + f.repo) | ||
| 555 | if code != 200 { | ||
| 556 | t.Fatalf("overview = %d", code) | ||
| 557 | } | ||
| 558 | for _, want := range []string{ | ||
| 559 | "Recent issues", "Recent change requests", | ||
| 560 | "Overview open issue", "Overview open change", | ||
| 561 | `href="/r/` + f.repo + `/issues/` + openIssueID + `"`, | ||
| 562 | `href="/r/` + f.repo + `/requests/` + openChangeID + `"`, | ||
| 563 | } { | ||
| 564 | if !strings.Contains(body, want) { | ||
| 565 | t.Fatalf("overview does not show %q:\n%s", want, body) | ||
| 566 | } | ||
| 567 | } | ||
| 568 | for _, gone := range []string{"Overview closed issue", "Overview closed change"} { | ||
| 569 | if strings.Contains(body, gone) { | ||
| 570 | t.Fatalf("overview shows closed record %q:\n%s", gone, body) | ||
| 571 | } | ||
| 572 | } | ||
| 573 | } | ||
| 574 | |||
| 470 | 575 | func TestCodeBrowserTreeAndFragments(t *testing.T) { | |
| 471 | 576 | t.Parallel() | |
| 472 | 577 | f := newFixture(t) | |