internal/store/diffcomments.go

5ffa892991a5437dc53680ea1609fabee4b85d42
gitbay/internal/store/diffcomments.go history · blame · raw

176 lines · 5716 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	// Pending marks a comment in a review its author has not submitted.
 20	// Only they can see it, and `mr review` publishes it.
 21	Pending   bool
 22	CreatedAt string
 23}
 24
 25// AddDiffComment creates a thread root (replyTo 0) or a reply. Replies
 26// inherit the root's anchor and must belong to the same MR.
 27func (s *Store) AddDiffComment(mrID, authorID int64, headSHA, path, side string, line int64, body string, replyTo int64, pending bool) (int64, error) {
 28	if replyTo != 0 {
 29		var rootMR int64
 30		var rootReply sql.NullInt64
 31		err := s.DB.QueryRow(
 32			"SELECT mr_id, reply_to FROM mr_diff_comments WHERE id = ?", replyTo).Scan(&rootMR, &rootReply)
 33		if errors.Is(err, sql.ErrNoRows) {
 34			return 0, fmt.Errorf("no thread %d: %w", replyTo, ErrNotFound)
 35		}
 36		if err != nil {
 37			return 0, err
 38		}
 39		if rootMR != mrID {
 40			return 0, fmt.Errorf("thread %d belongs to a different merge request", replyTo)
 41		}
 42		if rootReply.Valid {
 43			return 0, fmt.Errorf("reply to the thread root %d, not to a reply", rootReply.Int64)
 44		}
 45		err = s.DB.QueryRow(
 46			"SELECT head_sha, path, side, line FROM mr_diff_comments WHERE id = ?", replyTo).
 47			Scan(&headSHA, &path, &side, &line)
 48		if err != nil {
 49			return 0, err
 50		}
 51	}
 52	var reply any
 53	if replyTo != 0 {
 54		reply = replyTo
 55	}
 56	res, err := s.DB.Exec(`
 57		INSERT INTO mr_diff_comments (mr_id, author_id, head_sha, path, side, line, body, reply_to, pending)
 58		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
 59		mrID, authorID, headSHA, path, side, line, body, reply, pending)
 60	if err != nil {
 61		return 0, err
 62	}
 63	return res.LastInsertId()
 64}
 65
 66// ListDiffComments returns the diff comments on an MR that viewer may
 67// see: everything published, plus their own pending ones. viewer 0 is an
 68// anonymous reader, who sees only what is published.
 69func (s *Store) ListDiffComments(mrID, viewer int64) ([]DiffComment, error) {
 70	rows, err := s.DB.Query(`
 71		SELECT c.id, u.username, c.head_sha, c.path, c.side, c.line, c.body,
 72		       COALESCE(c.reply_to, 0), COALESCE(r.username, ''), c.pending, c.created_at
 73		FROM mr_diff_comments c
 74		JOIN users u ON u.id = c.author_id
 75		LEFT JOIN users r ON r.id = c.resolved_by
 76		WHERE c.mr_id = ?1 AND (c.pending = 0 OR c.author_id = ?2)
 77		ORDER BY c.id`, mrID, viewer)
 78	if err != nil {
 79		return nil, err
 80	}
 81	defer rows.Close()
 82	var out []DiffComment
 83	for rows.Next() {
 84		var c DiffComment
 85		if err := rows.Scan(&c.ID, &c.Author, &c.HeadSHA, &c.Path, &c.Side, &c.Line, &c.Body,
 86			&c.ReplyTo, &c.ResolvedBy, &c.Pending, &c.CreatedAt); err != nil {
 87			return nil, err
 88		}
 89		out = append(out, c)
 90	}
 91	return out, rows.Err()
 92}
 93
 94// SetThreadResolved resolves or unresolves a thread root.
 95func (s *Store) SetThreadResolved(mrID, rootID, byUser int64, resolved bool) error {
 96	var q string
 97	var args []any
 98	if resolved {
 99		q = `UPDATE mr_diff_comments SET resolved_at = strftime('%Y-%m-%dT%H:%M:%fZ','now'), resolved_by = ?
100		     WHERE id = ? AND mr_id = ? AND reply_to IS NULL`
101		args = []any{byUser, rootID, mrID}
102	} else {
103		q = `UPDATE mr_diff_comments SET resolved_at = NULL, resolved_by = NULL
104		     WHERE id = ? AND mr_id = ? AND reply_to IS NULL`
105		args = []any{rootID, mrID}
106	}
107	res, err := s.DB.Exec(q, args...)
108	if err != nil {
109		return err
110	}
111	if n, _ := res.RowsAffected(); n == 0 {
112		return ErrNotFound
113	}
114	return nil
115}
116
117// DiffCommentAuthor returns the author id of one comment.
118func (s *Store) DiffCommentAuthor(mrID, id int64) (int64, error) {
119	var author int64
120	err := s.DB.QueryRow(
121		"SELECT author_id FROM mr_diff_comments WHERE id = ? AND mr_id = ?", id, mrID).Scan(&author)
122	if errors.Is(err, sql.ErrNoRows) {
123		return 0, ErrNotFound
124	}
125	return author, err
126}
127
128// UnresolvedThreadCount counts unresolved thread roots on an MR.
129//
130// Pending roots are excluded: an unsubmitted comment is one reviewer's
131// note to themselves, and blocking a merge on it would let anyone stall
132// a merge request with a thread nobody else can see or resolve.
133func (s *Store) UnresolvedThreadCount(mrID int64) (int, error) {
134	var n int
135	err := s.DB.QueryRow(`
136		SELECT COUNT(*) FROM mr_diff_comments
137		WHERE mr_id = ? AND reply_to IS NULL AND resolved_at IS NULL AND pending = 0`,
138		mrID).Scan(&n)
139	return n, err
140}
141
142// PublishPendingComments makes an author's pending comments on an MR
143// visible, and reports how many. This is what `mr review` does with the
144// batch the reviewer composed.
145func (s *Store) PublishPendingComments(mrID, authorID int64) (int64, error) {
146	res, err := s.DB.Exec(
147		"UPDATE mr_diff_comments SET pending = 0 WHERE mr_id = ? AND author_id = ? AND pending = 1",
148		mrID, authorID)
149	if err != nil {
150		return 0, err
151	}
152	return res.RowsAffected()
153}
154
155// DiscardPendingComments deletes an author's unsubmitted comments. Only
156// pending rows: a published comment is part of the conversation and is
157// not something its author can quietly take back.
158func (s *Store) DiscardPendingComments(mrID, authorID int64) (int64, error) {
159	res, err := s.DB.Exec(
160		"DELETE FROM mr_diff_comments WHERE mr_id = ? AND author_id = ? AND pending = 1",
161		mrID, authorID)
162	if err != nil {
163		return 0, err
164	}
165	return res.RowsAffected()
166}
167
168// CountPendingComments is how many unsubmitted comments an author holds
169// on an MR, for the reminder that they have a review in progress.
170func (s *Store) CountPendingComments(mrID, authorID int64) int {
171	var n int
172	s.DB.QueryRow(
173		"SELECT COUNT(*) FROM mr_diff_comments WHERE mr_id = ? AND author_id = ? AND pending = 1",
174		mrID, authorID).Scan(&n)
175	return n
176}