josie / simplegit

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