internal/store/repos.go

24c0aad9bf75f925497b0f6de2c8e831e67eaac9
gitbay/internal/store/repos.go history · blame · raw

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