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.

Sort the issue list by when it last moved

db4081ec934ddd9a7ce123b4d3cf6d6ea43a4537 · Keenan Nemetz · 5d ago · parent a32a859a

Sort the issue list by when it last moved

An issue queue is read for what changed, not for the order things were
filed. The list now shows the newest activity first and carries the columns
that answer the question: labels have their own column, assignees render like
authors do, and the creation time and author sit under the title.

The last column is "updated": the newest commit over the issue's directory,
which covers an edit, a comment, a label change, and a close or reopen. The
metadata branch has no such field on purpose — the commit carries the time —
so the index derives it from one `git log` per rebuild or one per refreshed
range, and stores it. An incremental refresh also reads the commits' paths, so
an edit reverted inside the range still counts as activity.

Bumps the index schema version, which drops the derived rows and rebuilds them
from the branch on the next read.

Closes #192

forge/index/issues.go +46−9

@@ -6,6 +6,7 @@ import (
6 6 "strings"
7 7 "time"
8 8
9 "github.com/rodent-software/vole/core/git"
9 10 "github.com/rodent-software/vole/forge/repo"
10 11 "github.com/rodent-software/vole/metadata"
11 12 )
@@ -13,12 +14,22 @@ import (
13 14 // insertIssue writes the row and filter tables for one issue. The caller decides
14 15 // whether it is replacing an existing issue, so this stays one shape used by the
15 16 // full rebuild and the incremental refresh alike.
16 func insertIssue(tx execer, repoID string, iss *metadata.Issue) error {
17 //
18 // updated is when the branch last touched the issue directory. It is passed in
19 // rather than read from the issue, which has no such field: the commit that
20 // changed the file carries the time.
21 func insertIssue(tx execer, repoID string, iss *metadata.Issue, updated int64) error {
22 // An issue nobody has touched since it was filed still had one moment of
23 // activity. A commit dated before its own frontmatter is a fixture or a skewed
24 // clock, and reporting an update older than the opening would read as a bug.
25 if created := iss.Created.Unix(); updated < created {
26 updated = created
27 }
17 28 if _, err := tx.Exec(
18 `INSERT INTO issues(repo_id, id, title, state, author, created, labels, assignees, comments, body)
19 VALUES(?,?,?,?,?,?,?,?,?,?)`,
29 `INSERT INTO issues(repo_id, id, title, state, author, created, updated, labels, assignees, comments, body)
30 VALUES(?,?,?,?,?,?,?,?,?,?,?)`,
20 31 repoID, iss.ID, iss.Title, iss.State, iss.Author,
21 iss.Created.Unix(), strings.Join(iss.Labels, "\x1f"), strings.Join(iss.Assignees, "\x1f"),
32 iss.Created.Unix(), updated, strings.Join(iss.Labels, "\x1f"), strings.Join(iss.Assignees, "\x1f"),
22 33 len(iss.Comments), iss.Description,
23 34 ); err != nil {
24 35 return err
@@ -103,7 +114,8 @@ func addNumbers(tx *sql.Tx, repoID string, additions []metadata.Entity) error {
103 114 return nil
104 115 }
105 116
106 // Issues returns a repo's indexed issues, newest first.
117 // unixTime turns a stored epoch second back into a time. Zero stays the zero
118 // time so a row the index has not filled in reads as absent rather than 1970.
107 119 func unixTime(sec int64) time.Time {
108 120 if sec == 0 {
109 121 return time.Time{}
@@ -122,13 +134,37 @@ type Filter struct {
122 134 Limit int
123 135 }
124 136
137 // issueTouchTimes maps the paths one `git log` touched to the issues they belong
138 // to, keeping the newest commit time per issue. rev may be a commit range, so an
139 // incremental refresh only reads the commits it has not applied.
140 func issueTouchTimes(g *git.Repo, rev string) (map[string]int64, error) {
141 touched, err := g.TouchTimes(rev, metadata.IssuesDir)
142 if err != nil {
143 return nil, err
144 }
145 times := make(map[string]int64, len(touched))
146 for path, when := range touched {
147 id, ok := metadata.IssueIDFromPath(path)
148 if !ok {
149 continue
150 }
151 if when > times[id] {
152 times[id] = when
153 }
154 }
155 return times, nil
156 }
157
125 158 // issueSelect is the row every issue list scans: the indexed issue with its
126 159 // `#42` alias from the numbering table.
127 const issueSelect = `SELECT i.id, i.title, i.state, i.author, i.created, i.labels, i.assignees, i.comments,
160 const issueSelect = `SELECT i.id, i.title, i.state, i.author, i.created, i.updated, i.labels, i.assignees, i.comments,
128 161 COALESCE(n.number, 0)
129 162 FROM issues i
130 163 LEFT JOIN entity_numbers n ON n.repo_id = i.repo_id AND n.id = i.id`
131 164
165 // Issues returns a repo's indexed issues, most recently updated first. That is
166 // the order every list shows by default: an issue queue is read for what moved,
167 // not for what was filed last.
132 168 func (s *Store) Issues(repoID string, f Filter) ([]IssueRow, error) {
133 169 q := issueSelect + `
134 170 WHERE i.repo_id=?`
@@ -150,7 +186,7 @@ func (s *Store) Issues(repoID string, f Filter) ([]IssueRow, error) {
150 186 like := "%" + term + "%"
151 187 args = append(args, like, like)
152 188 }
153 q += ` ORDER BY i.id DESC`
189 q += ` ORDER BY i.updated DESC, i.id DESC`
154 190 if f.Limit > 0 {
155 191 q += ` LIMIT ?`
156 192 args = append(args, f.Limit)
@@ -208,12 +244,13 @@ func scanIssueRows(rows *sql.Rows) ([]IssueRow, error) {
208 244 var out []IssueRow
209 245 for rows.Next() {
210 246 var r IssueRow
211 var created int64
247 var created, updated int64
212 248 var labels, assignees string
213 if err := rows.Scan(&r.ID, &r.Title, &r.State, &r.Author, &created, &labels, &assignees, &r.Comments, &r.Number); err != nil {
249 if err := rows.Scan(&r.ID, &r.Title, &r.State, &r.Author, &created, &updated, &labels, &assignees, &r.Comments, &r.Number); err != nil {
214 250 return nil, err
215 251 }
216 252 r.Created = unixTime(created)
253 r.Updated = unixTime(updated)
217 254 if labels != "" {
218 255 r.Labels = strings.Split(labels, "\x1f")
219 256 }