krz/gitbay

A CLI-first git forge.

clone: git clone https://gitbay.org/krz/gitbay.git

repo-descriptions: internal/store/mrs.go · raw

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