502 lines
14 KiB
Go
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
|
|
}
|