Artifact 98d5cc1defd69777fb26c1b73300016c52a58a1adbaad9656fa1b7405e001754:
- File
internal/oltp/repo.go
— part of check-in
[a8d2831bf4]
at
2026-08-18 11:42:29
on branch trunk
— repo.delete: destroy a repository, with two brakes
Removing a repository's disk space meant going to the shell and doing it without quarry knowing, which is worse than quarry doing it. Deletion now purges the store, the derived index, and every forge row keyed to the repository — grants, sync users, mirror, webhooks, CI records — in one transaction, so a new repository of the same name cannot inherit somebody's credentials.
Two brakes. confirm must equal the name: a yes/no prompt is answered reflexively, typing the name requires having read which repository is about to go. And the store is removed ONLY when it lives under this node's repositories root — quarry serves stores it did not create, and this forge runs on a corpus of exactly those, so deleting one named by --repo would destroy a checkout somebody else manages. Verified: the attempt on pikchr refuses and says to unregister instead. (user: dmestas, size: 7674) [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', 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'`, } // 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 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 } // 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 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, 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.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, created FROM repo WHERE name = ?`, name). Scan(&r.Name, &r.Path, &r.Description, &r.Visibility, &r.Created) if errors.Is(err, sql.ErrNoRows) { return Repo{}, false, nil } return r, err == nil, err }