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/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)