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.

Say what each change is waiting for in the list

7cc36ba0f4ba96d3f4938b13758b2cc20c22375a · Keenan Nemetz · 5d 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

forge/index/requests.go +51−16

@@ -15,6 +15,7 @@ import (
15 15 "strings"
16 16 "time"
17 17
18 "github.com/rodent-software/vole/core/git"
18 19 "github.com/rodent-software/vole/forge/repo"
19 20 "github.com/rodent-software/vole/metadata"
20 21 )
@@ -22,11 +23,15 @@ import (
22 23 // RequestRow is one indexed request. Comments is a count: the rows themselves
23 24 // are in the comments table.
24 25 type RequestRow struct {
25 ID string
26 Title string
27 State string
28 Author string
29 Created time.Time
26 ID string
27 Title string
28 State string
29 Author string
30 Created time.Time
31 // Updated is when the metadata branch last touched the request: an edit of
32 // its body or target, a comment, or an approval. The branch keeps no such
33 // field, so the index dates it from the commits, exactly like an issue.
34 Updated time.Time
30 35 Base string
31 36 Head string
32 37 HeadRef string
@@ -46,7 +51,7 @@ type RequestFilter struct {
46 51 Limit int
47 52 }
48 53
49 const requestSelect = `SELECT r.id, r.title, r.state, r.author, r.created, r.base, r.head, r.head_ref,
54 const requestSelect = `SELECT r.id, r.title, r.state, r.author, r.created, r.updated, r.base, r.head, r.head_ref,
50 55 r.comments, COALESCE(n.number, 0)
51 56 FROM requests r
52 57 LEFT JOIN entity_numbers n ON n.repo_id = r.repo_id AND n.id = r.id`
@@ -85,9 +90,10 @@ func (s *Store) RequestsFollowing(m *repo.Meta, branch string) (follows, ok bool
85 90 return true, true, nil
86 91 }
87 92
88 // Requests returns a repo's indexed requests, open first and newest first within
89 // each state. That is the order the list page shows, so paging does not have to
90 // re-sort what the database already ordered.
93 // Requests returns a repo's indexed requests, most recently updated first within
94 // each state, with open ones ahead of merged and closed. That is the order the
95 // list page shows, so paging does not have to re-sort what the database already
96 // ordered.
91 97 func (s *Store) Requests(repoID string, f RequestFilter) ([]RequestRow, error) {
92 98 q := requestSelect + `
93 99 WHERE r.repo_id=?`
@@ -101,7 +107,7 @@ func (s *Store) Requests(repoID string, f RequestFilter) ([]RequestRow, error) {
101 107 q += ` AND (r.title LIKE ? OR r.body LIKE ? OR r.author LIKE ?)`
102 108 args = append(args, like, like, like)
103 109 }
104 q += ` ORDER BY CASE WHEN r.state='open' THEN 0 ELSE 1 END, r.id DESC`
110 q += ` ORDER BY CASE WHEN r.state='open' THEN 0 ELSE 1 END, r.updated DESC, r.id DESC`
105 111 if f.Limit > 0 {
106 112 q += ` LIMIT ?`
107 113 args = append(args, f.Limit)
@@ -115,17 +121,39 @@ func (s *Store) Requests(repoID string, f RequestFilter) ([]RequestRow, error) {
115 121 var out []RequestRow
116 122 for rows.Next() {
117 123 var r RequestRow
118 var created int64
119 if err := rows.Scan(&r.ID, &r.Title, &r.State, &r.Author, &created, &r.Base,
124 var created, updated int64
125 if err := rows.Scan(&r.ID, &r.Title, &r.State, &r.Author, &created, &updated, &r.Base,
120 126 &r.Head, &r.HeadRef, &r.CommentCount, &r.Number); err != nil {
121 127 return nil, err
122 128 }
123 129 r.Created = unixTime(created)
130 r.Updated = unixTime(updated)
124 131 out = append(out, r)
125 132 }
126 133 return out, rows.Err()
127 134 }
128 135
136 // requestTouchTimes maps the paths one `git log` touched to the requests they
137 // belong to, keeping the newest commit time per request. rev may be a commit
138 // range, so an incremental refresh only reads the commits it has not applied.
139 func requestTouchTimes(g *git.Repo, rev string) (map[string]int64, error) {
140 touched, err := g.TouchTimes(rev, metadata.RequestsDir)
141 if err != nil {
142 return nil, err
143 }
144 times := make(map[string]int64, len(touched))
145 for path, when := range touched {
146 id, ok := metadata.RequestIDFromPath(path)
147 if !ok {
148 continue
149 }
150 if when > times[id] {
151 times[id] = when
152 }
153 }
154 return times, nil
155 }
156
129 157 // RequestCount returns the requests in one state. An empty state, or "all",
130 158 // counts every request. The repository header uses it for the open badge, which
131 159 // every page shows.
@@ -239,12 +267,19 @@ func (s *Store) Closes(repoID, issueID string) ([]string, error) {
239 267 // insertRequest writes the row and its sets. The caller decides whether it
240 268 // replaces an existing request, so this stays one shape for the rebuild and the
241 269 // incremental refresh alike.
242 func insertRequest(tx execer, repoID string, req *metadata.Request) error {
270 //
271 // updated is when the branch last touched the request directory, passed in
272 // because the request has no such field: the commit that changed it carries the
273 // time.
274 func insertRequest(tx execer, repoID string, req *metadata.Request, updated int64) error {
275 if created := unixOrZero(req.Created); updated < created {
276 updated = created
277 }
243 278 if _, err := tx.Exec(
244 `INSERT INTO requests(repo_id, id, title, state, author, created, base, head, head_ref,
279 `INSERT INTO requests(repo_id, id, title, state, author, created, updated, base, head, head_ref,
245 280 merged_base, merged_head, merged_by, merged_at, body, comments)
246 VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
247 repoID, req.ID, req.Title, req.State, req.Author, unixOrZero(req.Created),
281 VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
282 repoID, req.ID, req.Title, req.State, req.Author, unixOrZero(req.Created), updated,
248 283 req.Base, req.Head, req.HeadRef, req.MergedBase, req.MergedHead, req.MergedBy,
249 284 unixOrZero(req.MergedAt), req.Description, len(req.Comments),
250 285 ); err != nil {