package app import ( "context" "errors" "fmt" "net/http" "net/url" "os" "os/exec" "strings" ) func cliRepo(args []string) error { if len(args) < 1 { return errors.New("usage: gitocean repo ") } switch args[0] { case "create": return cliRepoCreate(args[1:]) case "publish": return cliRepoPublish(args[1:]) case "view": return cliRepoView(args[1:]) case "branches": return cliRepoRefs(args[1:], "branches") case "tags": return cliRepoRefs(args[1:], "tags") case "set": return cliRepoSet(args[1:]) case "archive": return cliRepoArchive(args[1:], true) case "unarchive": return cliRepoArchive(args[1:], false) case "delete": return cliRepoDelete(args[1:]) case "search": return cliRepoSearch(args[1:]) case "fork": return cliRepoFork(args[1:]) case "collaborators": return cliRepoCollaborators(args[1:]) case "collaborator": return cliRepoCollaborator(args[1:]) default: return fmt.Errorf("unknown repo command %q", args[0]) } } func cliRepoCreate(args []string) error { name, visibility, description, jsonOut, err := parseRepoCreateArgs(args) if err != nil { return err } cfg, err := loadConfig() if err != nil { return err } var out Repository if err := apiRequest(context.Background(), cfg.Server, http.MethodPost, "/api/repos", cfg.Token, map[string]string{"name": name, "visibility": visibility, "description": description}, &out); err != nil { return err } if jsonOut { printJSON(out) return nil } printRepoCreated(cfg.Server, out) return nil } func cliRepoPublish(args []string) error { name, visibility, description, remoteName, branch, err := parseRepoPublishArgs(args) if err != nil { return err } if err := runGit("", "rev-parse", "--is-inside-work-tree"); err != nil { return errors.New("repo publish must be run inside a Git repository") } cfg, err := loadConfig() if err != nil { return err } var out Repository if err := apiRequest(context.Background(), cfg.Server, http.MethodPost, "/api/repos", cfg.Token, map[string]string{"name": name, "visibility": visibility, "description": description}, &out); err != nil { return err } remote := repoCloneURL(cfg.Server, out.Owner, out.Name) if err := runGit("", "remote", "get-url", remoteName); err != nil { if err := runGit("", "remote", "add", remoteName, remote); err != nil { return err } } else if err := runGit("", "remote", "set-url", remoteName, remote); err != nil { return err } _ = runGit("", "branch", "-M", branch) cmd := exec.Command("git", "push", "-u", remoteName, branch) cmd.Stdout, cmd.Stderr, cmd.Stdin = os.Stdout, os.Stderr, os.Stdin if err := cmd.Run(); err != nil { return err } if branch != out.DefaultBranch { body := map[string]any{"default_branch": branch} _ = apiRequest(context.Background(), cfg.Server, http.MethodPatch, fmt.Sprintf("/api/repos/%s/%s", out.Owner, out.Name), cfg.Token, body, &out) } fmt.Printf("Published %s/%s to %s\n", out.Owner, out.Name, remote) return nil } func cliRepoView(args []string) error { jsonOut := removeBoolFlag(&args, "--json") if len(args) != 1 { return errors.New("usage: gitocean repo view OWNER/REPO") } owner, repo, err := splitOwnerRepo(args[0]) if err != nil { return err } server, token := envServer(), "" if cfg, err := loadConfig(); err == nil { server, token = cfg.Server, cfg.Token } var out Repository if err := apiRequest(context.Background(), server, http.MethodGet, fmt.Sprintf("/api/repos/%s/%s", owner, repo), token, nil, &out); err != nil { return err } if jsonOut { printJSON(out) return nil } printRepoView(server, out) return nil } func cliRepoRefs(args []string, kind string) error { jsonOut := removeBoolFlag(&args, "--json") if len(args) != 1 { return fmt.Errorf("usage: gitocean repo %s OWNER/REPO", kind) } owner, repo, err := splitOwnerRepo(args[0]) if err != nil { return err } server, token := envServer(), "" if cfg, err := loadConfig(); err == nil { server, token = cfg.Server, cfg.Token } var out []RefInfo if err := apiRequest(context.Background(), server, http.MethodGet, fmt.Sprintf("/api/repos/%s/%s/%s", owner, repo, kind), token, nil, &out); err != nil { return err } if jsonOut { printJSON(out) return nil } printRefs(kind, out) return nil } func cliRepoSet(args []string) error { if len(args) < 1 { return errors.New("usage: gitocean repo set OWNER/REPO [--description TEXT] [--visibility public|private] [--default-branch main]") } repoRef := args[0] body := map[string]any{} for i := 1; i < len(args); i++ { switch args[i] { case "--description": if i+1 >= len(args) { return errors.New("--description requires a value") } i++ body["description"] = args[i] case "--visibility": if i+1 >= len(args) { return errors.New("--visibility requires a value") } i++ body["visibility"] = args[i] case "--default-branch": if i+1 >= len(args) { return errors.New("--default-branch requires a value") } i++ body["default_branch"] = args[i] default: return fmt.Errorf("unknown flag %s", args[i]) } } if len(body) == 0 { return errors.New("no settings provided") } return cliRepoPatch(repoRef, body) } func cliRepoArchive(args []string, archived bool) error { if len(args) != 1 { return errors.New("usage: gitocean repo archive OWNER/REPO") } return cliRepoPatch(args[0], map[string]any{"archived": archived}) } func cliRepoPatch(repoRef string, body map[string]any) error { owner, repo, err := splitOwnerRepo(repoRef) if err != nil { return err } cfg, err := loadConfig() if err != nil { return err } var out Repository if err := apiRequest(context.Background(), cfg.Server, http.MethodPatch, fmt.Sprintf("/api/repos/%s/%s", owner, repo), cfg.Token, body, &out); err != nil { return err } printRepoView(cfg.Server, out) return nil } func cliRepoDelete(args []string) error { var repoRef string force := false for _, arg := range args { switch arg { case "--force", "-force": force = true default: if strings.HasPrefix(arg, "-") { return fmt.Errorf("unknown flag %s", arg) } if repoRef != "" { return errors.New("usage: gitocean repo delete OWNER/REPO [--force]") } repoRef = arg } } if repoRef == "" { return errors.New("usage: gitocean repo delete OWNER/REPO [--force]") } owner, repo, err := splitOwnerRepo(repoRef) if err != nil { return err } cfg, err := loadConfig() if err != nil { return err } path := fmt.Sprintf("/api/repos/%s/%s?force=%v", owner, repo, force) if err := apiRequest(context.Background(), cfg.Server, http.MethodDelete, path, cfg.Token, nil, nil); err != nil { return err } fmt.Printf("Deleted %s/%s\n", owner, repo) return nil } func cliRepoSearch(args []string) error { jsonOut := removeBoolFlag(&args, "--json") var query string scope := "all" for _, arg := range args { switch arg { case "--mine", "-mine": scope = "mine" case "--all", "-all": scope = "all" default: if strings.HasPrefix(arg, "-") { return fmt.Errorf("unknown flag %s", arg) } if query != "" { return errors.New("usage: gitocean repo search QUERY [--all|--mine]") } query = arg } } if query == "" { return errors.New("usage: gitocean repo search QUERY [--all|--mine]") } server := envServer() token := "" if cfg, err := loadConfig(); err == nil { server, token = cfg.Server, cfg.Token } path := "/api/repos/search?q=" + url.QueryEscape(query) + "&scope=" + scope var out []Repository if err := apiRequest(context.Background(), server, http.MethodGet, path, token, nil, &out); err != nil { return err } if jsonOut { printJSON(out) return nil } printRepoTable(out) return nil } func cliRepoFork(args []string) error { var repoRef string newName := "" for i := 0; i < len(args); i++ { arg := args[i] switch { case arg == "--name" || arg == "-name": if i+1 >= len(args) { return errors.New("--name requires a value") } i++ newName = args[i] case strings.HasPrefix(arg, "--name="): newName = strings.TrimPrefix(arg, "--name=") case strings.HasPrefix(arg, "-name="): newName = strings.TrimPrefix(arg, "-name=") default: if strings.HasPrefix(arg, "-") { return fmt.Errorf("unknown flag %s", arg) } if repoRef != "" { return errors.New("usage: gitocean repo fork OWNER/REPO [--name NEW_NAME]") } repoRef = arg } } if repoRef == "" { return errors.New("usage: gitocean repo fork OWNER/REPO [--name NEW_NAME]") } owner, repo, err := splitOwnerRepo(repoRef) if err != nil { return err } cfg, err := loadConfig() if err != nil { return err } var out Repository if err := apiRequest(context.Background(), cfg.Server, http.MethodPost, fmt.Sprintf("/api/repos/%s/%s/fork", owner, repo), cfg.Token, map[string]string{"name": newName}, &out); err != nil { return err } fmt.Printf("Forked %s/%s to %s/%s\n", owner, repo, out.Owner, out.Name) fmt.Printf("Clone: git clone %s\n", repoCloneURL(cfg.Server, out.Owner, out.Name)) return nil } func cliRepoCollaborators(args []string) error { if len(args) != 1 { return errors.New("usage: gitocean repo collaborators OWNER/REPO") } owner, repo, err := splitOwnerRepo(args[0]) if err != nil { return err } cfg, err := loadConfig() if err != nil { return err } var out []Collaborator if err := apiRequest(context.Background(), cfg.Server, http.MethodGet, fmt.Sprintf("/api/repos/%s/%s/collaborators", owner, repo), cfg.Token, nil, &out); err != nil { return err } for _, c := range out { fmt.Printf("%-20s %s\n", c.Username, c.Role) } return nil } func cliRepoCollaborator(args []string) error { if len(args) < 1 { return errors.New("usage: gitocean repo collaborator ") } switch args[0] { case "add": if len(args) != 5 || args[3] != "--role" { return errors.New("usage: gitocean repo collaborator add OWNER/REPO USER --role read|write") } owner, repo, err := splitOwnerRepo(args[1]) if err != nil { return err } cfg, err := loadConfig() if err != nil { return err } var out map[string]string if err := apiRequest(context.Background(), cfg.Server, http.MethodPost, fmt.Sprintf("/api/repos/%s/%s/collaborators", owner, repo), cfg.Token, map[string]string{"username": args[2], "role": args[4]}, &out); err != nil { return err } fmt.Printf("Added %s as %s\n", out["username"], out["role"]) return nil case "remove": if len(args) != 3 { return errors.New("usage: gitocean repo collaborator remove OWNER/REPO USER") } owner, repo, err := splitOwnerRepo(args[1]) if err != nil { return err } cfg, err := loadConfig() if err != nil { return err } return apiRequest(context.Background(), cfg.Server, http.MethodDelete, fmt.Sprintf("/api/repos/%s/%s/collaborators/%s", owner, repo, strings.ToLower(args[2])), cfg.Token, nil, nil) default: return fmt.Errorf("unknown collaborator command %q", args[0]) } }