internal/store/mrs.go

ea05aa6c8cd4d033d0eefd739160d29d4ead008a
gitbay/internal/store/mrs.go history · blame · raw

233 lines · 6668 bytes

  1package store
  2
  3import (
  4	"database/sql"
  5	"errors"
  6)
  7
  8type MR struct {
  9	ID           int64
 10	RepoID       int64
 11	Number       int64
 12	Author       string
 13	SourceRepoID int64 // 0 when the source repo is gone
 14	SourcePath   string // owner/name of source repo, "" when gone
 15	SourceRef    string
 16	TargetRef    string
 17	Title        string
 18	Body         string
 19	State        string // open | merged | closed | source_gone
 20	HeadSHA      string
 21	CreatedAt    string
 22	UpdatedAt    string
 23}
 24
 25type MRReview struct {
 26	Reviewer  string
 27	Verdict   string
 28	HeadSHA   string
 29	Stale     bool
 30	CreatedAt string
 31}
 32
 33func (s *Store) CreateMR(repoID, authorID, sourceRepoID int64, sourceRef, targetRef, title, body, headSHA string) (int64, error) {
 34	tx, err := s.DB.Begin()
 35	if err != nil {
 36		return 0, err
 37	}
 38	defer tx.Rollback()
 39	if _, err := tx.Exec("UPDATE repos SET mr_counter = mr_counter + 1 WHERE id = ?", repoID); err != nil {
 40		return 0, err
 41	}
 42	var n int64
 43	if err := tx.QueryRow("SELECT mr_counter FROM repos WHERE id = ?", repoID).Scan(&n); err != nil {
 44		return 0, err
 45	}
 46	if _, err := tx.Exec(`
 47		INSERT INTO merge_requests (repo_id, number, author_id, source_repo_id, source_ref, target_ref, title, body, head_sha)
 48		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
 49		repoID, n, authorID, sourceRepoID, sourceRef, targetRef, title, body, headSHA); err != nil {
 50		return 0, err
 51	}
 52	return n, tx.Commit()
 53}
 54
 55const mrSelect = `
 56	SELECT m.id, m.repo_id, m.number, u.username,
 57	       COALESCE(m.source_repo_id, 0),
 58	       COALESCE(su.username || '/' || sr.name, ''),
 59	       m.source_ref, m.target_ref, m.title, m.body, m.state, m.head_sha,
 60	       m.created_at, m.updated_at
 61	FROM merge_requests m
 62	JOIN users u ON u.id = m.author_id
 63	LEFT JOIN repos sr ON sr.id = m.source_repo_id
 64	LEFT JOIN users su ON sr.owner_kind = 'user' AND su.id = sr.owner_id`
 65
 66func scanMR(row interface{ Scan(...any) error }) (MR, error) {
 67	var m MR
 68	err := row.Scan(&m.ID, &m.RepoID, &m.Number, &m.Author, &m.SourceRepoID, &m.SourcePath,
 69		&m.SourceRef, &m.TargetRef, &m.Title, &m.Body, &m.State, &m.HeadSHA, &m.CreatedAt, &m.UpdatedAt)
 70	return m, err
 71}
 72
 73func (s *Store) MRByNumber(repoID, number int64) (MR, error) {
 74	m, err := scanMR(s.DB.QueryRow(mrSelect+" WHERE m.repo_id = ? AND m.number = ?", repoID, number))
 75	if errors.Is(err, sql.ErrNoRows) {
 76		return m, ErrNotFound
 77	}
 78	return m, err
 79}
 80
 81func (s *Store) ListMRs(repoID int64, state string) ([]MR, error) {
 82	q := mrSelect + " WHERE m.repo_id = ?"
 83	args := []any{repoID}
 84	if state != "all" {
 85		q += " AND m.state = ?"
 86		args = append(args, state)
 87	}
 88	q += " ORDER BY m.number DESC"
 89	rows, err := s.DB.Query(q, args...)
 90	if err != nil {
 91		return nil, err
 92	}
 93	defer rows.Close()
 94	var out []MR
 95	for rows.Next() {
 96		m, err := scanMR(rows)
 97		if err != nil {
 98			return nil, err
 99		}
100		out = append(out, m)
101	}
102	return out, rows.Err()
103}
104
105// OpenMRsBySource returns open (and source_gone) MRs fed by the given source
106// repo branch — the cross-repo hook effect consults this.
107func (s *Store) OpenMRsBySource(sourceRepoID int64, sourceRef string) ([]MR, error) {
108	rows, err := s.DB.Query(
109		mrSelect+" WHERE m.source_repo_id = ? AND m.source_ref = ? AND m.state IN ('open','source_gone')",
110		sourceRepoID, sourceRef)
111	if err != nil {
112		return nil, err
113	}
114	defer rows.Close()
115	var out []MR
116	for rows.Next() {
117		m, err := scanMR(rows)
118		if err != nil {
119			return nil, err
120		}
121		out = append(out, m)
122	}
123	return out, rows.Err()
124}
125
126func (s *Store) SetMRState(mrID int64, state string) error {
127	res, err := s.DB.Exec(
128		"UPDATE merge_requests SET state = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?",
129		state, mrID)
130	if err != nil {
131		return err
132	}
133	if n, _ := res.RowsAffected(); n == 0 {
134		return ErrNotFound
135	}
136	return nil
137}
138
139// UpdateMRHead records a new head and marks every review at another head
140// stale, in one transaction.
141func (s *Store) UpdateMRHead(mrID int64, headSHA string) error {
142	tx, err := s.DB.Begin()
143	if err != nil {
144		return err
145	}
146	defer tx.Rollback()
147	if _, err := tx.Exec(
148		"UPDATE merge_requests SET head_sha = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?",
149		headSHA, mrID); err != nil {
150		return err
151	}
152	if _, err := tx.Exec(
153		"UPDATE mr_reviews SET stale = 1 WHERE mr_id = ? AND head_sha <> ?", mrID, headSHA); err != nil {
154		return err
155	}
156	return tx.Commit()
157}
158
159// MarkSourceGoneForRepo flags every open MR sourced from the repo; called
160// when a fork is deleted. Head refs in the target repos are retained.
161func (s *Store) MarkSourceGoneForRepo(sourceRepoID int64) error {
162	_, err := s.DB.Exec(
163		"UPDATE merge_requests SET state = 'source_gone', updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE source_repo_id = ? AND state = 'open'",
164		sourceRepoID)
165	return err
166}
167
168func (s *Store) AddMRComment(mrID, authorID int64, body string) error {
169	_, err := s.DB.Exec(
170		"INSERT INTO mr_comments (mr_id, author_id, body) VALUES (?, ?, ?)", mrID, authorID, body)
171	return err
172}
173
174func (s *Store) ListMRComments(mrID int64) ([]IssueComment, error) {
175	rows, err := s.DB.Query(`
176		SELECT u.username, c.body, c.created_at
177		FROM mr_comments c JOIN users u ON u.id = c.author_id
178		WHERE c.mr_id = ? ORDER BY c.id`, mrID)
179	if err != nil {
180		return nil, err
181	}
182	defer rows.Close()
183	var out []IssueComment
184	for rows.Next() {
185		var c IssueComment
186		if err := rows.Scan(&c.Author, &c.Body, &c.CreatedAt); err != nil {
187			return nil, err
188		}
189		out = append(out, c)
190	}
191	return out, rows.Err()
192}
193
194func (s *Store) AddMRReview(mrID, reviewerID int64, verdict, headSHA string) error {
195	_, err := s.DB.Exec(
196		"INSERT INTO mr_reviews (mr_id, reviewer_id, verdict, head_sha) VALUES (?, ?, ?, ?)",
197		mrID, reviewerID, verdict, headSHA)
198	return err
199}
200
201func (s *Store) ListMRReviews(mrID int64) ([]MRReview, error) {
202	rows, err := s.DB.Query(`
203		SELECT u.username, r.verdict, r.head_sha, r.stale, r.created_at
204		FROM mr_reviews r JOIN users u ON u.id = r.reviewer_id
205		WHERE r.mr_id = ? ORDER BY r.id`, mrID)
206	if err != nil {
207		return nil, err
208	}
209	defer rows.Close()
210	var out []MRReview
211	for rows.Next() {
212		var r MRReview
213		var stale int
214		if err := rows.Scan(&r.Reviewer, &r.Verdict, &r.HeadSHA, &stale, &r.CreatedAt); err != nil {
215			return nil, err
216		}
217		r.Stale = stale != 0
218		out = append(out, r)
219	}
220	return out, rows.Err()
221}
222
223// PrimaryVerifiedEmail returns the user's primary email if verified, else "".
224func (s *Store) PrimaryVerifiedEmail(userID int64) (string, error) {
225	var addr string
226	err := s.DB.QueryRow(
227		"SELECT address FROM emails WHERE user_id = ? AND is_primary = 1 AND verified_at IS NOT NULL",
228		userID).Scan(&addr)
229	if errors.Is(err, sql.ErrNoRows) {
230		return "", nil
231	}
232	return addr, err
233}