package cli import ( "context" "fmt" "io" "os" "strconv" "strings" "git.knownelement.com/ukrrs/mopac-gitea-go/gitea" ) // --- repo ------------------------------------------------------------------- func cmdRepo(args []string, stdout, stderr io.Writer) int { if len(args) == 0 { return usageErr(stderr, "repo: want create|list|show") } sub, rest := args[0], args[1:] switch sub { case "create": return repoCreate(rest, stdout, stderr) case "list": return repoList(rest, stdout, stderr) case "show": return repoShow(rest, stdout, stderr) default: return usageErr(stderr, "repo: unknown subcommand %q (create|list|show)", sub) } } func repoCreate(args []string, stdout, stderr io.Writer) int { fs, cfgPath, out := newFlags("repo create", stderr) owner := fs.String("owner", "", "create under this organization (default: your account)") name := fs.String("n", "", "repository name (required)") desc := fs.String("desc", "", "description") priv := fs.Bool("private", false, "make the repository private") autoInit := fs.Bool("auto-init", false, "initialize with a README commit") defBranch := fs.String("default-branch", "", "default branch (with --auto-init)") pos, err := parseArgs(fs, args) if err != nil { return usageErr(stderr, "repo create: %v", err) } if len(pos) > 0 { return usageErr(stderr, "repo create: unexpected argument %q", pos[0]) } if *name == "" { return usageErr(stderr, "repo create: -n NAME is required") } c, err := client(*cfgPath) if err != nil { return usageErr(stderr, "%v", err) } repo, err := c.CreateRepo(context.Background(), gitea.RepoParams{ Owner: *owner, Name: *name, Description: *desc, Private: *priv, AutoInit: *autoInit, DefaultBranch: *defBranch, }) if err != nil { return apiErr(stderr, err) } if *out == "json" { emitJSON(stdout, repo) return 0 } vis := "public" if repo.Private { vis = "private" } fmt.Fprintf(stdout, "created repo %s (%s)\n%s\n", repo.FullName, vis, repo.HTMLURL) return 0 } func repoList(args []string, stdout, stderr io.Writer) int { fs, cfgPath, out := newFlags("repo list", stderr) owner := fs.String("owner", "", "list this account's repos (default: yours)") pos, err := parseArgs(fs, args) if err != nil { return usageErr(stderr, "repo list: %v", err) } if len(pos) > 0 { return usageErr(stderr, "repo list: unexpected argument %q", pos[0]) } c, err := client(*cfgPath) if err != nil { return usageErr(stderr, "%v", err) } repos, err := c.ListRepos(context.Background(), *owner) if err != nil { return apiErr(stderr, err) } if *out == "json" { emitJSON(stdout, repos) return 0 } for _, r := range repos { vis := "public" if r.Private { vis = "private" } fmt.Fprintf(stdout, "%-40s %-12s %-7s %s\n", r.FullName, r.DefaultBranch, vis, r.Description) } return 0 } func repoShow(args []string, stdout, stderr io.Writer) int { fs, cfgPath, out := newFlags("repo show", stderr) pos, err := parseArgs(fs, args) if err != nil { return usageErr(stderr, "repo show: %v", err) } if len(pos) != 1 { return usageErr(stderr, "repo show: want OWNER/REPO") } owner, name, err := repoArg(pos[0]) if err != nil { return usageErr(stderr, "repo show: %v", err) } c, err := client(*cfgPath) if err != nil { return usageErr(stderr, "%v", err) } repo, err := c.GetRepo(context.Background(), owner, name) if err != nil { return apiErr(stderr, err) } if *out == "json" { emitJSON(stdout, repo) return 0 } vis := "public" if repo.Private { vis = "private" } fmt.Fprintf(stdout, "%s (%s)\n", repo.FullName, vis) if repo.Description != "" { fmt.Fprintf(stdout, "%s\n", repo.Description) } fmt.Fprintf(stdout, "default branch: %s\n", repo.DefaultBranch) fmt.Fprintf(stdout, "%s\n", repo.HTMLURL) return 0 } // --- branch ------------------------------------------------------------------- func cmdBranch(args []string, stdout, stderr io.Writer) int { if len(args) == 0 { return usageErr(stderr, "branch: want list") } if args[0] != "list" { return usageErr(stderr, "branch: unknown subcommand %q (list)", args[0]) } fs, cfgPath, out := newFlags("branch list", stderr) pos, err := parseArgs(fs, args[1:]) if err != nil { return usageErr(stderr, "branch list: %v", err) } if len(pos) != 1 { return usageErr(stderr, "branch list: want OWNER/REPO") } owner, name, err := repoArg(pos[0]) if err != nil { return usageErr(stderr, "branch list: %v", err) } c, err := client(*cfgPath) if err != nil { return usageErr(stderr, "%v", err) } branches, err := c.ListBranches(context.Background(), owner, name) if err != nil { return apiErr(stderr, err) } if *out == "json" { emitJSON(stdout, branches) return 0 } for _, b := range branches { fmt.Fprintf(stdout, "%-30s %s\n", b.Name, shortSha(b.Commit.ID)) } return 0 } // --- status ------------------------------------------------------------------- var statusStates = map[string]bool{ gitea.StatusPending: true, gitea.StatusSuccess: true, gitea.StatusError: true, gitea.StatusFailure: true, } func cmdStatus(args []string, stdout, stderr io.Writer) int { if len(args) == 0 { return usageErr(stderr, "status: want create|list") } sub, rest := args[0], args[1:] switch sub { case "create": return statusCreate(rest, stdout, stderr) case "list": return statusList(rest, stdout, stderr) default: return usageErr(stderr, "status: unknown subcommand %q (create|list)", sub) } } func statusCreate(args []string, stdout, stderr io.Writer) int { fs, cfgPath, out := newFlags("status create", stderr) state := fs.String("state", "", "success|pending|error|failure (required)") ctxName := fs.String("context", "", "check name (e.g. ci/lint)") desc := fs.String("description", "", "free text") target := fs.String("target-url", "", "link to the check run") pos, err := parseArgs(fs, args) if err != nil { return usageErr(stderr, "status create: %v", err) } if len(pos) != 2 { return usageErr(stderr, "status create: want OWNER/REPO REF") } owner, name, err := repoArg(pos[0]) if err != nil { return usageErr(stderr, "status create: %v", err) } ref := pos[1] if !statusStates[*state] { return usageErr(stderr, "status create: --state must be success|pending|error|failure") } c, err := client(*cfgPath) if err != nil { return usageErr(stderr, "%v", err) } st, err := c.CreateCommitStatus(context.Background(), owner, name, ref, gitea.CommitStatusParams{ State: *state, Context: *ctxName, Description: *desc, TargetURL: *target, }) if err != nil { return apiErr(stderr, err) } if *out == "json" { emitJSON(stdout, st) return 0 } fmt.Fprintf(stdout, "reported %s on %s/%s@%s (context %s)\n", st.State, owner, name, shortSha(ref), st.Context) return 0 } func statusList(args []string, stdout, stderr io.Writer) int { fs, cfgPath, out := newFlags("status list", stderr) pos, err := parseArgs(fs, args) if err != nil { return usageErr(stderr, "status list: %v", err) } if len(pos) != 2 { return usageErr(stderr, "status list: want OWNER/REPO REF") } owner, name, err := repoArg(pos[0]) if err != nil { return usageErr(stderr, "status list: %v", err) } c, err := client(*cfgPath) if err != nil { return usageErr(stderr, "%v", err) } comb, err := c.GetCombinedStatus(context.Background(), owner, name, pos[1]) if err != nil { return apiErr(stderr, err) } if *out == "json" { emitJSON(stdout, comb) return 0 } fmt.Fprintf(stdout, "combined %s (%d checks) on %s\n", comb.State, comb.TotalCount, shortSha(comb.SHA)) for _, s := range comb.Statuses { fmt.Fprintf(stdout, "%-8s %-20s %s\n", s.State, s.Context, s.Description) } return 0 } // --- pull requests -------------------------------------------------------------- var mergeStyles = map[string]bool{ gitea.MergeMerge: true, gitea.MergeRebase: true, gitea.MergeRebaseMerge: true, gitea.MergeSquash: true, gitea.MergeFastForward: true, } func cmdPR(args []string, stdout, stderr io.Writer) int { if len(args) == 0 { return usageErr(stderr, "pr: want create|list|show|merge") } sub, rest := args[0], args[1:] switch sub { case "create": return prCreate(rest, stdout, stderr) case "list": return prList(rest, stdout, stderr) case "show": return prShow(rest, stdout, stderr) case "merge": return prMerge(rest, stdout, stderr) default: return usageErr(stderr, "pr: unknown subcommand %q (create|list|show|merge)", sub) } } // readBody loads a PR body from a file, or stdin when path is "-". func readBody(path string) (string, error) { if path == "" { return "", nil } var b []byte var err error if path == "-" { b, err = io.ReadAll(os.Stdin) } else { b, err = os.ReadFile(path) } if err != nil { return "", fmt.Errorf("cannot read body: %w", err) } return string(b), nil } func prCreate(args []string, stdout, stderr io.Writer) int { fs, cfgPath, out := newFlags("pr create", stderr) title := fs.String("title", "", "PR title (required)") body := fs.String("body", "", "PR body from FILE or - for stdin") base := fs.String("base", "", "target branch (required)") head := fs.String("head", "", "source branch, owner:branch for forks (required)") pos, err := parseArgs(fs, args) if err != nil { return usageErr(stderr, "pr create: %v", err) } if len(pos) != 1 { return usageErr(stderr, "pr create: want OWNER/REPO") } owner, name, err := repoArg(pos[0]) if err != nil { return usageErr(stderr, "pr create: %v", err) } if *title == "" { return usageErr(stderr, "pr create: --title is required") } if *base == "" { return usageErr(stderr, "pr create: --base is required") } if *head == "" { return usageErr(stderr, "pr create: --head is required") } bodyText, err := readBody(*body) if err != nil { return usageErr(stderr, "pr create: %v", err) } c, err := client(*cfgPath) if err != nil { return usageErr(stderr, "%v", err) } pr, err := c.CreatePullRequest(context.Background(), owner, name, gitea.PRParams{ Title: *title, Body: bodyText, Base: *base, Head: *head, }) if err != nil { return apiErr(stderr, err) } if *out == "json" { emitJSON(stdout, pr) return 0 } fmt.Fprintf(stdout, "created pull request #%d: %s\n%s\n", pr.Number, pr.Title, pr.HTMLURL) return 0 } func prList(args []string, stdout, stderr io.Writer) int { fs, cfgPath, out := newFlags("pr list", stderr) state := fs.String("state", gitea.PRStateOpen, "open|closed|all") pos, err := parseArgs(fs, args) if err != nil { return usageErr(stderr, "pr list: %v", err) } if len(pos) != 1 { return usageErr(stderr, "pr list: want OWNER/REPO") } owner, name, err := repoArg(pos[0]) if err != nil { return usageErr(stderr, "pr list: %v", err) } switch *state { case gitea.PRStateOpen, gitea.PRStateClosed, gitea.PRStateAll: default: return usageErr(stderr, "pr list: --state must be open|closed|all") } c, err := client(*cfgPath) if err != nil { return usageErr(stderr, "%v", err) } prs, err := c.ListPullRequests(context.Background(), owner, name, *state) if err != nil { return apiErr(stderr, err) } if *out == "json" { emitJSON(stdout, prs) return 0 } for _, pr := range prs { fmt.Fprintf(stdout, "#%-4d %-7s %s (%s..%s)\n", pr.Number, pr.State, pr.Title, prBranchRef(pr.Base), prBranchRef(pr.Head)) } return 0 } func prBranchRef(b *gitea.PRBranch) string { if b == nil { return "?" } return b.Ref } func prShow(args []string, stdout, stderr io.Writer) int { fs, cfgPath, out := newFlags("pr show", stderr) pos, err := parseArgs(fs, args) if err != nil { return usageErr(stderr, "pr show: %v", err) } if len(pos) != 2 { return usageErr(stderr, "pr show: want OWNER/REPO NUMBER") } owner, name, err := repoArg(pos[0]) if err != nil { return usageErr(stderr, "pr show: %v", err) } num, err := strconv.ParseInt(pos[1], 10, 64) if err != nil { return usageErr(stderr, "pr show: NUMBER must be numeric") } c, err := client(*cfgPath) if err != nil { return usageErr(stderr, "%v", err) } pr, err := c.GetPullRequest(context.Background(), owner, name, num) if err != nil { return apiErr(stderr, err) } if *out == "json" { emitJSON(stdout, pr) return 0 } state := pr.State if pr.Merged { state = "merged" } fmt.Fprintf(stdout, "#%d %s [%s]\n", pr.Number, pr.Title, state) fmt.Fprintf(stdout, "%s..%s\n", prBranchRef(pr.Base), prBranchRef(pr.Head)) fmt.Fprintf(stdout, "mergeable: %v\n", pr.Mergeable) if pr.Body != "" { for _, line := range strings.Split(strings.TrimRight(pr.Body, "\n"), "\n") { fmt.Fprintf(stdout, " %s\n", line) } } fmt.Fprintf(stdout, "%s\n", pr.HTMLURL) return 0 } func prMerge(args []string, stdout, stderr io.Writer) int { fs, cfgPath, _ := newFlags("pr merge", stderr) style := fs.String("do", gitea.MergeMerge, "merge|rebase|rebase-merge|squash|fast-forward") pos, err := parseArgs(fs, args) if err != nil { return usageErr(stderr, "pr merge: %v", err) } if len(pos) != 2 { return usageErr(stderr, "pr merge: want OWNER/REPO NUMBER") } owner, name, err := repoArg(pos[0]) if err != nil { return usageErr(stderr, "pr merge: %v", err) } num, err := strconv.ParseInt(pos[1], 10, 64) if err != nil { return usageErr(stderr, "pr merge: NUMBER must be numeric") } if !mergeStyles[*style] { return usageErr(stderr, "pr merge: --do must be merge|rebase|rebase-merge|squash|fast-forward") } c, err := client(*cfgPath) if err != nil { return usageErr(stderr, "%v", err) } if err := c.MergePullRequest(context.Background(), owner, name, num, gitea.MergeParams{Do: *style}); err != nil { return apiErr(stderr, err) } fmt.Fprintf(stdout, "merged pull request #%d (%s)\n", num, *style) return 0 }