// repoplace.go is where a repository has BEEN.
//
// A repository appears under a namespace — a team when a team holds it,
// its owner's handle otherwise — and that namespace changes when it
// moves into or out of a team. Every link anybody saved carries the
// namespace of the moment, so a move made them all 404 (c12a84b2, G-52
// as jva measured it from jan's seat): the bare /r/<repo> form was the
// only saved link that survived. The bare form redirects because the
// forge knows where the repository IS; the old namespaced form can only
// redirect if the forge remembers where it WAS. This table is that
// memory, and placeRepo is the one writer of a repository's namespace,
// so every move records its predecessor whichever door it came through.
package oltp
import (
"database/sql"
"errors"
"fmt"
"strings"
"time"
)
const repoPlaceSchema = `
CREATE TABLE IF NOT EXISTS repo_place_past (
repo TEXT NOT NULL,
namespace TEXT NOT NULL, -- a namespace the repository once appeared under
until TEXT NOT NULL, -- when it left
PRIMARY KEY (repo, namespace)
);`
// execer is what placeRepo needs of a *sql.Tx or a *sql.DB.
type execer interface {
Exec(query string, args ...any) (sql.Result, error)
QueryRow(query string, args ...any) *sql.Row
}
// placeRepo moves a repository to ns, remembering the namespace it
// leaves. The namespace it arrives in stops being a past one, so a
// repository that comes home resolves there as current and not as a
// redirect to itself. An empty former namespace (a store nobody held)
// is nothing to remember: it had no URL of its own.
func placeRepo(x execer, repo, ns, by string, at time.Time) error {
if shown, clash := shownClash(x, repo, ns); clash {
return addressRefusal(x, repo, shown)
}
var cur string
if err := x.QueryRow(`SELECT namespace FROM repo WHERE name = ?`, repo).Scan(&cur); err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
when := at.UTC().Format(time.RFC3339)
if cur != "" && cur != ns {
if _, err := x.Exec(`INSERT OR REPLACE INTO repo_place_past (repo, namespace, until) VALUES (?, ?, ?)`, repo, cur, when); err != nil {
return err
}
}
if _, err := x.Exec(`DELETE FROM repo_place_past WHERE repo = ? AND namespace = ?`, repo, ns); err != nil {
return err
}
_, err := x.Exec(`UPDATE repo SET namespace = ?, namespace_by = ?, namespace_at = ? WHERE name = ?`, ns, by, when, repo)
if err != nil && strings.Contains(err.Error(), "UNIQUE") {
return addressRefusal(x, repo, ns)
}
return err
}
// privateTeamRepos is every repository a private team holds: these
// appear under their owner, not the team (placeOf).
const privateTeamRepos = `(SELECT tr.repo FROM team_repo tr JOIN team t ON t.name = tr.team WHERE t.private = 1)`
// shownClash reports the address readers would see repo at once it is
// placed under ns, and whether another repository already shows there
// (RFC 0025). A private team's repositories show under their owner, so
// a repository placed in one shows there, and the owner's own and their
// other private teams' repositories are what can be beside it.
func shownClash(x execer, repo, ns string) (string, bool) {
shown := ns
var private int
_ = x.QueryRow(`SELECT private FROM team WHERE name = ?`, ns).Scan(&private)
if private == 1 {
_ = x.QueryRow(`SELECT owner FROM repo WHERE name = ?`, repo).Scan(&shown)
}
if shown == "" {
return "", false
}
var one int
err := x.QueryRow(`SELECT 1 FROM repo o, repo r WHERE r.name = ? AND o.handle = r.handle AND o.name <> r.name
AND ((o.namespace = ? AND o.name NOT IN `+privateTeamRepos+`)
OR (o.owner = ? AND o.name IN `+privateTeamRepos+`))`, repo, shown, shown).Scan(&one)
return shown, err == nil
}
// addressRefusal says that ns already has a repository called what repo
// is called (RFC 0025: one repository per address).
func addressRefusal(x execer, repo, ns string) error {
var handle string
_ = x.QueryRow(`SELECT handle FROM repo WHERE name = ?`, repo).Scan(&handle)
return Refusal{fmt.Sprintf("%s already has a repository called %s", ns, handle)}
}
// WasPlacedAt reports whether the repository once appeared under ns and
// no longer does — the question a saved link asks.
func (d *DB) WasPlacedAt(repo, ns string) bool {
if repo == "" || ns == "" {
return false
}
var one int
return d.db.QueryRow(`SELECT 1 FROM repo_place_past WHERE repo = ? AND namespace = ?`, repo, ns).Scan(&one) == nil
}