Artifact 6610f4ced00082a7294f54831f3e78ede651f9547e1fa4358c5adb7b11aa3aae:
- File
cmd/quarry/repocmd.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: 10757) [annotate] [blame] [check-ins using]
package main import ( "encoding/json" "flag" "fmt" "net/http" "net/url" "os" "strings" "time" ) // repoCmd is the client-side verb for managing repositories. Unlike // `account` and `user`, which open the forge database directly, this // talks to a RUNNING server over the API — because registering a // repository is not a config edit, it is a live operation: the server // opens an index, starts a pipeline and begins serving. Two processes // writing that registry would disagree about which repositories exist. // // It is therefore also the first proof that the API is sufficient: the // CLI has no privileged path, so anything it can do, any client can. func repoCmd(args []string) error { if len(args) < 1 { return fmt.Errorf("usage: quarry repo new|clone|list|describe|private|public|remove|delete [flags]") } verb, rest := args[0], args[1:] fs := flag.NewFlagSet("repo "+verb, flag.ContinueOnError) server := fs.String("server", envOr("QUARRY_SERVER", "http://127.0.0.1:8080"), "quarry base URL") user := fs.String("user", envOr("QUARRY_USER", ""), "admin account") password := fs.String("password", "", "password (or QUARRY_PASSWORD)") name := fs.String("name", "", "repository name") path := fs.String("path", "", "store directory (default: the server's repositories root)") desc := fs.String("description", "", "one-line summary") from := fs.String("from", "", "clone this URL") every := fs.Int("every", 0, "mirror interval in seconds (clone only; 0 = server default)") noMirror := fs.Bool("no-mirror", false, "do not record a mirror when cloning") private := fs.Bool("private", false, "only accounts with a role may see it") readme := fs.Bool("readme", false, "seed a README and make the first check-in") license := fs.String("license", "", "seed a license: mit or bsd-2") ignore := fs.Bool("ignore", false, "seed a starter .zitignore") confirm := fs.String("confirm", "", "for delete: type the repository's name") _ = fs.Bool("public", false, "make an existing repository public again") // verb form is used if err := fs.Parse(rest); err != nil { return err } if r := fs.Args(); len(r) > 0 { return fmt.Errorf("unexpected argument %q", r[0]) } pw := *password if pw == "" { pw = os.Getenv("QUARRY_PASSWORD") } c := &apiClient{base: strings.TrimRight(*server, "/"), user: *user, pass: pw} switch verb { case "list": var out struct { Repos []struct { Name, Description string Checkins int Private bool } `json:"repos"` } if err := c.call("GET", "/api/v1/repos", nil, &out); err != nil { return err } for _, r := range out.Repos { mark := " " if r.Private { mark = "*" } fmt.Printf("%s%-23s %6d %s\n", mark, r.Name, r.Checkins, r.Description) } return nil case "new", "clone": if *name == "" { return fmt.Errorf("repo %s wants --name", verb) } if verb == "clone" && *from == "" { return fmt.Errorf("repo clone wants --from URL") } body := map[string]any{ "name": *name, "path": *path, "description": *desc, "create": verb == "new", "clone": *from, "noMirror": *noMirror, "mirrorEvery": *every, "private": *private, "readme": *readme, "license": *license, "ignore": *ignore, } var out struct { Name, Path string Mirrored bool } if err := c.call("POST", "/api/v1/repos", body, &out); err != nil { return err } fmt.Printf("%s created at %s\n", out.Name, out.Path) if out.Mirrored { fmt.Printf("mirroring %s\n", *from) } fmt.Printf("\n zit clone %s/r/%s %s\n\n", c.base, out.Name, out.Name) return nil case "private", "public": if *name == "" { return fmt.Errorf("repo %s wants --name", verb) } // Read the current settings first: update applies fields whole, // so sending only visibility would blank the description. cur, err := c.repoSettings(*name) if err != nil { return err } cur["private"] = verb == "private" if err := c.call("POST", "/api/v1/repos/update", cur, nil); err != nil { return err } fmt.Printf("%s is now %s\n", *name, verb) return nil case "describe": if *name == "" { return fmt.Errorf("repo describe wants --name") } cur, err := c.repoSettings(*name) if err != nil { return err } cur["description"] = *desc if err := c.call("POST", "/api/v1/repos/update", cur, nil); err != nil { return err } fmt.Printf("%s: %s\n", *name, *desc) return nil case "delete": if *name == "" { return fmt.Errorf("repo delete wants --name and --confirm") } if err := c.call("POST", "/api/v1/repos/delete", map[string]any{"name": *name, "confirm": *confirm}, nil); err != nil { return err } fmt.Printf("%s destroyed\n", *name) return nil case "remove": if *name == "" { return fmt.Errorf("repo remove wants --name") } if err := c.call("POST", "/api/v1/repos/remove", map[string]any{"name": *name}, nil); err != nil { return err } fmt.Printf("%s is no longer served (its store on disk is untouched)\n", *name) return nil } return fmt.Errorf("unknown verb %q: want new|clone|list|describe|private|public|remove|delete", verb) } // accountAPICmd manages accounts through the API rather than the // database. `quarry account` still opens OLTP directly — it has to, so // the FIRST account can be created before any server is running or any // admin exists to authenticate as. Past that, the API is the surface, // and driving it from here is what keeps it honest. func accountAPICmd(args []string) error { if len(args) < 1 { return fmt.Errorf("usage: quarry accounts list|add|passwd|grant|revoke|delete [flags]") } verb, rest := args[0], args[1:] fs := flag.NewFlagSet("accounts "+verb, flag.ContinueOnError) server := fs.String("server", envOr("QUARRY_SERVER", "http://127.0.0.1:8080"), "quarry base URL") as := fs.String("as", envOr("QUARRY_USER", ""), "admin account to act as") asPassword := fs.String("as-password", "", "that account's password (or QUARRY_PASSWORD)") name := fs.String("user", "", "the account to operate on") password := fs.String("password", "", "new password (or QUARRY_NEW_PASSWORD)") admin := fs.Bool("admin", false, "make a site admin") repo := fs.String("repo", "", "repository, for grant and revoke") role := fs.String("role", "read", "read|write|admin") if err := fs.Parse(rest); err != nil { return err } if r := fs.Args(); len(r) > 0 { return fmt.Errorf("unexpected argument %q", r[0]) } c := &apiClient{base: strings.TrimRight(*server, "/"), user: *as, pass: firstNonEmpty(*asPassword, os.Getenv("QUARRY_PASSWORD"))} switch verb { case "list": var out struct { Accounts []struct { Name string Admin bool Created string Grants []struct{ Repo, Role string } } } if err := c.call("GET", "/api/v1/accounts", nil, &out); err != nil { return err } for _, a := range out.Accounts { kind := "" if a.Admin { kind = " (site admin)" } fmt.Printf("%-24s%s\n", a.Name, kind) for _, g := range a.Grants { fmt.Printf(" %-20s %s\n", g.Repo, g.Role) } } return nil case "add", "passwd": if *name == "" { return fmt.Errorf("accounts %s wants --user", verb) } pw := firstNonEmpty(*password, os.Getenv("QUARRY_NEW_PASSWORD")) if pw == "" { return fmt.Errorf("no password: use --password or QUARRY_NEW_PASSWORD") } path, body := "/api/v1/accounts", map[string]any{"name": *name, "password": pw, "admin": *admin} if verb == "passwd" { path = "/api/v1/accounts/passwd" body = map[string]any{"name": *name, "password": pw} } if err := c.call("POST", path, body, nil); err != nil { return err } fmt.Printf("%s: password set\n", *name) return nil case "grant", "revoke": if *name == "" || *repo == "" { return fmt.Errorf("accounts %s wants --user and --repo", verb) } body := map[string]any{"name": *name, "repo": *repo} if verb == "grant" { body["role"] = *role } if err := c.call("POST", "/api/v1/accounts/"+verb, body, nil); err != nil { return err } if verb == "grant" { fmt.Printf("%s: %s on %s\n", *name, *role, *repo) } else { fmt.Printf("%s: no access to %s\n", *name, *repo) } return nil case "delete": if *name == "" { return fmt.Errorf("accounts delete wants --user") } if err := c.call("POST", "/api/v1/accounts/delete", map[string]any{"name": *name}, nil); err != nil { return err } fmt.Printf("%s deleted\n", *name) return nil } return fmt.Errorf("unknown verb %q: want list|add|passwd|grant|revoke|delete", verb) } // repoSettings reads a repository's current settings as an update body, // so a caller changing one field does not silently blank the others. func (c *apiClient) repoSettings(name string) (map[string]any, error) { var info struct { Name, Description string Private bool } if err := c.call("GET", "/api/v1/r/"+name+"/info", nil, &info); err != nil { return nil, err } return map[string]any{"name": info.Name, "description": info.Description, "private": info.Private}, nil } func firstNonEmpty(vals ...string) string { for _, v := range vals { if v != "" { return v } } return "" } func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // apiClient is the smallest thing that can drive quarry's API with an // admin credential. Basic auth because that is what the API already // takes (0001 §7: one credential per user). type apiClient struct { base, user, pass string } func (c *apiClient) call(method, path string, body any, out any) error { var rdr *strings.Reader if body != nil { raw, err := json.Marshal(body) if err != nil { return err } rdr = strings.NewReader(string(raw)) } var req *http.Request var err error if rdr != nil { req, err = http.NewRequest(method, c.base+path, rdr) } else { req, err = http.NewRequest(method, c.base+path, nil) } if err != nil { return err } req.Header.Set("Content-Type", "application/json") if c.user != "" { req.SetBasicAuth(c.user, c.pass) } // Generous: a clone of a large repository is minutes, and the server // holds the request open for it. resp, err := (&http.Client{Timeout: 30 * time.Minute}).Do(req) if err != nil { return fmt.Errorf("%s: %w", urlOf(c.base, path), err) } defer resp.Body.Close() if resp.StatusCode >= 400 { var e struct { Error string `json:"error"` } json.NewDecoder(resp.Body).Decode(&e) if e.Error == "" { e.Error = resp.Status } return fmt.Errorf("%s", e.Error) } if out == nil { return nil } return json.NewDecoder(resp.Body).Decode(out) } func urlOf(base, path string) string { u, err := url.Parse(base + path) if err != nil { return base + path } return u.String() }