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.

Index the rest of the metadata branch

7f547ca3b8da218249120d3c910c73c9a1a1e52d · Keenan Nemetz · 10d ago · parent c9202f7f

Index the rest of the metadata branch

The index held issues and their numbers. Everything else on the branch —
change requests, their comments, approvals and closes, and release
artifacts — was read from git on every page that needed it, and the
repository header read the whole requests subtree just to print a count.

Add the tables and derive them from the same commit:

- requests, with the body preserved for search;
- request_closes and request_approvals, each in its own table so a
  backlink and a current-approval count are queries rather than a walk of
  every request;
- comments for both kinds of entity, with the anchor a review comment
  carries;
- artifacts, the release pointers, with the size from the pointer.

The refresh is still driven by the path delta between two commits: a
changed issue or request replaces its own rows, its comments and its sets,
and a deleted one removes them while keeping its `#42` number. Rebuild and
the first read go through IndexRepo, which is the definition the
incremental path has to reproduce; a rebuild now clears the derived tables
first so a deleted repository leaves nothing behind.

Schema version 4 drops the old rows on first open, which is the existing
recovery path: the index is disposable and built from the branch.

forge/index/issues.go +1−156

@@ -10,162 +10,6 @@ import (
10 10 "github.com/rodent-software/vole/metadata"
11 11 )
12 12
13 // Ensure refreshes everything a repo derives from its metadata branch: the issue
14 // rows, the label and assignee filters, and the `#42` numbering. Called on read,
15 // so a push is visible without a hook or a full rebuild.
16 //
17 // A branch that moved forward is refreshed from the paths that changed. The full
18 // read is reserved for the cases that need it: the first read, a rewritten
19 // branch, or an incremental refresh that cannot be trusted.
20 func (s *Store) Ensure(m *repo.Meta) error {
21 current, err := m.Git().RevParse(metadata.Ref)
22 if err != nil {
23 return err
24 }
25 return s.EnsureAt(m, current)
26 }
27
28 // EnsureAt refreshes the derived rows to the metadata commit the caller already
29 // resolved. It is Ensure for a caller that needs the commit for something else
30 // too, so one page does not resolve the same ref twice.
31 func (s *Store) EnsureAt(m *repo.Meta, current string) error {
32 var stored string
33 err := s.db.QueryRow(`SELECT issues_sha FROM repos WHERE id=?`, m.ID).Scan(&stored)
34 if err != nil {
35 return err
36 }
37 if stored == current {
38 return nil
39 }
40 if stored != "" && current != "" {
41 if ok, err := m.Git().IsAncestor(stored, current); err == nil && ok {
42 if err := s.indexIssuesFrom(m, stored, current); err == nil {
43 return nil
44 }
45 // The delta could not be applied, so the rows and the stored sha would
46 // disagree. Fall through to the reconstruction that defines the index.
47 }
48 }
49 return s.IndexIssues(m)
50 }
51
52 // EnsureIssues keeps the older name for the issue surfaces that read it. It is
53 // Ensure, because numbers live in the same refresh as the issue rows.
54 func (s *Store) EnsureIssues(m *repo.Meta) error { return s.Ensure(m) }
55
56 // IndexIssues replaces a repo's issue rows and its entity numbers from the
57 // metadata branch.
58 func (s *Store) IndexIssues(m *repo.Meta) error {
59 list, err := metadata.LoadIssues(m.Git(), metadata.Ref)
60 if err != nil {
61 return err
62 }
63 numbers, err := metadata.LoadNumbers(m.Git(), metadata.Ref)
64 if err != nil {
65 return err
66 }
67 sha, err := m.Git().RevParse(metadata.Ref)
68 if err != nil {
69 return err
70 }
71
72 tx, err := s.db.Begin()
73 if err != nil {
74 return err
75 }
76 defer tx.Rollback()
77 for _, table := range []string{"issues", "issue_labels", "issue_assignees", "entity_numbers"} {
78 if _, err := tx.Exec(`DELETE FROM `+table+` WHERE repo_id=?`, m.ID); err != nil {
79 return err
80 }
81 }
82 for number := 1; number <= numbers.Len(); number++ {
83 id, kind, ok := numbers.At(number)
84 if !ok {
85 continue
86 }
87 if err := insertNumber(tx, m.ID, number, kind, id); err != nil {
88 return err
89 }
90 }
91 for _, iss := range list {
92 if err := insertIssue(tx, m.ID, iss); err != nil {
93 return err
94 }
95 }
96 if _, err := tx.Exec(`UPDATE repos SET issues_sha=? WHERE id=?`, sha, m.ID); err != nil {
97 return err
98 }
99 return tx.Commit()
100 }
101
102 // indexIssuesFrom applies the difference between two commits to the index. The
103 // caller has already established that from is an ancestor of to, so every issue
104 // that exists at to is either unchanged, changed in the range, or new.
105 func (s *Store) indexIssuesFrom(m *repo.Meta, from, to string) error {
106 changes, err := m.Git().DiffStatusPaths(from, to, metadata.IssuesDir)
107 if err != nil {
108 return err
109 }
110 ids := map[string]bool{}
111 for path := range changes {
112 if id, ok := metadata.IssueIDFromPath(path); ok {
113 ids[id] = true
114 }
115 }
116
117 // Read every changed issue before opening the write transaction. `to` is a
118 // commit, so a concurrent push cannot make the rows describe two different
119 // tree states.
120 issues := make(map[string]*metadata.Issue, len(ids))
121 for id := range ids {
122 iss, found, err := metadata.LoadIssueAt(m.Git(), to, id)
123 if err != nil {
124 return err
125 }
126 if found {
127 issues[id] = iss
128 }
129 }
130
131 // Numbers are historical: a deleted entity keeps its number, and a creation
132 // that was deleted again inside the range still consumed one. The additions
133 // are therefore taken from the commit range, not from the current tree.
134 groups, err := m.Git().AddedPathsPerCommit(from+".."+to, metadata.IssuesDir, metadata.RequestsDir)
135 if err != nil {
136 return err
137 }
138
139 tx, err := s.db.Begin()
140 if err != nil {
141 return err
142 }
143 defer tx.Rollback()
144 for id := range ids {
145 if _, found := issues[id]; found {
146 continue
147 }
148 if err := deleteIssue(tx, m.ID, id); err != nil {
149 return err
150 }
151 }
152 for _, iss := range issues {
153 if err := deleteIssue(tx, m.ID, iss.ID); err != nil {
154 return err
155 }
156 if err := insertIssue(tx, m.ID, iss); err != nil {
157 return err
158 }
159 }
160 if err := addNumbers(tx, m.ID, metadata.Additions(groups)); err != nil {
161 return err
162 }
163 if _, err := tx.Exec(`UPDATE repos SET issues_sha=? WHERE id=?`, to, m.ID); err != nil {
164 return err
165 }
166 return tx.Commit()
167 }
168
169 13 // insertIssue writes the row and filter tables for one issue. The caller decides
170 14 // whether it is replacing an existing issue, so this stays one shape used by the
171 15 // full rebuild and the incremental refresh alike.
@@ -203,6 +47,7 @@ func deleteIssue(tx execer, repoID, id string) error {
203 47 `DELETE FROM issues WHERE repo_id=? AND id=?`,
204 48 `DELETE FROM issue_labels WHERE repo_id=? AND issue_id=?`,
205 49 `DELETE FROM issue_assignees WHERE repo_id=? AND issue_id=?`,
50 `DELETE FROM comments WHERE repo_id=? AND kind='issue' AND parent=?`,
206 51 } {
207 52 if _, err := tx.Exec(q, repoID, id); err != nil {
208 53 return err