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/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 57m ago 4b8c9e1d
history blame raw 4.6 KiB
1package 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
11import (
12 "database/sql"
13 "time"
14
15 "rodent.software/vole/metadata"
16)
17
18const (
19 commentKindIssue = "issue"
20 commentKindRequest = "request"
21)
22
23// replaceComments replaces one entity's comments. Deleting before inserting is
24// what makes an edit or a deletion visible; the caller has already replaced the
25// entity's own row in the same transaction.
26func replaceComments(tx execer, repoID, kind, parent, title string, comments []metadata.Comment) error {
27 if _, err := tx.Exec(
28 `DELETE FROM comments WHERE repo_id=? AND kind=? AND parent=?`,
29 repoID, kind, parent); err != nil {
30 return err
31 }
32 // The search rows for the thread go with the thread, so an edited or deleted
33 // comment cannot survive in a query.
34 if _, err := tx.Exec(
35 `DELETE FROM search WHERE repo_id=? AND kind=? AND parent_id=?`,
36 repoID, commentKindFor(kind), parent); err != nil {
37 return err
38 }
39 for i := range comments {
40 c := &comments[i]
41 var path, side, blob string
42 var line int
43 if c.Anchor != nil {
44 path, side, line, blob = c.Anchor.Path, c.Anchor.Side, c.Anchor.Line, c.Anchor.Blob
45 }
46 deleted := 0
47 if c.Deleted {
48 deleted = 1
49 }
50 if _, err := tx.Exec(
51 `INSERT OR REPLACE INTO comments(repo_id, kind, parent, id, author, created, body,
52 reply_to, deleted, path, side, line, blob)
53 VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)`,
54 repoID, kind, parent, c.ID, c.Author, unixOrZero(c.Created), c.Body,
55 c.ReplyTo, deleted, path, side, line, blob); err != nil {
56 return err
57 }
58 // The title is the parent's, so a result row can say what the comment is
59 // on without a second read; a tombstone keeps no searchable body.
60 if err := indexDocument(tx, repoID, commentKindFor(kind), c.ID, parent, title, c.Author, c.Body); err != nil {
61 return err
62 }
63 }
64 return nil
65}
66
67// CommentRow is one indexed comment. It is the shape a search result and a
68// thread renderer both need; a tombstone keeps its row so replies still have a
69// parent to point at.
70type CommentRow struct {
71 Kind string
72 Parent string
73 ID string
74 Author string
75 Created time.Time
76 Body string
77 ReplyTo string
78 Deleted bool
79 Path string
80 Side string
81 Line int
82 Blob string
83}
84
85// Comments returns one entity's comments in creation order. The detail pages
86// read the entity from git today; this exists for the surfaces that only need
87// the discussion, and for search.
88func (s *Store) Comments(repoID, kind, parent string) ([]CommentRow, error) {
89 rows, err := s.db.Query(
90 `SELECT kind, parent, id, author, created, body, reply_to, deleted, path, side, line, blob
91 FROM comments WHERE repo_id=? AND kind=? AND parent=? ORDER BY id`,
92 repoID, kind, parent)
93 if err != nil {
94 return nil, err
95 }
96 defer rows.Close()
97 return scanComments(rows)
98}
99
100// CommentSummary is one comment as a feed names it: who, when, and which
101// thread. It carries no body and no anchor.
102type CommentSummary struct {
103 Parent string
104 ID string
105 Author string
106 Created time.Time
107}
108
109// CommentSummaries returns every comment of one kind in a repository, for a feed
110// that groups them by parent. It reads no body: a feed over the whole history
111// must not pay for text it will not render.
112func (s *Store) CommentSummaries(repoID, kind string) ([]CommentSummary, error) {
113 rows, err := s.db.Query(
114 `SELECT parent, id, author, created FROM comments
115 WHERE repo_id=? AND kind=? ORDER BY parent, id`,
116 repoID, kind)
117 if err != nil {
118 return nil, err
119 }
120 defer rows.Close()
121 var out []CommentSummary
122 for rows.Next() {
123 var c CommentSummary
124 var created int64
125 if err := rows.Scan(&c.Parent, &c.ID, &c.Author, &created); err != nil {
126 return nil, err
127 }
128 c.Created = unixTime(created)
129 out = append(out, c)
130 }
131 return out, rows.Err()
132}
133
134// scanComments reads the columns Comments selects.
135func scanComments(rows *sql.Rows) ([]CommentRow, error) {
136 var out []CommentRow
137 for rows.Next() {
138 var c CommentRow
139 var created int64
140 var deleted int
141 if err := rows.Scan(&c.Kind, &c.Parent, &c.ID, &c.Author, &created, &c.Body,
142 &c.ReplyTo, &deleted, &c.Path, &c.Side, &c.Line, &c.Blob); err != nil {
143 return nil, err
144 }
145 c.Created = unixTime(created)
146 c.Deleted = deleted != 0
147 out = append(out, c)
148 }
149 return out, rows.Err()
150}
151