Artifact 4248ed0f6ef8b83c22333d902e5abd36475a00cd33eda28d48e955b26bafaff8:
- File
internal/oltp/repo.go
— part of check-in
[2fd88bc87a]
at
2026-08-21 19:17:01
on branch quarry-forum
— forum votes: OLTP vote store, forum.vote op, arrows + derived scores
Score is always derived from vote rows (no counter); votes are forge-local 0001 §8 state, never synced. Umbrella plan: ticket aa351dec. (user: dmestas, size: 8560) [annotate] [blame] [check-ins using]
// 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 description TEXT NOT NULL DEFAULT '', -- 'public' or 'private'. Public by DEFAULT and on migration: an -- upgrade must never silently hide what was already being served, -- and taking a repository private is the deliberate act. visibility TEXT NOT NULL DEFAULT 'public', homepage TEXT NOT NULL DEFAULT '', topics TEXT NOT NULL DEFAULT '', -- space-separated created TEXT NOT NULL );` // repoMigrations are additive columns for registries written by an // earlier schema. A duplicate-column error means it is already there. var repoMigrations = []string{ `ALTER TABLE repo ADD COLUMN description TEXT NOT NULL DEFAULT ''`, `ALTER TABLE repo ADD COLUMN visibility TEXT NOT NULL DEFAULT 'public'`, `ALTER TABLE repo ADD COLUMN homepage TEXT NOT NULL DEFAULT ''`, `ALTER TABLE repo ADD COLUMN topics TEXT NOT NULL DEFAULT ''`, } // Visibility values. Private means only an identity with a role on the // repository may see it at all — including that it exists. const ( Public = "public" Private = "private" ) // 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 // Visibility is Public or Private. Visibility string // Description is the one line that says what this repository is. // Every forge has it and quarry had nowhere to put it, so the // repository list could only ever show names and counts. Description string // Homepage and Topics are the About-panel settings a maintainer // edits in place, GitHub-style; .quarry/repo.json remains the // in-tree alternative and these override it when set. Homepage string Topics string // space-separated 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, description, visibility 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 } if visibility != Private { visibility = Public } _, err = d.db.Exec(`INSERT INTO repo (name, path, description, visibility, created) VALUES (?, ?, ?, ?, ?)`, name, abs, description, visibility, 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 } // SetRepoAbout updates the About-panel settings: the project homepage // and its space-separated topic list. func (d *DB) SetRepoAbout(name, homepage, topics string) error { _, err := d.db.Exec(`UPDATE repo SET homepage = ?, topics = ? WHERE name = ?`, homepage, topics, name) return err } // SetRepoDescription updates the one-line summary. func (d *DB) SetRepoDescription(name, description string) error { _, err := d.db.Exec(`UPDATE repo SET description = ? WHERE name = ?`, description, name) return err } // SetRepoVisibility takes a repository public or private. // // It REFUSES a repository with no registry row rather than updating // nothing and reporting success. That is not hypothetical: repositories // named by --repo had no row, so taking one private silently did // nothing and told the operator it had worked — a security control that // reports success while doing nothing is worse than one that is absent. func (d *DB) SetRepoVisibility(name, visibility string) error { if visibility != Private { visibility = Public } res, err := d.db.Exec(`UPDATE repo SET visibility = ? WHERE name = ?`, visibility, name) if err != nil { return err } n, err := res.RowsAffected() if err != nil { return err } if n == 0 { return fmt.Errorf("oltp: repository %q is not in the registry", name) } return nil } // EnsureRepo records a repository that is being served but was never // registered — the --repo bootstrap path. Existing rows are left alone, // so a description or visibility set through the API survives a restart // that also names the repository on the command line. func (d *DB) EnsureRepo(name, path string) error { abs, err := filepath.Abs(path) if err != nil { return err } _, err = d.db.Exec(`INSERT INTO repo (name, path, description, visibility, created) VALUES (?, ?, '', 'public', ?) ON CONFLICT (name) DO NOTHING`, name, abs, time.Now().UTC().Format(time.RFC3339)) return err } // PrivateRepos returns the set of private repository names. A SET rather // than a per-name query because the read gate runs on every request and // the answer changes only when an admin changes it. func (d *DB) PrivateRepos() (map[string]bool, error) { rows, err := d.db.Query(`SELECT name FROM repo WHERE visibility = 'private'`) if err != nil { return nil, err } defer rows.Close() out := map[string]bool{} for rows.Next() { var n string if err := rows.Scan(&n); err != nil { return nil, err } out[n] = true } return out, rows.Err() } // PurgeRepo removes a repository's registration and everything keyed to // it: grants, sync users, its mirror, its webhooks and its CI records. // // One transaction and one place, because the failure mode of scattering // this is silent: a deletion that misses a table leaves credentials or // an outbound webhook pointing at a repository that no longer exists, // and if a repository of the same name is later created it inherits // them. When a new repo-keyed table is added, it is added here. func (d *DB) PurgeRepo(name string) error { tx, err := d.db.Begin() if err != nil { return err } defer tx.Rollback() for _, q := range []string{ `DELETE FROM repo_role WHERE repo = ?`, `DELETE FROM sync_user WHERE repo = ?`, `DELETE FROM mirror WHERE repo = ?`, `DELETE FROM webhook WHERE repo = ?`, `DELETE FROM ci_run WHERE repo = ?`, `DELETE FROM ci_watermark WHERE repo = ?`, `DELETE FROM forum_vote WHERE repo = ?`, `DELETE FROM repo WHERE name = ?`, } { if _, err := tx.Exec(q, name); err != nil { return err } } return tx.Commit() } // Repos lists registered repositories, name-ordered. func (d *DB) Repos() ([]Repo, error) { rows, err := d.db.Query(`SELECT name, path, description, visibility, homepage, topics, 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.Description, &r.Visibility, &r.Homepage, &r.Topics, &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, description, visibility, homepage, topics, created FROM repo WHERE name = ?`, name). Scan(&r.Name, &r.Path, &r.Description, &r.Visibility, &r.Homepage, &r.Topics, &r.Created) if errors.Is(err, sql.ErrNoRows) { return Repo{}, false, nil } return r, err == nil, err }