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.

Read the change list and its counts from the index

b739915c0941be0f97cef8b96fd28d8f4c4685ae · Keenan Nemetz · 10d ago · parent 7f547ca3

Read the change list and its counts from the index

The header, the change list and the release artifact list now come from
the derived rows instead of the metadata branch:

- The header's open-changes badge is one COUNT, so a page that only shows
  the badge no longer reads `ls-tree`, `batch-check` and `batch` on every
  view. That is the read that grew with the repository's issues and
  comments.
- The change list, its state chips and its approval counts are queries.
  An approval still only counts when it names the request's current head
  and base, and eligibility is still the ACL's decision, so a stale
  approval is shown as history exactly as before.
- The release page's artifact list is the indexed pointer set.
- A change page reads its one request rather than every request; the
  conversation page keeps reading git, so the record it renders is
  authoritative rather than a copy.

On the fixture (40 files, 30 issues, 30 changes), git calls per page:

  issues list    5 -> 2        change list   6 -> 3
  releases       6 -> 3        commits       7 -> 4
  overview      19 -> 6 (repeat)  code       12 -> 9
  change view   14 -> 12

The remaining calls are the one `rev-parse` the refresh needs, the branch
and tree reads the page itself shows, and the detail pages' own git read.

server/web/requests.go +114−54

