Artifact 0937a8f40a7fb7f589b24c9c991e13e83b7cab90e275b5ca880d28212db21c36:
- File
cmd/quarry/repocmd.go
— part of check-in
[87a492fe4f]
at
2026-08-18 01:59:53
on branch trunk
— repo: a create flow a person can actually use
Nobody picks a filesystem path when they make a repository on a forge, and requiring one made repo.add usable only by the machine's operator. --repos-root gives the server somewhere to put them; path is now optional and the response says where it landed.
Repositories gain a description โ every forge has one and quarry had nowhere to put it, so the listing could only show names and counts.
Three surfaces on one op: quarry repo new|clone|list|remove drives the HTTP API rather than the database, so the CLI has no privileged path and anything it can do a client can; /new is the same three questions a forge always asks; and an empty repository shows how to push to it instead of a bare file table, because it is waiting, not broken.
A rejected form comes back as a form with the reason on it โ an error page throws the reader's input away. (user: dmestas, size: 4900) [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|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") 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 } `json:"repos"` } if err := c.call("GET", "/api/v1/repos", nil, &out); err != nil { return err } for _, r := range out.Repos { fmt.Printf("%-24s %6d %s\n", 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, } 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 "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|remove", verb) } 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() }