internal/store/diffcomments.go
130 lines · 3834 bytes
1package store
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7)
8
9type DiffComment struct {
10 ID int64
11 Author string
12 HeadSHA string
13 Path string
14 Side string
15 Line int64
16 Body string
17 ReplyTo int64 // 0 for thread roots
18 ResolvedBy string
19 CreatedAt string
20}
21
22// AddDiffComment creates a thread root (replyTo 0) or a reply. Replies
23// inherit the root's anchor and must belong to the same MR.
24func (s *Store) AddDiffComment(mrID, authorID int64, headSHA, path, side string, line int64, body string, replyTo int64) (int64, error) {
25 if replyTo != 0 {
26 var rootMR int64
27 var rootReply sql.NullInt64
28 err := s.DB.QueryRow(
29 "SELECT mr_id, reply_to FROM mr_diff_comments WHERE id = ?", replyTo).Scan(&rootMR, &rootReply)
30 if errors.Is(err, sql.ErrNoRows) {
31 return 0, fmt.Errorf("no thread %d: %w", replyTo, ErrNotFound)
32 }
33 if err != nil {
34 return 0, err
35 }
36 if rootMR != mrID {
37 return 0, fmt.Errorf("thread %d belongs to a different merge request", replyTo)
38 }
39 if rootReply.Valid {
40 return 0, fmt.Errorf("reply to the thread root %d, not to a reply", rootReply.Int64)
41 }
42 err = s.DB.QueryRow(
43 "SELECT head_sha, path, side, line FROM mr_diff_comments WHERE id = ?", replyTo).
44 Scan(&headSHA, &path, &side, &line)
45 if err != nil {
46 return 0, err
47 }
48 }
49 var reply any
50 if replyTo != 0 {
51 reply = replyTo
52 }
53 res, err := s.DB.Exec(`
54 INSERT INTO mr_diff_comments (mr_id, author_id, head_sha, path, side, line, body, reply_to)
55 VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
56 mrID, authorID, headSHA, path, side, line, body, reply)
57 if err != nil {
58 return 0, err
59 }
60 return res.LastInsertId()
61}
62
63// ListDiffComments returns every diff comment on an MR, roots and replies,
64// oldest first.
65func (s *Store) ListDiffComments(mrID int64) ([]DiffComment, error) {
66 rows, err := s.DB.Query(`
67 SELECT c.id, u.username, c.head_sha, c.path, c.side, c.line, c.body,
68 COALESCE(c.reply_to, 0), COALESCE(r.username, ''), c.created_at
69 FROM mr_diff_comments c
70 JOIN users u ON u.id = c.author_id
71 LEFT JOIN users r ON r.id = c.resolved_by
72 WHERE c.mr_id = ? ORDER BY c.id`, mrID)
73 if err != nil {
74 return nil, err
75 }
76 defer rows.Close()
77 var out []DiffComment
78 for rows.Next() {
79 var c DiffComment
80 if err := rows.Scan(&c.ID, &c.Author, &c.HeadSHA, &c.Path, &c.Side, &c.Line, &c.Body,
81 &c.ReplyTo, &c.ResolvedBy, &c.CreatedAt); err != nil {
82 return nil, err
83 }
84 out = append(out, c)
85 }
86 return out, rows.Err()
87}
88
89// SetThreadResolved resolves or unresolves a thread root.
90func (s *Store) SetThreadResolved(mrID, rootID, byUser int64, resolved bool) error {
91 var q string
92 var args []any
93 if resolved {
94 q = `UPDATE mr_diff_comments SET resolved_at = strftime('%Y-%m-%dT%H:%M:%fZ','now'), resolved_by = ?
95 WHERE id = ? AND mr_id = ? AND reply_to IS NULL`
96 args = []any{byUser, rootID, mrID}
97 } else {
98 q = `UPDATE mr_diff_comments SET resolved_at = NULL, resolved_by = NULL
99 WHERE id = ? AND mr_id = ? AND reply_to IS NULL`
100 args = []any{rootID, mrID}
101 }
102 res, err := s.DB.Exec(q, args...)
103 if err != nil {
104 return err
105 }
106 if n, _ := res.RowsAffected(); n == 0 {
107 return ErrNotFound
108 }
109 return nil
110}
111
112// DiffCommentAuthor returns the author id of one comment.
113func (s *Store) DiffCommentAuthor(mrID, id int64) (int64, error) {
114 var author int64
115 err := s.DB.QueryRow(
116 "SELECT author_id FROM mr_diff_comments WHERE id = ? AND mr_id = ?", id, mrID).Scan(&author)
117 if errors.Is(err, sql.ErrNoRows) {
118 return 0, ErrNotFound
119 }
120 return author, err
121}
122
123// UnresolvedThreadCount counts unresolved thread roots on an MR.
124func (s *Store) UnresolvedThreadCount(mrID int64) (int, error) {
125 var n int
126 err := s.DB.QueryRow(
127 "SELECT COUNT(*) FROM mr_diff_comments WHERE mr_id = ? AND reply_to IS NULL AND resolved_at IS NULL",
128 mrID).Scan(&n)
129 return n, err
130}