package db import ( "database/sql" "errors" "fmt" ) // Repo is a row in the repos table. type Repo struct { ID int64 OwnerID int64 Name string Description string Visibility string DefaultBranch string PushedAt sql.NullInt64 } // CreateRepo inserts a repo row relying on column defaults for // default_branch and created_at, then returns the stored row. func CreateRepo(database *sql.DB, ownerID int64, name, description, visibility string) (Repo, error) { id, err := execLastID(database, `INSERT INTO repos (owner_id, name, description, visibility) VALUES (?, ?, ?, ?)`, ownerID, name, description, visibility) if err != nil { return Repo{}, fmt.Errorf("create repo %s: %w", name, err) } return GetRepoByID(database, id) } const repoColumns = `SELECT id, owner_id, name, description, visibility, default_branch, pushed_at FROM repos WHERE ` // scanRepo reads one repos row (see repoColumns) into repo. func scanRepo(s rowScanner, repo *Repo) error { return s.Scan(&repo.ID, &repo.OwnerID, &repo.Name, &repo.Description, &repo.Visibility, &repo.DefaultBranch, &repo.PushedAt) } // GetRepoByID looks up a repo by primary key. func GetRepoByID(database *sql.DB, id int64) (Repo, error) { var repo Repo err := scanRepo(database.QueryRow(repoColumns+`id = ?`, id), &repo) if errors.Is(err, sql.ErrNoRows) { return Repo{}, fmt.Errorf("repo %d: %w", id, ErrNotFound) } if err != nil { return Repo{}, fmt.Errorf("get repo %d: %w", id, err) } return repo, nil } // GetRepoByName looks up a repo by owner username and repo name. func GetRepoByName(database *sql.DB, ownerName, repoName string) (Repo, error) { var repo Repo err := scanRepo(database.QueryRow( repoColumns+`owner_id = (SELECT id FROM users WHERE username = ?) AND name = ?`, ownerName, repoName), &repo) if errors.Is(err, sql.ErrNoRows) { return Repo{}, fmt.Errorf("repo %s/%s: %w", ownerName, repoName, ErrNotFound) } if err != nil { return Repo{}, fmt.Errorf("get repo %s/%s: %w", ownerName, repoName, err) } return repo, nil } // DeleteRepo removes a repo row; its bare directory on disk is the // caller's responsibility. func DeleteRepo(database *sql.DB, id int64) error { return execScoped(database, fmt.Sprintf("delete repo %d", id), `DELETE FROM repos WHERE id = ?`, id) } // UpdateRepoVisibility flips a repo between public and private. func UpdateRepoVisibility(database *sql.DB, id int64, visibility string) error { return execScoped(database, fmt.Sprintf("update repo %d visibility", id), `UPDATE repos SET visibility = ? WHERE id = ?`, visibility, id) } // RenameRepo changes a repo's name; the caller moves the directory on disk. func RenameRepo(database *sql.DB, id int64, name string) error { return execScoped(database, fmt.Sprintf("rename repo %d", id), `UPDATE repos SET name = ? WHERE id = ?`, name, id) } // RecordPush updates a repo's pushed_at and, when defaultBranch is // non-empty, its default_branch. It returns ErrNotFound when no row // matches (a push to a repo with no metadata row). func RecordPush(database *sql.DB, ownerName, repoName string, pushedAt int64, defaultBranch string) error { query := `UPDATE repos SET pushed_at = ? WHERE owner_id = (SELECT id FROM users WHERE username = ?) AND name = ?` args := []any{pushedAt, ownerName, repoName} if defaultBranch != "" { query = `UPDATE repos SET pushed_at = ?, default_branch = ? WHERE owner_id = (SELECT id FROM users WHERE username = ?) AND name = ?` args = []any{pushedAt, defaultBranch, ownerName, repoName} } return execScoped(database, fmt.Sprintf("record push %s/%s", ownerName, repoName), query, args...) } // RepoView is a repo row plus its owner's username, for listings. type RepoView struct { Repo OwnerName string } // ListRepos returns public repos plus, when viewerID is non-zero, that // user's own repos. Ordered by owner then name. func ListRepos(database *sql.DB, viewerID int64) ([]RepoView, error) { repos, err := listQuery(database, `SELECT r.id, r.owner_id, r.name, r.description, r.visibility, r.default_branch, r.pushed_at, u.username FROM repos r JOIN users u ON u.id = r.owner_id WHERE r.visibility = 'public' OR r.owner_id = ? ORDER BY u.username, r.name`, []any{viewerID}, scanRepoView) if err != nil { return nil, fmt.Errorf("list repos: %w", err) } return repos, nil } // ListAllRepos returns every repo regardless of visibility; ListRepos is // viewer-scoped, so it cannot give a full inventory. func ListAllRepos(database *sql.DB) ([]RepoView, error) { repos, err := listQuery(database, `SELECT r.id, r.owner_id, r.name, r.description, r.visibility, r.default_branch, r.pushed_at, u.username FROM repos r JOIN users u ON u.id = r.owner_id ORDER BY u.username, r.name`, nil, scanRepoView) if err != nil { return nil, fmt.Errorf("list all repos: %w", err) } return repos, nil } // scanRepoView reads one repos+owner row (see ListRepos) into view. func scanRepoView(s rowScanner, view *RepoView) error { return s.Scan(&view.ID, &view.OwnerID, &view.Name, &view.Description, &view.Visibility, &view.DefaultBranch, &view.PushedAt, &view.OwnerName) }