internal/store/mrs.go

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

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