Artifact 2307bb421b5894db9c73b43637824c4c2748e3658979852e1755a7e6caf44d0f:
- File
cmd/quarry/repocmd.go
— part of check-in
[4a3165db34]
at
2026-08-18 11:17:05
on branch trunk
— repo.list: filter, sort and page the listing
The front page rendered every repository unsorted and unbounded — the same shape that made the timeline unusable, and it is at 23 now. Adds a name filter, sort by name, recent activity or size, and a limit, with Total alongside Count so a page can say '5 of 23'.
Sorting happens BEFORE the limit. The other order gives an arbitrary subset in the right sequence, which reads as correct and is not.
Activity comes from the event table's index — one walk stopping at the first row — so the column that makes a forge listing worth reading costs nothing.
Adds repo.get, so the settings page and the CLI stop scanning the whole listing to read one repository's settings. (user: dmestas, size: 10090) [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 [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") _ = 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, } 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 "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", 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() }