internal/store/repos.go

d8bcf1584ceb0dae5906070084c9b7b85174cfa2
gitbay/internal/store/repos.go history · blame · raw

215 lines · 6651 bytes

  1package store
  2
  3import (
  4	"database/sql"
  5	"encoding/json"
  6	"errors"
  7	"fmt"
  8	"strings"
  9)
 10
 11type Repo struct {
 12	ID            int64
 13	OwnerKind     string // user | org
 14	OwnerID       int64
 15	OwnerName     string // resolved for display and disk paths
 16	Name          string
 17	Visibility    string // public | private
 18	DefaultBranch string
 19	Settings      RepoSettings
 20}
 21
 22type RepoSettings struct {
 23	ProtectedBranches    []string `json:"protected_branches,omitempty"`
 24	RequireSignedCommits bool     `json:"require_signed_commits,omitempty"`
 25	GitDaemon            bool     `json:"git_daemon,omitempty"`
 26}
 27
 28// Path returns the canonical owner/name form.
 29func (r Repo) Path() string { return r.OwnerName + "/" + r.Name }
 30
 31func (s *Store) CreateRepo(ownerKind string, ownerID int64, name, visibility string) (int64, error) {
 32	res, err := s.DB.Exec(
 33		"INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES (?, ?, ?, ?)",
 34		ownerKind, ownerID, name, visibility)
 35	if err != nil {
 36		if isUniqueErr(err) {
 37			return 0, fmt.Errorf("repository %q already exists", name)
 38		}
 39		return 0, err
 40	}
 41	return res.LastInsertId()
 42}
 43
 44// RepoByPath resolves "owner/name". Only user owners exist until orgs land.
 45func (s *Store) RepoByPath(path string) (Repo, error) {
 46	owner, name, ok := strings.Cut(strings.TrimSuffix(strings.TrimPrefix(path, "/"), ".git"), "/")
 47	if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
 48		return Repo{}, fmt.Errorf("%w: repository path must be owner/name", ErrNotFound)
 49	}
 50	var r Repo
 51	var settingsJSON string
 52	err := s.DB.QueryRow(`
 53		SELECT r.id, r.owner_kind, r.owner_id, u.username, r.name, r.visibility, r.default_branch, r.settings_json
 54		FROM repos r JOIN users u ON r.owner_kind = 'user' AND u.id = r.owner_id
 55		WHERE u.username = ? AND r.name = ?`, owner, name).
 56		Scan(&r.ID, &r.OwnerKind, &r.OwnerID, &r.OwnerName, &r.Name, &r.Visibility, &r.DefaultBranch, &settingsJSON)
 57	if errors.Is(err, sql.ErrNoRows) {
 58		return Repo{}, ErrNotFound
 59	}
 60	if err != nil {
 61		return Repo{}, err
 62	}
 63	if err := json.Unmarshal([]byte(settingsJSON), &r.Settings); err != nil {
 64		return Repo{}, fmt.Errorf("repo %d settings: %w", r.ID, err)
 65	}
 66	return r, nil
 67}
 68
 69func (s *Store) SetRepoSettings(repoID int64, settings RepoSettings) error {
 70	raw, err := json.Marshal(settings)
 71	if err != nil {
 72		return err
 73	}
 74	_, err = s.DB.Exec("UPDATE repos SET settings_json = ? WHERE id = ?", string(raw), repoID)
 75	return err
 76}
 77
 78func (s *Store) DeleteRepo(repoID int64) error {
 79	res, err := s.DB.Exec("DELETE FROM repos WHERE id = ?", repoID)
 80	if err != nil {
 81		return err
 82	}
 83	if n, _ := res.RowsAffected(); n == 0 {
 84		return ErrNotFound
 85	}
 86	return nil
 87}
 88
 89// ListReposForUser returns repos the user owns or has an explicit grant on.
 90func (s *Store) ListReposForUser(userID int64) ([]Repo, error) {
 91	rows, err := s.DB.Query(`
 92		SELECT DISTINCT r.id, r.owner_kind, r.owner_id, u.username, r.name, r.visibility, r.default_branch, r.settings_json
 93		FROM repos r
 94		JOIN users u ON r.owner_kind = 'user' AND u.id = r.owner_id
 95		LEFT JOIN repo_access a ON a.repo_id = r.id AND a.subject_kind = 'user' AND a.subject_id = ?
 96		WHERE r.owner_id = ? OR a.subject_id IS NOT NULL
 97		ORDER BY u.username, r.name`, userID, userID)
 98	if err != nil {
 99		return nil, err
100	}
101	defer rows.Close()
102	var out []Repo
103	for rows.Next() {
104		var r Repo
105		var settingsJSON string
106		if err := rows.Scan(&r.ID, &r.OwnerKind, &r.OwnerID, &r.OwnerName, &r.Name, &r.Visibility, &r.DefaultBranch, &settingsJSON); err != nil {
107			return nil, err
108		}
109		if err := json.Unmarshal([]byte(settingsJSON), &r.Settings); err != nil {
110			return nil, err
111		}
112		out = append(out, r)
113	}
114	return out, rows.Err()
115}
116
117// AccessRole returns the explicit grant for userID on repoID ("" if none).
118func (s *Store) AccessRole(repoID, userID int64) (string, error) {
119	var role string
120	err := s.DB.QueryRow(
121		"SELECT role FROM repo_access WHERE repo_id = ? AND subject_kind = 'user' AND subject_id = ?",
122		repoID, userID).Scan(&role)
123	if errors.Is(err, sql.ErrNoRows) {
124		return "", nil
125	}
126	return role, err
127}
128
129func (s *Store) GrantAccess(repoID, userID int64, role string) error {
130	_, err := s.DB.Exec(`
131		INSERT INTO repo_access (repo_id, subject_kind, subject_id, role) VALUES (?, 'user', ?, ?)
132		ON CONFLICT (repo_id, subject_kind, subject_id) DO UPDATE SET role = excluded.role`,
133		repoID, userID, role)
134	return err
135}
136
137func (s *Store) RevokeAccess(repoID, userID int64) error {
138	res, err := s.DB.Exec(
139		"DELETE FROM repo_access WHERE repo_id = ? AND subject_kind = 'user' AND subject_id = ?",
140		repoID, userID)
141	if err != nil {
142		return err
143	}
144	if n, _ := res.RowsAffected(); n == 0 {
145		return ErrNotFound
146	}
147	return nil
148}
149
150type AccessEntry struct {
151	Username string
152	Role     string
153}
154
155func (s *Store) ListAccess(repoID int64) ([]AccessEntry, error) {
156	rows, err := s.DB.Query(`
157		SELECT u.username, a.role FROM repo_access a
158		JOIN users u ON a.subject_kind = 'user' AND u.id = a.subject_id
159		WHERE a.repo_id = ? ORDER BY u.username`, repoID)
160	if err != nil {
161		return nil, err
162	}
163	defer rows.Close()
164	var out []AccessEntry
165	for rows.Next() {
166		var e AccessEntry
167		if err := rows.Scan(&e.Username, &e.Role); err != nil {
168			return nil, err
169		}
170		out = append(out, e)
171	}
172	return out, rows.Err()
173}
174
175func (s *Store) RepoByID(id int64) (Repo, error) {
176	var r Repo
177	var settingsJSON string
178	err := s.DB.QueryRow(`
179		SELECT r.id, r.owner_kind, r.owner_id, u.username, r.name, r.visibility, r.default_branch, r.settings_json
180		FROM repos r JOIN users u ON r.owner_kind = 'user' AND u.id = r.owner_id
181		WHERE r.id = ?`, id).
182		Scan(&r.ID, &r.OwnerKind, &r.OwnerID, &r.OwnerName, &r.Name, &r.Visibility, &r.DefaultBranch, &settingsJSON)
183	if errors.Is(err, sql.ErrNoRows) {
184		return Repo{}, ErrNotFound
185	}
186	if err != nil {
187		return Repo{}, err
188	}
189	if err := json.Unmarshal([]byte(settingsJSON), &r.Settings); err != nil {
190		return Repo{}, err
191	}
192	return r, nil
193}
194
195// ListPublicRepos returns all public repositories, for the anonymous index.
196func (s *Store) ListPublicRepos() ([]Repo, error) {
197	rows, err := s.DB.Query(`
198		SELECT r.id, r.owner_kind, r.owner_id, u.username, r.name, r.visibility, r.default_branch, r.settings_json
199		FROM repos r JOIN users u ON r.owner_kind = 'user' AND u.id = r.owner_id
200		WHERE r.visibility = 'public' ORDER BY u.username, r.name`)
201	if err != nil {
202		return nil, err
203	}
204	defer rows.Close()
205	var out []Repo
206	for rows.Next() {
207		var r Repo
208		var settingsJSON string
209		if err := rows.Scan(&r.ID, &r.OwnerKind, &r.OwnerID, &r.OwnerName, &r.Name, &r.Visibility, &r.DefaultBranch, &settingsJSON); err != nil {
210			return nil, err
211		}
212		out = append(out, r)
213	}
214	return out, rows.Err()
215}