@@ -4,9 +4,10 @@ package web
4 4 // conversation page is request_view.go and the Files tab is request_files.go;
5 5 // this file is the surface a reader arrives at.
6 6 //
7 // A change request is read straight from git rather than through the index. The
8 // volume is small and the branch is authoritative, so there is no second copy to
9 // go stale.
7 // The list and the counts are read from the index, refreshed at the start of the
8 // request from the metadata branch. The conversation page still reads its one
9 // request from git: the index is for finding and listing, git is for rendering
10 // the record itself.
10 11
11 12 import (
12 13 "net/http"
@@ -14,15 +15,32 @@ import (
14 15 "sort"
15 16 "strconv"
16 17 "strings"
18 "time"
17 19
18 20 "github.com/rodent-software/vole/forge/access"
21 "github.com/rodent-software/vole/forge/index"
19 22 "github.com/rodent-software/vole/forge/repo"
20 23 "github.com/rodent-software/vole/metadata"
21 24 )
22 25
23 // requestRow is a request plus the number of approvals that still name its
24 // current head and base. The count is per row because an approval of an older
25 // head is history, not a vote for what is being proposed now.
26 // requestRow is one row of the change list: the indexed request plus the number
27 // of approvals that still name its current head and base. The count is per row
28 // because an approval of an older head is history, not a vote for what is being
29 // proposed now.
30 type requestRow struct {
31 ID string
32 Title string
33 State string
34 Author string
35 Base string
36 Head string
37 HeadRef string
38 Created time.Time
39 CommentCount int
40 Number int
41 Current int
42 }
43
26 44 // requestFormData is shared by the list page's New change button and the
27 45 // dedicated form page: the same branch rules must decide what the picker offers.
28 46 type requestFormData struct {
@@ -80,26 +98,14 @@ func headBranches(all []string, acl *access.ACL, account string) []string {
80 98 return out
81 99 }
82 100
83 type requestRow struct {
84 *metadata.Request
85 Current int
86 // Number is the `#42` alias from the numbers map the page was built with.
87 Number int
88 }
89
90 // Requests lists a repo's change requests. They are read straight from git: the
91 // volume is small and the branch is authoritative, so there is no index to go
92 // stale here.
101 // Requests lists a repo's change requests from the index. The rows are derived
102 // from the metadata branch and refreshed at the start of the request, so the
103 // list is as current as git without reading the branch on every view.
93 104 func (w *Web) Requests(rw http.ResponseWriter, r *http.Request) {
94 105 m, ok := w.open(rw, r)
95 106 if !ok {
96 107 return
97 108 }
98 list, err := w.requestsAt(memoOf(r), m)
99 if err != nil {
100 w.serverError(rw, r, "cannot read changes: "+err.Error())
101 return
102 }
103 109 state := strings.TrimSpace(r.URL.Query().Get("state"))
104 110 if state == "" {
105 111 state = "open"
@@ -108,36 +114,12 @@ func (w *Web) Requests(rw http.ResponseWriter, r *http.Request) {
108 114 state = "open"
109 115 }
110 116 query := strings.TrimSpace(r.URL.Query().Get("q"))
111 needle := strings.ToLower(query)
112 nums := w.entityNumbers(memoOf(r), m)
113 acl, _ := m.ACL()
114 rows := make([]requestRow, 0, len(list))
115 open, merged := 0, 0
116 for _, req := range list {
117 switch req.State {
118 case "open":
119 open++
120 case "merged":
121 merged++
122 }
123 if state != "all" && req.State != state {
124 continue
125 }
126 if needle != "" {
127 haystack := strings.ToLower(req.Title + "\n" + req.Description + "\n" + req.Author)
128 if !strings.Contains(haystack, needle) {
129 continue
130 }
131 }
132 current := currentApprovals(acl, req)
133 rows = append(rows, requestRow{Request: req, Current: current, Number: nums[req.ID]})
117 rows, counts, err := w.requestList(r, m, state, query)
118 if err != nil {
119 w.serverError(rw, r, "cannot read changes: "+err.Error())
120 return
134 121 }
135 sort.SliceStable(rows, func(i, j int) bool {
136 if (rows[i].State == "open") != (rows[j].State == "open") {
137 return rows[i].State == "open"
138 }
139 return rows[i].ID > rows[j].ID
140 })
122
141 123 page, _ := strconv.Atoi(r.URL.Query().Get("page"))
142 124 pg := newPager(len(rows), listPageSize, page, func(n int) string {
143 125 return requestListURL(m.Name, state, query, n)
@@ -146,6 +128,7 @@ func (w *Web) Requests(rw http.ResponseWriter, r *http.Request) {
146 128 rows = rows[from:to]
147 129 form := w.requestForm(r, m)
148 130
131 acl, _ := m.ACL()
149 132 required := 0
150 133 if acl != nil {
151 134 required = acl.RequiredApprovals()
@@ -154,7 +137,6 @@ func (w *Web) Requests(rw http.ResponseWriter, r *http.Request) {
154 137 Page
155 138 Name string
156 139 Rows []requestRow
157 Numbers map[string]int
158 140 State string
159 141 Query string
160 142 Open int
@@ -170,9 +152,8 @@ func (w *Web) Requests(rw http.ResponseWriter, r *http.Request) {
170 152 Pager pager
171 153 }{
172 154 Page: w.page(r, m.Name+" · changes"), Name: m.Name, Rows: rows, State: state,
173 Numbers: nums,
174 Query: query, Open: open, Merged: merged, Total: len(list), Closed: len(list) - open - merged,
175 Required: required, Filtered: query != "" || (state != "open" && len(list) > 0),
155 Query: query, Open: counts[0], Merged: counts[1], Closed: counts[2], Total: counts[3],
156 Required: required, Filtered: query != "" || (state != "open" && counts[3] > 0),
176 157 CanOpen: form.CanOpen, Base: form.Base,
177 158 BaseBranches: form.BaseBranches, HeadBranches: form.HeadBranches,
178 159 Pager: pg,
@@ -184,6 +165,85 @@ func (w *Web) Requests(rw http.ResponseWriter, r *http.Request) {
184 165 w.render(rw, "requests", view, 0)
185 166 }
186 167
168 // requestList returns the rows the list page shows and the chip counts, which
169 // describe the whole repository rather than the filtered page. The index answers
170 // both; a test or a server without an index falls back to reading git.
171 func (w *Web) requestList(r *http.Request, m *repo.Meta, state, query string) ([]requestRow, [4]int, error) {
172 var counts [4]int
173 if w.Index == nil {
174 list, err := w.requestsAt(memoOf(r), m)
175 if err != nil {
176 return nil, counts, err
177 }
178 rows := make([]requestRow, 0, len(list))
179 for _, req := range list {
180 switch req.State {
181 case "open":
182 counts[0]++
183 case "merged":
184 counts[1]++
185 }
186 if state != "all" && req.State != state {
187 continue
188 }
189 if query != "" && !strings.Contains(strings.ToLower(req.Title+"\n"+req.Description+"\n"+req.Author), strings.ToLower(query)) {
190 continue
191 }
192 rows = append(rows, requestRow{
193 ID: req.ID, Title: req.Title, State: req.State, Author: req.Author,
194 Base: req.Base, Head: req.Head, HeadRef: req.HeadRef, Created: req.Created,
195 CommentCount: len(req.Comments),
196 })
197 }
198 counts[2] = len(list) - counts[0] - counts[1]
199 counts[3] = len(list)
200 sort.SliceStable(rows, func(i, j int) bool {
201 if (rows[i].State == "open") != (rows[j].State == "open") {
202 return rows[i].State == "open"
203 }
204 return rows[i].ID > rows[j].ID
205 })
206 return rows, counts, nil
207 }
208
209 if err := w.ensureIndex(memoOf(r), m); err != nil {
210 return nil, counts, err
211 }
212 list, err := w.Index.Requests(m.ID, index.RequestFilter{State: state, Query: query})
213 if err != nil {
214 return nil, counts, err
215 }
216 open, merged, closed, total, err := w.Index.RequestCounts(m.ID)
217 if err != nil {
218 return nil, counts, err
219 }
220 counts = [4]int{open, merged, closed, total}
221 ids := make([]string, len(list))
222 for i, row := range list {
223 ids[i] = row.ID
224 }
225 approvals, err := w.Index.Approvals(m.ID, ids)
226 if err != nil {
227 return nil, counts, err
228 }
229 acl, _ := m.ACL()
230 rows := make([]requestRow, 0, len(list))
231 for _, row := range list {
232 current := 0
233 for _, a := range approvals[row.ID] {
234 if a.Head == row.Head && a.Base == row.Base && acl != nil && acl.EligibleApprover(a.Principal) {
235 current++
236 }
237 }
238 rows = append(rows, requestRow{
239 ID: row.ID, Title: row.Title, State: row.State, Author: row.Author,
240 Base: row.Base, Head: row.Head, HeadRef: row.HeadRef, Created: row.Created,
241 CommentCount: row.CommentCount, Number: row.Number, Current: current,
242 })
243 }
244 return rows, counts, nil
245 }
246
187 247 func requestListURL(name, state, query string, page int) string {
188 248 v := url.Values{}
189 249 if state != "" {