Artifact a95a1c7587ca0c28809644d6f3f4a4f7788ec588b5beadf4bd0e85458984499a:
- File
cmd/quarry/repocmd.go
— part of check-in
[a41b410958]
at
2026-08-18 11:06:22
on branch trunk
— repo: settings page, descriptions everywhere
repo.update replaces repo.visibility and applies a repository's settings whole โ a settings form submits every field it manages, so partial-update semantics would only add a way for the two to disagree about what absent means. The CLI reads current settings before writing, so 'repo private' no longer blanks the description.
A settings page at /r/{repo}/settings, admin-gated, and the description now appears on the repository itself rather than only in the listing: a reader arriving from a link otherwise sees nav and a file tree and no statement of what the project is. (user: dmestas, size: 10219) [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 out struct { Repos []struct { Name, Description string Private bool } `json:"repos"` } if err := c.call("GET", "/api/v1/repos", nil, &out); err != nil { return nil, err } for _, r := range out.Repos { if r.Name == name { return map[string]any{"name": r.Name, "description": r.Description, "private": r.Private}, nil } } return nil, fmt.Errorf("no repository %q", name) } 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() }