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/web/overview.go +102−27
| @@ -12,11 +12,14 @@ import ( | |||
| 12 | 12 | "path" | |
| 13 | 13 | "sort" | |
| 14 | 14 | "strings" | |
| 15 | "time" | ||
| 15 | 16 | ||
| 16 | 17 | "github.com/rodent-software/vole/ci/runs" | |
| 17 | 18 | "github.com/rodent-software/vole/core/git" | |
| 19 | "github.com/rodent-software/vole/forge/index" | ||
| 18 | 20 | "github.com/rodent-software/vole/forge/repo" | |
| 19 | 21 | "github.com/rodent-software/vole/internal/format" | |
| 22 | "github.com/rodent-software/vole/metadata" | ||
| 20 | 23 | ) | |
| 21 | 24 | ||
| 22 | 25 | // maxReadmeBytes is the largest README rendered on the overview. A file past it | |
| @@ -38,6 +41,22 @@ type contributorView struct { | |||
| 38 | 41 | Tone int | |
| 39 | 42 | } | |
| 40 | 43 | ||
| 44 | // overviewEntity is the small shape the overview's issue and change sections | ||
| 45 | // need: enough to link, title, name the author and say how old it is. The list | ||
| 46 | // pages carry richer rows; the sidebar says only what fits beside a README. | ||
| 47 | type overviewEntity struct { | ||
| 48 | ID string | ||
| 49 | Title string | ||
| 50 | Author string | ||
| 51 | Created time.Time | ||
| 52 | Number int | ||
| 53 | } | ||
| 54 | |||
| 55 | // overviewSideLimit is how many open issues and change requests the overview | ||
| 56 | // shows. Like the commit and run sections, it is a pointer to the full list, not | ||
| 57 | // a replacement for it. | ||
| 58 | const overviewSideLimit = 5 | ||
| 59 | |||
| 41 | 60 | func (w *Web) Overview(rw http.ResponseWriter, r *http.Request) { | |
| 42 | 61 | m, ok := w.open(rw, r) | |
| 43 | 62 | if !ok { | |
| @@ -82,42 +101,98 @@ func (w *Web) Overview(rw http.ResponseWriter, r *http.Request) { | |||
| 82 | 101 | recentRuns, _ = w.Runs.Recent(m.Name, 5) | |
| 83 | 102 | } | |
| 84 | 103 | ||
| 104 | recentIssues, recentRequests := w.overviewRecent(m) | ||
| 105 | |||
| 85 | 106 | readme, readmeName := w.readmeAt(m, ref, commit) | |
| 86 | 107 | site := w.siteURL(r, m.Name) | |
| 87 | 108 | ||
| 88 | 109 | w.render(rw, "overview", struct { | |
| 89 | 110 | Page | |
| 90 | Name string | ||
| 91 | Ref string | ||
| 92 | Commit string | ||
| 93 | CloneURL string | ||
| 94 | SSHURL string | ||
| 95 | Site string | ||
| 96 | Readme template.HTML | ||
| 97 | ReadmeName string | ||
| 98 | Languages []languageShare | ||
| 99 | Commits []git.Commit | ||
| 100 | Runs []*runs.Run | ||
| 101 | Tags []git.Tag | ||
| 102 | Contributors []contributorView | ||
| 111 | Name string | ||
| 112 | Ref string | ||
| 113 | Commit string | ||
| 114 | CloneURL string | ||
| 115 | SSHURL string | ||
| 116 | Site string | ||
| 117 | Readme template.HTML | ||
| 118 | ReadmeName string | ||
| 119 | Languages []languageShare | ||
| 120 | Commits []git.Commit | ||
| 121 | RecentIssues []overviewEntity | ||
| 122 | RecentRequests []overviewEntity | ||
| 123 | Runs []*runs.Run | ||
| 124 | Tags []git.Tag | ||
| 125 | Contributors []contributorView | ||
| 103 | 126 | }{ | |
| 104 | Page: w.page(r, m.Name), | ||
| 105 | Name: m.Name, | ||
| 106 | Ref: ref, | ||
| 107 | Commit: commit, | ||
| 108 | CloneURL: w.cloneURL(r, m.Name), | ||
| 109 | SSHURL: sshURL(w.Cfg.SSHAddr, m.Name), | ||
| 110 | Site: site, | ||
| 111 | Readme: readme, | ||
| 112 | ReadmeName: readmeName, | ||
| 113 | Languages: languages, | ||
| 114 | Commits: commits, | ||
| 115 | Runs: recentRuns, | ||
| 116 | Tags: tags, | ||
| 117 | Contributors: contributors, | ||
| 127 | Page: w.page(r, m.Name), | ||
| 128 | Name: m.Name, | ||
| 129 | Ref: ref, | ||
| 130 | Commit: commit, | ||
| 131 | CloneURL: w.cloneURL(r, m.Name), | ||
| 132 | SSHURL: sshURL(w.Cfg.SSHAddr, m.Name), | ||
| 133 | Site: site, | ||
| 134 | Readme: readme, | ||
| 135 | ReadmeName: readmeName, | ||
| 136 | Languages: languages, | ||
| 137 | Commits: commits, | ||
| 138 | RecentIssues: recentIssues, | ||
| 139 | RecentRequests: recentRequests, | ||
| 140 | Runs: recentRuns, | ||
| 141 | Tags: tags, | ||
| 142 | Contributors: contributors, | ||
| 118 | 143 | }, 0) | |
| 119 | 144 | } | |
| 120 | 145 | ||
| 146 | // overviewRecent returns the newest open issues and change requests for the | ||
| 147 | // overview sidebar. Closed and merged records are history and already have a | ||
| 148 | // home in their own lists; the overview is a map of what is still being worked | ||
| 149 | // on. A failed read leaves an empty section rather than failing the README page. | ||
| 150 | func (w *Web) overviewRecent(m *repo.Meta) (issues, requests []overviewEntity) { | ||
| 151 | var numbers map[string]int | ||
| 152 | if w.Index != nil { | ||
| 153 | if err := w.Index.Ensure(m); err == nil { | ||
| 154 | if nums, err := w.Index.Numbers(m.ID); err == nil { | ||
| 155 | numbers = nums | ||
| 156 | } | ||
| 157 | if rows, err := w.Index.Issues(m.ID, index.Filter{State: "open"}); err == nil { | ||
| 158 | for _, row := range rows { | ||
| 159 | issues = append(issues, overviewEntity{ | ||
| 160 | ID: row.ID, Title: row.Title, Author: row.Author, | ||
| 161 | Created: row.Created, Number: row.Number, | ||
| 162 | }) | ||
| 163 | } | ||
| 164 | } | ||
| 165 | } | ||
| 166 | } | ||
| 167 | if list, err := metadata.LoadRequests(m.Git(), metadata.Ref); err == nil { | ||
| 168 | for _, req := range list { | ||
| 169 | if req.Open() { | ||
| 170 | requests = append(requests, overviewEntity{ | ||
| 171 | ID: req.ID, Title: req.Title, Author: req.Author, | ||
| 172 | Created: req.Created, Number: numbers[req.ID], | ||
| 173 | }) | ||
| 174 | } | ||
| 175 | } | ||
| 176 | } | ||
| 177 | return newestOverview(issues), newestOverview(requests) | ||
| 178 | } | ||
| 179 | |||
| 180 | // newestOverview puts the newest entity first and keeps only what the sidebar | ||
| 181 | // shows. Created is the human fact the row prints, so it decides the order; | ||
| 182 | // the id breaks a tie without depending on map iteration or a ULID's spelling. | ||
| 183 | func newestOverview(list []overviewEntity) []overviewEntity { | ||
| 184 | sort.SliceStable(list, func(i, j int) bool { | ||
| 185 | if !list[i].Created.Equal(list[j].Created) { | ||
| 186 | return list[i].Created.After(list[j].Created) | ||
| 187 | } | ||
| 188 | return list[i].ID > list[j].ID | ||
| 189 | }) | ||
| 190 | if len(list) > overviewSideLimit { | ||
| 191 | list = list[:overviewSideLimit] | ||
| 192 | } | ||
| 193 | return list | ||
| 194 | } | ||
| 195 | |||
| 121 | 196 | // readmeAt returns the rendered README and the name it was found under. The name | |
| 122 | 197 | // is shown above the body: a rendered file with no filename is harder to trust. | |
| 123 | 198 | // The ref travels with it because a relative link in the README has to resolve to | |