Skip to content
quarry Artifact [41531117dc]
Login

Artifact [41531117dc]

Artifact 41531117dc52dbcafaf67cc1f7ce2c8013b152a3b20436ac11d194ab0c0e46bb:


// repo.go stores which repositories the forge serves. 0001 §8 lists the
// repo registry as forge OLTP state alongside accounts and tokens; until
// now it lived only in argv, so adding a repository meant editing the
// server's command line and restarting every other repository with it.
package oltp

import (
	"database/sql"
	"errors"
	"fmt"
	"path/filepath"
	"regexp"
	"strings"
	"time"
)

const repoSchema = `
CREATE TABLE IF NOT EXISTS repo (
	name    TEXT PRIMARY KEY, -- the forge's name, as it appears in URLs
	path    TEXT NOT NULL,    -- directory holding .zit, on this node
	created TEXT NOT NULL
);`

// RepoName is the accepted repository name. Same rule the --repo flag
// enforces, kept here so a name that arrives through the API cannot be
// one the flag would have refused.
//
// It is deliberately narrow: the name appears in a URL path segment, so
// anything needing escaping to be safe there is better rejected than
// encoded.
var RepoName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`)

// Repo is one registered repository.
type Repo struct {
	Name    string
	Path    string
	Created string
}

// AddRepo registers a repository. The path is stored absolute: a
// relative one would resolve against whatever directory the server
// happened to start in, which is not a property anybody intends to
// depend on and changes silently between a shell and a service manager.
func (d *DB) AddRepo(name, path string) error {
	if !RepoName.MatchString(name) {
		return fmt.Errorf("oltp: repository name %q: use letters, digits, dot, dash, underscore", name)
	}
	if path == "" {
		return errors.New("oltp: repository needs a path")
	}
	abs, err := filepath.Abs(path)
	if err != nil {
		return err
	}
	_, err = d.db.Exec(`INSERT INTO repo (name, path, created) VALUES (?, ?, ?)`,
		name, abs, time.Now().UTC().Format(time.RFC3339))
	if err != nil && strings.Contains(err.Error(), "UNIQUE") {
		return fmt.Errorf("oltp: repository %q already registered", name)
	}
	return err
}

// DeleteRepo unregisters a repository. It removes the registration only
// — the store on disk and its derived index are left alone, because
// "stop serving this" and "destroy this" are different intentions and
// only one of them is reversible.
func (d *DB) DeleteRepo(name string) error {
	_, err := d.db.Exec(`DELETE FROM repo WHERE name = ?`, name)
	return err
}

// Repos lists registered repositories, name-ordered.
func (d *DB) Repos() ([]Repo, error) {
	rows, err := d.db.Query(`SELECT name, path, created FROM repo ORDER BY name`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Repo
	for rows.Next() {
		var r Repo
		if err := rows.Scan(&r.Name, &r.Path, &r.Created); err != nil {
			return nil, err
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

// RepoByName returns one registration.
func (d *DB) RepoByName(name string) (Repo, bool, error) {
	var r Repo
	err := d.db.QueryRow(`SELECT name, path, created FROM repo WHERE name = ?`, name).
		Scan(&r.Name, &r.Path, &r.Created)
	if errors.Is(err, sql.ErrNoRows) {
		return Repo{}, false, nil
	}
	return r, err == nil, err
}