Files
2026-06-08 13:34:53 -05:00

404 lines
11 KiB
Go

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 <create|publish|view|branches|tags|set|archive|unarchive|delete|search|fork|collaborators|collaborator>")
}
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 <add|remove>")
}
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])
}
}