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/comments.go +98
| @@ -0,0 +1,98 @@ | |||
| 1 | package index | ||
| 2 | |||
| 3 | // Comments live in one table for both kinds of entity: an issue comment and a | ||
| 4 | // change-request comment have the same file shape (author, created, body) and | ||
| 5 | // differ only in the entity they belong to, which is the (kind, parent) pair. | ||
| 6 | // | ||
| 7 | // The rows are here for search and for anything that pages through a thread | ||
| 8 | // without reading the branch. The ownership rule stays where it was: a comment | ||
| 9 | // belongs to its parent, so replacing the parent replaces its comments. | ||
| 10 | |||
| 11 | import ( | ||
| 12 | "time" | ||
| 13 | |||
| 14 | "github.com/rodent-software/vole/metadata" | ||
| 15 | ) | ||
| 16 | |||
| 17 | const ( | ||
| 18 | commentKindIssue = "issue" | ||
| 19 | commentKindRequest = "request" | ||
| 20 | ) | ||
| 21 | |||
| 22 | // replaceComments replaces one entity's comments. Deleting before inserting is | ||
| 23 | // what makes an edit or a deletion visible; the caller has already replaced the | ||
| 24 | // entity's own row in the same transaction. | ||
| 25 | func replaceComments(tx execer, repoID, kind, parent string, comments []metadata.Comment) error { | ||
| 26 | if _, err := tx.Exec( | ||
| 27 | `DELETE FROM comments WHERE repo_id=? AND kind=? AND parent=?`, | ||
| 28 | repoID, kind, parent); err != nil { | ||
| 29 | return err | ||
| 30 | } | ||
| 31 | for i := range comments { | ||
| 32 | c := &comments[i] | ||
| 33 | var path, side, blob string | ||
| 34 | var line int | ||
| 35 | if c.Anchor != nil { | ||
| 36 | path, side, line, blob = c.Anchor.Path, c.Anchor.Side, c.Anchor.Line, c.Anchor.Blob | ||
| 37 | } | ||
| 38 | deleted := 0 | ||
| 39 | if c.Deleted { | ||
| 40 | deleted = 1 | ||
| 41 | } | ||
| 42 | if _, err := tx.Exec( | ||
| 43 | `INSERT OR REPLACE INTO comments(repo_id, kind, parent, id, author, created, body, | ||
| 44 | reply_to, deleted, path, side, line, blob) | ||
| 45 | VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)`, | ||
| 46 | repoID, kind, parent, c.ID, c.Author, unixOrZero(c.Created), c.Body, | ||
| 47 | c.ReplyTo, deleted, path, side, line, blob); err != nil { | ||
| 48 | return err | ||
| 49 | } | ||
| 50 | } | ||
| 51 | return nil | ||
| 52 | } | ||
| 53 | |||
| 54 | // CommentRow is one indexed comment. It is the shape a search result and a | ||
| 55 | // thread renderer both need; a tombstone keeps its row so replies still have a | ||
| 56 | // parent to point at. | ||
| 57 | type CommentRow struct { | ||
| 58 | Kind string | ||
| 59 | Parent string | ||
| 60 | ID string | ||
| 61 | Author string | ||
| 62 | Created time.Time | ||
| 63 | Body string | ||
| 64 | ReplyTo string | ||
| 65 | Deleted bool | ||
| 66 | Path string | ||
| 67 | Side string | ||
| 68 | Line int | ||
| 69 | Blob string | ||
| 70 | } | ||
| 71 | |||
| 72 | // Comments returns one entity's comments in creation order. The detail pages | ||
| 73 | // read the entity from git today; this exists for the surfaces that only need | ||
| 74 | // the discussion, and for search. | ||
| 75 | func (s *Store) Comments(repoID, kind, parent string) ([]CommentRow, error) { | ||
| 76 | rows, err := s.db.Query( | ||
| 77 | `SELECT kind, parent, id, author, created, body, reply_to, deleted, path, side, line, blob | ||
| 78 | FROM comments WHERE repo_id=? AND kind=? AND parent=? ORDER BY created, id`, | ||
| 79 | repoID, kind, parent) | ||
| 80 | if err != nil { | ||
| 81 | return nil, err | ||
| 82 | } | ||
| 83 | defer rows.Close() | ||
| 84 | var out []CommentRow | ||
| 85 | for rows.Next() { | ||
| 86 | var c CommentRow | ||
| 87 | var created int64 | ||
| 88 | var deleted int | ||
| 89 | if err := rows.Scan(&c.Kind, &c.Parent, &c.ID, &c.Author, &created, &c.Body, | ||
| 90 | &c.ReplyTo, &deleted, &c.Path, &c.Side, &c.Line, &c.Blob); err != nil { | ||
| 91 | return nil, err | ||
| 92 | } | ||
| 93 | c.Created = unixTime(created) | ||
| 94 | c.Deleted = deleted != 0 | ||
| 95 | out = append(out, c) | ||
| 96 | } | ||
| 97 | return out, rows.Err() | ||
| 98 | } | ||