Artifact 41531117dc52dbcafaf67cc1f7ce2c8013b152a3b20436ac11d194ab0c0e46bb:
- File
internal/oltp/repo.go
— part of check-in
[2c157378a3]
at
2026-08-18 01:24:41
on branch trunk
— repo: add and remove repositories without restarting
Adding a repository meant editing the server's command line and restarting, which took every other repository down with it. The registry now lives in OLTP where 0001 §8 always placed it, and repo.add / repo.remove (admin) register and tear down at runtime.
registrar owns a repository's whole lifetime — index handle, pipeline, sync backend, poll goroutine. The startup loop could not: it relied on defer ix.Close() reaching serve's return and on a poll loop that never stops, and both leak the moment a repository is removed.
Add persists before it serves, and rolls the record back if opening fails: a registration that serves without being recorded vanishes at the next restart with no trace of why. Remove stops serving and forgets, but leaves the store and index on disk — 'stop serving' and 'destroy' are different intentions and only one is reversible.
--repo stays as bootstrap and override. (user: dmestas, size: 3126) [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 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 }