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 | |