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)
}