Files

502 lines
14 KiB
Go

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
}