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.

Number issues and change requests for human references

e05f1c48d4ee55635cd7b77889f0545137198106 · Keenan Nemetz · 13d ago · parent 41d1fee9

Number issues and change requests for human references

A ULID is an unreadable handle, and the 8-character prefix the list showed
is mostly timestamp: this repository's 27 issues shared seven distinct
six-character prefixes and two pairs already collided at eight. The part
people actually type carried almost no information.

Issues and change requests now share one gap-free sequence (#1, #42, ...)
derived from the creation order in the metadata branch's history, the way
one forge counts both in a single run.

The number is not stored. It is the rank of the entity's creation commit,
so nothing is written when an entity is created, a repository needs no
backfill, the same history gives the same numbers on every clone, and
`vole admin index rebuild` reproduces them from git. A ULID stays the
identity: paths, front matter, `closes:` and audit entries are unchanged,
and data/index.db only caches the mapping so a page render does not walk
history.

Every id-taking command accepts a number, `#42` links in a body through a
kind-agnostic /r/<repo>/n/42 route, and --closes resolves an alias to the
ULID before writing.

Closes #45

forge/index/issues.go +39−12

@@ -8,9 +8,10 @@ import (
8 8 "github.com/rodent-software/vole/metadata"
9 9 )
10 10
11 // EnsureIssues refreshes a repo's indexed issues if the issues branch moved.
12 // Called on read, so a push is visible without a hook or a full rebuild.
13 func (s *Store) EnsureIssues(m *repo.Meta) error {
11 // Ensure refreshes everything a repo derives from its metadata branch: the issue
12 // rows, the label and assignee filters, and the `#42` numbering. Called on read,
13 // so a push is visible without a hook or a full rebuild.
14 func (s *Store) Ensure(m *repo.Meta) error {
14 15 current := ""
15 16 if sha, err := m.Git().RevParse(metadata.Ref); err == nil {
16 17 current = sha
@@ -26,12 +27,21 @@ func (s *Store) EnsureIssues(m *repo.Meta) error {
26 27 return s.IndexIssues(m)
27 28 }
28 29
29 // IndexIssues replaces a repo's issue rows from the issues branch.
30 // EnsureIssues keeps the older name for the issue surfaces that read it. It is
31 // Ensure, because numbers live in the same refresh as the issue rows.
32 func (s *Store) EnsureIssues(m *repo.Meta) error { return s.Ensure(m) }
33
34 // IndexIssues replaces a repo's issue rows and its entity numbers from the
35 // metadata branch.
30 36 func (s *Store) IndexIssues(m *repo.Meta) error {
31 37 list, err := metadata.LoadIssues(m.Git(), metadata.Ref)
32 38 if err != nil {
33 39 return err
34 40 }
41 numbers, err := metadata.LoadNumbers(m.Git(), metadata.Ref)
42 if err != nil {
43 return err
44 }
35 45 sha, err := m.Git().RevParse(metadata.Ref)
36 46 if err != nil {
37 47 return err
@@ -51,6 +61,20 @@ func (s *Store) IndexIssues(m *repo.Meta) error {
51 61 if _, err := tx.Exec(`DELETE FROM issue_assignees WHERE repo_id=?`, m.ID); err != nil {
52 62 return err
53 63 }
64 if _, err := tx.Exec(`DELETE FROM entity_numbers WHERE repo_id=?`, m.ID); err != nil {
65 return err
66 }
67 for number := 1; number <= numbers.Len(); number++ {
68 id, kind, ok := numbers.At(number)
69 if !ok {
70 continue
71 }
72 if _, err := tx.Exec(
73 `INSERT INTO entity_numbers(repo_id, number, kind, id) VALUES(?,?,?,?)`,
74 m.ID, number, kind, id); err != nil {
75 return err
76 }
77 }
54 78 for _, iss := range list {
55 79 if _, err := tx.Exec(
56 80 `INSERT INTO issues(repo_id, id, title, state, author, created, labels, assignees, comments, body)
@@ -99,27 +123,30 @@ type Filter struct {
99 123 }
100 124
101 125 func (s *Store) Issues(repoID string, f Filter) ([]IssueRow, error) {
102 q := `SELECT id, title, state, author, created, labels, assignees, comments
103 FROM issues WHERE repo_id=?`
126 q := `SELECT i.id, i.title, i.state, i.author, i.created, i.labels, i.assignees, i.comments,
127 COALESCE(n.number, 0)
128 FROM issues i
129 LEFT JOIN entity_numbers n ON n.repo_id = i.repo_id AND n.id = i.id
130 WHERE i.repo_id=?`
104 131 args := []any{repoID}
105 132 if f.State != "" && f.State != "all" {
106 q += ` AND state=?`
133 q += ` AND i.state=?`
107 134 args = append(args, f.State)
108 135 }
109 136 if f.Label != "" {
110 q += ` AND id IN (SELECT issue_id FROM issue_labels WHERE repo_id=? AND label=?)`
137 q += ` AND i.id IN (SELECT issue_id FROM issue_labels WHERE repo_id=? AND label=?)`
111 138 args = append(args, repoID, f.Label)
112 139 }
113 140 if f.Assignee != "" {
114 q += ` AND id IN (SELECT issue_id FROM issue_assignees WHERE repo_id=? AND assignee=?)`
141 q += ` AND i.id IN (SELECT issue_id FROM issue_assignees WHERE repo_id=? AND assignee=?)`
115 142 args = append(args, repoID, f.Assignee)
116 143 }
117 144 if term := strings.TrimSpace(f.Query); term != "" {
118 q += ` AND (title LIKE ? OR body LIKE ?)`
145 q += ` AND (i.title LIKE ? OR i.body LIKE ?)`
119 146 like := "%" + term + "%"
120 147 args = append(args, like, like)
121 148 }
122 q += ` ORDER BY id DESC`
149 q += ` ORDER BY i.id DESC`
123 150
124 151 rows, err := s.db.Query(q, args...)
125 152 if err != nil {
@@ -131,7 +158,7 @@ func (s *Store) Issues(repoID string, f Filter) ([]IssueRow, error) {
131 158 var r IssueRow
132 159 var created int64
133 160 var labels, assignees string
134 if err := rows.Scan(&r.ID, &r.Title, &r.State, &r.Author, &created, &labels, &assignees, &r.Comments); err != nil {
161 if err := rows.Scan(&r.ID, &r.Title, &r.State, &r.Author, &created, &labels, &assignees, &r.Comments, &r.Number); err != nil {
135 162 return nil, err
136 163 }
137 164 r.Created = unixTime(created)