internal/store/statuses.go

v1.43.0
gitbay/internal/store/statuses.go history · blame · raw

190 lines · 6074 bytes

  1package store
  2
  3import (
  4	"strings"
  5	"time"
  6)
  7
  8type CommitStatus struct {
  9	Context     string
 10	State       string // pending | success | failure | error | skipped
 11	Description string
 12	TargetURL   string
 13	Creator     string
 14	UpdatedAt   string
 15}
 16
 17// SetCommitStatus upserts the latest state for one context on one commit.
 18// A zero creatorID records no creator (system actions like the scheduler).
 19func (s *Store) SetCommitStatus(repoID int64, sha, context, state, description, targetURL string, creatorID int64) error {
 20	var creator any
 21	if creatorID != 0 {
 22		creator = creatorID
 23	}
 24	_, err := s.DB.Exec(`
 25		INSERT INTO commit_statuses (repo_id, commit_sha, context, state, description, target_url, creator_id)
 26		VALUES (?, ?, ?, ?, ?, ?, ?)
 27		ON CONFLICT (repo_id, commit_sha, context) DO UPDATE SET
 28			state = excluded.state, description = excluded.description,
 29			target_url = excluded.target_url, creator_id = excluded.creator_id,
 30			updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')`,
 31		repoID, sha, context, state, description, targetURL, creator)
 32	return err
 33}
 34
 35// ListCommitStatuses returns the latest status per context for a commit.
 36func (s *Store) ListCommitStatuses(repoID int64, sha string) ([]CommitStatus, error) {
 37	rows, err := s.DB.Query(`
 38		SELECT cs.context, cs.state, cs.description, cs.target_url, COALESCE(u.username, ''), cs.updated_at
 39		FROM commit_statuses cs LEFT JOIN users u ON u.id = cs.creator_id
 40		WHERE cs.repo_id = ? AND cs.commit_sha = ? ORDER BY cs.context`, repoID, sha)
 41	if err != nil {
 42		return nil, err
 43	}
 44	defer rows.Close()
 45	var out []CommitStatus
 46	for rows.Next() {
 47		var c CommitStatus
 48		if err := rows.Scan(&c.Context, &c.State, &c.Description, &c.TargetURL, &c.Creator, &c.UpdatedAt); err != nil {
 49			return nil, err
 50		}
 51		out = append(out, c)
 52	}
 53	return out, rows.Err()
 54}
 55
 56// CommitStatusesFor is ListCommitStatuses for every commit on a list
 57// page in one query, keyed by commit.
 58func (s *Store) CommitStatusesFor(repoID int64, shas []string) (map[string][]CommitStatus, error) {
 59	out := map[string][]CommitStatus{}
 60	if len(shas) == 0 {
 61		return out, nil
 62	}
 63	args := []any{repoID}
 64	for _, sha := range shas {
 65		args = append(args, sha)
 66	}
 67	rows, err := s.DB.Query(`
 68		SELECT cs.commit_sha, cs.context, cs.state, cs.description, cs.target_url, COALESCE(u.username, ''), cs.updated_at
 69		FROM commit_statuses cs LEFT JOIN users u ON u.id = cs.creator_id
 70		WHERE cs.repo_id = ? AND cs.commit_sha IN (?`+strings.Repeat(",?", len(shas)-1)+`)
 71		ORDER BY cs.context`, args...)
 72	if err != nil {
 73		return nil, err
 74	}
 75	defer rows.Close()
 76	for rows.Next() {
 77		var sha string
 78		var c CommitStatus
 79		if err := rows.Scan(&sha, &c.Context, &c.State, &c.Description, &c.TargetURL, &c.Creator, &c.UpdatedAt); err != nil {
 80			return nil, err
 81		}
 82		out[sha] = append(out[sha], c)
 83	}
 84	return out, rows.Err()
 85}
 86
 87// RepoHasStatuses reports whether anything has ever reported a status in
 88// this repository. It is how require_checks tells a repository whose
 89// checks come from outside — `status set`, with no .gitbay/ci.yml — from
 90// one that has no checks at all.
 91func (s *Store) RepoHasStatuses(repoID int64) (bool, error) {
 92	var n int
 93	err := s.DB.QueryRow(`SELECT EXISTS(SELECT 1 FROM commit_statuses WHERE repo_id = ?)`, repoID).Scan(&n)
 94	return n == 1, err
 95}
 96
 97// CombinedStatus reduces per-context states to one: error/failure dominate,
 98// then pending, then success; "" when no statuses exist.
 99// CombinedStatusFor returns the combined state for each of several commits
100// in one query. The log lists fifty commits at a time; asking per commit
101// turns one page into fifty round trips.
102func (s *Store) CombinedStatusFor(repoID int64, shas []string) (map[string]string, error) {
103	out := map[string]string{}
104	if len(shas) == 0 {
105		return out, nil
106	}
107	args := make([]any, 0, len(shas)+1)
108	args = append(args, repoID)
109	for _, sha := range shas {
110		args = append(args, sha)
111	}
112	q := `SELECT commit_sha, state FROM commit_statuses WHERE repo_id = ? AND commit_sha IN (?` +
113		strings.Repeat(",?", len(shas)-1) + `)`
114	rows, err := s.DB.Query(q, args...)
115	if err != nil {
116		return nil, err
117	}
118	defer rows.Close()
119	for rows.Next() {
120		var sha, state string
121		if err := rows.Scan(&sha, &state); err != nil {
122			return nil, err
123		}
124		// Same precedence as CombinedStatus, folded in as rows arrive.
125		switch {
126		case out[sha] == "failure":
127		case state == "error" || state == "failure":
128			out[sha] = "failure"
129		case state == "pending":
130			out[sha] = "pending"
131		case out[sha] == "":
132			out[sha] = "success"
133		}
134	}
135	return out, rows.Err()
136}
137
138func CombinedStatus(statuses []CommitStatus) string {
139	if len(statuses) == 0 {
140		return ""
141	}
142	combined := "success"
143	for _, s := range statuses {
144		switch s.State {
145		case "error", "failure":
146			return "failure"
147		case "pending":
148			combined = "pending"
149		}
150	}
151	return combined
152}
153
154// Check is a commit status paired with the build behind it. Only ci/<job>
155// statuses have one; anything posted through `status set` reports just the
156// state and when it last changed.
157type Check struct {
158	CommitStatus
159	Build    int64         // build number, 0 when the check is not CI's
160	Duration time.Duration // 0 until that build has finished
161}
162
163// ChecksForCommit lists a commit's checks with their timing, and reduces
164// them to one combined state. Two queries at most, whatever the surface.
165func (s *Store) ChecksForCommit(repoID int64, sha string) ([]Check, string, error) {
166	statuses, err := s.ListCommitStatuses(repoID, sha)
167	if err != nil {
168		return nil, "", err
169	}
170	var builds map[string]Build
171	for _, st := range statuses {
172		if strings.HasPrefix(st.Context, "ci/") {
173			if builds, err = s.BuildsForCommit(repoID, sha); err != nil {
174				return nil, "", err
175			}
176			break
177		}
178	}
179	checks := make([]Check, 0, len(statuses))
180	for _, st := range statuses {
181		c := Check{CommitStatus: st}
182		if job, ok := strings.CutPrefix(st.Context, "ci/"); ok {
183			if b, ok := builds[job]; ok {
184				c.Build, c.Duration = b.Number, b.Elapsed()
185			}
186		}
187		checks = append(checks, c)
188	}
189	return checks, CombinedStatus(statuses), nil
190}