feat(cli): mgit CLI over the library (flags accepted after positionals)
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
// Command mgit is the Gitea CLI over the mopac-gitea-go library.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.knownelement.com/ukrrs/mopac-gitea-go/internal/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
os.Exit(cli.Run(os.Args[1:], os.Stdout, os.Stderr))
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
// Package cli implements the mgit command line: a thin, machine-friendly
|
||||||
|
// front end over the gitea library. Connection settings come from
|
||||||
|
// GITEA_URL / GITEA_KEY env vars or a 0600 --config env file — the token
|
||||||
|
// is never a flag value and never logged. Every command accepts -o json
|
||||||
|
// for machine output. Exit codes: 0 ok, 1 usage/config, 2 API error
|
||||||
|
// (single-line stderr carrying the http status, parseable).
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.knownelement.com/ukrrs/mopac-gitea-go/gitea"
|
||||||
|
"git.knownelement.com/ukrrs/mopac-gitea-go/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
const usage = `mgit: Gitea CLI (stdlib-only, library-backed)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
mgit repo create -n NAME [--owner ORG] [--desc TEXT] [--private]
|
||||||
|
[--auto-init] [--default-branch B] [-o json]
|
||||||
|
mgit repo list [--owner OWNER] [-o json]
|
||||||
|
mgit repo show OWNER/REPO [-o json]
|
||||||
|
mgit branch list OWNER/REPO [-o json]
|
||||||
|
mgit status create OWNER/REPO REF --state success|pending|error|failure
|
||||||
|
[--context NAME] [--description TEXT] [--target-url URL] [-o json]
|
||||||
|
mgit status list OWNER/REPO REF [-o json] (combined + per-check)
|
||||||
|
mgit pr create OWNER/REPO --title TITLE [--body FILE|-] --base B --head H [-o json]
|
||||||
|
mgit pr list OWNER/REPO [--state open|closed|all] [-o json]
|
||||||
|
mgit pr show OWNER/REPO NUMBER [-o json]
|
||||||
|
mgit pr merge OWNER/REPO NUMBER [--do merge|rebase|rebase-merge|squash|fast-forward]
|
||||||
|
mgit help
|
||||||
|
|
||||||
|
Connection: GITEA_URL + GITEA_KEY env vars, or --config PATH pointing at a
|
||||||
|
0600 env file with the same keys. The token never appears in flags,
|
||||||
|
logs, or error output.
|
||||||
|
|
||||||
|
REF accepts anything the server resolves (full or short sha, branch,
|
||||||
|
tag). PR head may be "branch" or "owner:branch" (fork syntax). Flags
|
||||||
|
parse before OR after positionals.
|
||||||
|
|
||||||
|
Exit codes: 0 ok, 1 usage/config error, 2 API error (stderr: one line,
|
||||||
|
"http NNN" included).`
|
||||||
|
|
||||||
|
// Run executes one command; it returns the process exit code.
|
||||||
|
func Run(args []string, stdout, stderr io.Writer) int {
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Fprint(stderr, usage)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
// Hoist a leading global --config PATH onto the subcommand (the
|
||||||
|
// subcommand flag sets already accept it anywhere).
|
||||||
|
var hoisted []string
|
||||||
|
for len(args) >= 2 && (args[0] == "--config" || args[0] == "-config") {
|
||||||
|
hoisted = append(hoisted, args[0], args[1])
|
||||||
|
args = args[2:]
|
||||||
|
}
|
||||||
|
if len(hoisted) > 0 {
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Fprint(stderr, usage)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
args = append(args, hoisted...)
|
||||||
|
}
|
||||||
|
switch args[0] {
|
||||||
|
case "help", "-h", "--help":
|
||||||
|
fmt.Fprint(stdout, usage)
|
||||||
|
return 0
|
||||||
|
case "repo":
|
||||||
|
return cmdRepo(args[1:], stdout, stderr)
|
||||||
|
case "branch":
|
||||||
|
return cmdBranch(args[1:], stdout, stderr)
|
||||||
|
case "status":
|
||||||
|
return cmdStatus(args[1:], stdout, stderr)
|
||||||
|
case "pr":
|
||||||
|
return cmdPR(args[1:], stdout, stderr)
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(stderr, "mgit: unknown command %q\n\n%s\n", args[0], usage)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newFlags builds a quiet flag set with the shared --config and -o flags.
|
||||||
|
func newFlags(name string, stderr io.Writer) (*flag.FlagSet, *string, *string) {
|
||||||
|
fs := flag.NewFlagSet(name, flag.ContinueOnError)
|
||||||
|
fs.SetOutput(io.Discard)
|
||||||
|
cfg := fs.String("config", "", "env file with GITEA_URL/GITEA_KEY (must be 0600)")
|
||||||
|
out := fs.String("o", "text", "output format: text|json")
|
||||||
|
return fs, cfg, out
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseArgs parses flags that may appear AFTER positionals (the standard
|
||||||
|
// flag package stops at the first positional; mgit's documented surface
|
||||||
|
// is "pr merge OWNER/REPO NUMBER --do squash").
|
||||||
|
func parseArgs(fs *flag.FlagSet, args []string) ([]string, error) {
|
||||||
|
var positionals []string
|
||||||
|
rest := args
|
||||||
|
for {
|
||||||
|
if err := fs.Parse(rest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
got := fs.Args()
|
||||||
|
i := 0
|
||||||
|
for i < len(got) && (got[i] == "-" || !strings.HasPrefix(got[i], "-")) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
positionals = append(positionals, got[:i]...)
|
||||||
|
if i == len(got) {
|
||||||
|
return positionals, nil
|
||||||
|
}
|
||||||
|
rest = got[i:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// client resolves config (env + optional 0600 file) and builds the
|
||||||
|
// client. A nil client and non-nil error means exit 1; API failures
|
||||||
|
// surface later as exit 2.
|
||||||
|
func client(cfgPath string) (*gitea.Client, error) {
|
||||||
|
if cfgPath == "" {
|
||||||
|
cfgPath = os.Getenv("MGIT_CONFIG")
|
||||||
|
}
|
||||||
|
if cfgPath == "" {
|
||||||
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
|
cand := filepath.Join(home, ".config", "mgit", "env")
|
||||||
|
if _, err := os.Stat(cand); err == nil {
|
||||||
|
cfgPath = cand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg, _, err := config.Load(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return gitea.New(gitea.Config{BaseURL: cfg.BaseURL, Token: cfg.Token}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiErr reports an API failure the mgit way: one line, exit 2.
|
||||||
|
func apiErr(stderr io.Writer, err error) int {
|
||||||
|
fmt.Fprintf(stderr, "mgit: %v\n", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// usageErr reports a usage/config failure: one line, exit 1.
|
||||||
|
func usageErr(stderr io.Writer, format string, args ...any) int {
|
||||||
|
fmt.Fprintf(stderr, "mgit: "+format+"\n", args...)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// emitJSON pretty-prints v for -o json.
|
||||||
|
func emitJSON(stdout io.Writer, v any) {
|
||||||
|
b, err := json.MarshalIndent(v, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return // library types are JSON-clean by construction
|
||||||
|
}
|
||||||
|
stdout.Write(b)
|
||||||
|
fmt.Fprintln(stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// repoArg splits "OWNER/REPO" into its parts.
|
||||||
|
func repoArg(arg string) (owner, repo string, err error) {
|
||||||
|
parts := strings.Split(arg, "/")
|
||||||
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||||
|
return "", "", fmt.Errorf("bad repository %q (want OWNER/REPO)", arg)
|
||||||
|
}
|
||||||
|
return parts[0], parts[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shortSha renders a sha for humans (8 chars).
|
||||||
|
func shortSha(sha string) string {
|
||||||
|
if len(sha) > 8 {
|
||||||
|
return sha[:8]
|
||||||
|
}
|
||||||
|
return sha
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.knownelement.com/ukrrs/mopac-gitea-go/internal/fakegitea"
|
||||||
|
)
|
||||||
|
|
||||||
|
const fakeToken = "fake-gitea-token-0123456789"
|
||||||
|
|
||||||
|
// run boots a fake Gitea, points the CLI at it via env, and runs one
|
||||||
|
// command; it returns the exit code plus captured output.
|
||||||
|
func run(t *testing.T, srv *fakegitea.Server, args ...string) (int, string, string) {
|
||||||
|
t.Helper()
|
||||||
|
t.Setenv("GITEA_URL", srv.URL)
|
||||||
|
t.Setenv("GITEA_KEY", fakeToken)
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
code := Run(args, &out, &errb)
|
||||||
|
return code, out.String(), errb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServer(t *testing.T) *fakegitea.Server {
|
||||||
|
t.Helper()
|
||||||
|
srv := fakegitea.New(fakeToken, "ukrrs")
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelpAndUsage(t *testing.T) {
|
||||||
|
srv := newServer(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
code int
|
||||||
|
}{
|
||||||
|
{name: "help", args: []string{"help"}, code: 0},
|
||||||
|
{name: "no args", args: nil, code: 1},
|
||||||
|
{name: "unknown command", args: []string{"frobnicate"}, code: 1},
|
||||||
|
{name: "unknown subcommand", args: []string{"repo", "frob"}, code: 1},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
code, _, errOut := run(t, srv, tt.args...)
|
||||||
|
if code != tt.code {
|
||||||
|
t.Fatalf("code = %d, want %d (stderr: %s)", code, tt.code, errOut)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
code, out, _ := run(t, srv, "help")
|
||||||
|
if code != 0 || !strings.Contains(out, "mgit repo create") || !strings.Contains(out, "GITEA_URL") {
|
||||||
|
t.Fatalf("help output = %q", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoLifecycle(t *testing.T) {
|
||||||
|
srv := newServer(t)
|
||||||
|
|
||||||
|
code, out, errOut := run(t, srv, "repo", "create", "-n", "vertical-x", "--desc", "one vertical",
|
||||||
|
"--private", "--auto-init")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("repo create: code=%d stderr=%s", code, errOut)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "ukrrs/vertical-x") || !strings.Contains(out, "private") {
|
||||||
|
t.Fatalf("out = %q", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, out, errOut = run(t, srv, "repo", "create", "-n", "vertical-y", "-o", "json")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("repo create json: %s", errOut)
|
||||||
|
}
|
||||||
|
var repo struct {
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
Private bool `json:"private"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(out), &repo); err != nil {
|
||||||
|
t.Fatalf("json out: %v (%s)", err, out)
|
||||||
|
}
|
||||||
|
if repo.FullName != "ukrrs/vertical-y" || repo.Private {
|
||||||
|
t.Fatalf("json = %+v", repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, out, errOut = run(t, srv, "repo", "list")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("repo list: %s", errOut)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "ukrrs/vertical-x") || !strings.Contains(out, "ukrrs/vertical-y") {
|
||||||
|
t.Fatalf("list = %q", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, out, errOut = run(t, srv, "repo", "list", "--owner", "other")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("repo list other: %s", errOut)
|
||||||
|
}
|
||||||
|
if strings.Contains(out, "ukrrs/") {
|
||||||
|
t.Fatalf("list other = %q", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, out, errOut = run(t, srv, "repo", "show", "ukrrs/vertical-x")
|
||||||
|
if code != 0 || !strings.Contains(out, "one vertical") {
|
||||||
|
t.Fatalf("repo show: code=%d out=%q err=%s", code, out, errOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate create -> exit 2, one-line stderr with the http code.
|
||||||
|
code, out, errOut = run(t, srv, "repo", "create", "-n", "vertical-x")
|
||||||
|
if code != 2 {
|
||||||
|
t.Fatalf("dup create code = %d, want 2", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(errOut, "http 409") || strings.Count(strings.TrimRight(errOut, "\n"), "\n") != 0 {
|
||||||
|
t.Fatalf("dup stderr = %q", errOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoCreateMissingName(t *testing.T) {
|
||||||
|
srv := newServer(t)
|
||||||
|
code, _, errOut := run(t, srv, "repo", "create")
|
||||||
|
if code != 1 || !strings.Contains(errOut, "-n") {
|
||||||
|
t.Fatalf("code=%d err=%q", code, errOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBranchList(t *testing.T) {
|
||||||
|
srv := newServer(t)
|
||||||
|
srv.AddRepo("ukrrs", "MOPAC", "main", "main", "feat/quota")
|
||||||
|
|
||||||
|
code, out, errOut := run(t, srv, "branch", "list", "ukrrs/MOPAC")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("branch list: %s", errOut)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "main") || !strings.Contains(out, "feat/quota") {
|
||||||
|
t.Fatalf("out = %q", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, out, errOut = run(t, srv, "branch", "list", "ukrrs/MOPAC", "-o", "json")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("branch list json: %s", errOut)
|
||||||
|
}
|
||||||
|
var branches []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(out), &branches); err != nil || len(branches) != 2 {
|
||||||
|
t.Fatalf("json = %s (%v)", out, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if code, _, errOut = run(t, srv, "branch", "list", "ukrrs/absent"); code != 2 || !strings.Contains(errOut, "http 404") {
|
||||||
|
t.Fatalf("missing repo: code=%d err=%q", code, errOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusCommands(t *testing.T) {
|
||||||
|
srv := newServer(t)
|
||||||
|
srv.AddRepo("ukrrs", "MOPAC", "main", "main")
|
||||||
|
sha := srv.AddBranch("ukrrs", "MOPAC", "feat/quota")
|
||||||
|
|
||||||
|
code, out, errOut := run(t, srv, "status", "create", "ukrrs/MOPAC", sha,
|
||||||
|
"--state", "success", "--context", "ci/lint", "--description", "clean")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("status create: %s", errOut)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "success") || !strings.Contains(out, "ci/lint") {
|
||||||
|
t.Fatalf("out = %q", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid state is a usage error before any request.
|
||||||
|
if code, _, errOut = run(t, srv, "status", "create", "ukrrs/MOPAC", sha, "--state", "green"); code != 1 {
|
||||||
|
t.Fatalf("invalid state code = %d err=%q", code, errOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, out, errOut = run(t, srv, "status", "list", "ukrrs/MOPAC", "feat/quota")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("status list: %s", errOut)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "success") || !strings.Contains(out, "ci/lint") {
|
||||||
|
t.Fatalf("out = %q", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPullRequestFlow(t *testing.T) {
|
||||||
|
srv := newServer(t)
|
||||||
|
srv.AddRepo("ukrrs", "MOPAC", "main", "main", "feat/quota")
|
||||||
|
|
||||||
|
bodyFile := filepath.Join(t.TempDir(), "body.md")
|
||||||
|
os.WriteFile(bodyFile, []byte("## Scope\n- quota accounting"), 0o600)
|
||||||
|
|
||||||
|
code, out, errOut := run(t, srv, "pr", "create", "ukrrs/MOPAC",
|
||||||
|
"--title", "Quota accounting", "--body", bodyFile,
|
||||||
|
"--base", "main", "--head", "feat/quota", "-o", "json")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("pr create: %s", errOut)
|
||||||
|
}
|
||||||
|
var pr struct {
|
||||||
|
Number int64 `json:"number"`
|
||||||
|
State string `json:"state"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(out), &pr); err != nil || pr.Number != 1 || pr.State != "open" {
|
||||||
|
t.Fatalf("json = %s (%v)", out, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, out, errOut = run(t, srv, "pr", "list", "ukrrs/MOPAC")
|
||||||
|
if code != 0 || !strings.Contains(out, "Quota accounting") || !strings.Contains(out, "#1") {
|
||||||
|
t.Fatalf("pr list: code=%d out=%q err=%s", code, out, errOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, out, errOut = run(t, srv, "pr", "show", "ukrrs/MOPAC", "1")
|
||||||
|
if code != 0 || !strings.Contains(out, "quota accounting") || !strings.Contains(out, "main..feat/quota") {
|
||||||
|
t.Fatalf("pr show: code=%d out=%q err=%s", code, out, errOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, out, errOut = run(t, srv, "pr", "merge", "ukrrs/MOPAC", "1", "--do", "squash")
|
||||||
|
if code != 0 || !strings.Contains(out, "#1") || !strings.Contains(out, "squash") {
|
||||||
|
t.Fatalf("pr merge: code=%d out=%q err=%s", code, out, errOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merged: the open list is empty and re-merge fails with 409.
|
||||||
|
code, out, _ = run(t, srv, "pr", "list", "ukrrs/MOPAC")
|
||||||
|
if code != 0 || strings.Contains(out, "Quota accounting") {
|
||||||
|
t.Fatalf("pr list after merge = %q", out)
|
||||||
|
}
|
||||||
|
if code, _, errOut = run(t, srv, "pr", "merge", "ukrrs/MOPAC", "1"); code != 2 || !strings.Contains(errOut, "http 409") {
|
||||||
|
t.Fatalf("re-merge: code=%d err=%q", code, errOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPullRequestUsageErrors(t *testing.T) {
|
||||||
|
srv := newServer(t)
|
||||||
|
srv.AddRepo("ukrrs", "MOPAC", "main", "main", "feat/quota")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
}{
|
||||||
|
{"missing title", []string{"pr", "create", "ukrrs/MOPAC", "--base", "main", "--head", "feat/quota"}},
|
||||||
|
{"missing base", []string{"pr", "create", "ukrrs/MOPAC", "--title", "t", "--head", "feat/quota"}},
|
||||||
|
{"missing head", []string{"pr", "create", "ukrrs/MOPAC", "--title", "t", "--base", "main"}},
|
||||||
|
{"bad repo arg", []string{"pr", "create", "MOPAC", "--title", "t", "--base", "main", "--head", "x"}},
|
||||||
|
{"non-numeric index", []string{"pr", "merge", "ukrrs/MOPAC", "abc"}},
|
||||||
|
{"bad merge style", []string{"pr", "merge", "ukrrs/MOPAC", "1", "--do", "blend"}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
code, _, errOut := run(t, srv, tt.args...)
|
||||||
|
if code != 1 {
|
||||||
|
t.Fatalf("code = %d, want usage error 1 (stderr: %s)", code, errOut)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlagsAfterPositionals(t *testing.T) {
|
||||||
|
srv := newServer(t)
|
||||||
|
srv.AddRepo("ukrrs", "MOPAC", "main", "main", "feat/quota")
|
||||||
|
|
||||||
|
code, _, errOut := run(t, srv, "pr", "create", "ukrrs/MOPAC", "--base", "main",
|
||||||
|
"--head", "feat/quota", "--title", "flags after positional")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("pr create: %s", errOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenNeverInOutput(t *testing.T) {
|
||||||
|
srv := newServer(t)
|
||||||
|
srv.AddRepo("ukrrs", "MOPAC", "main", "main")
|
||||||
|
|
||||||
|
t.Setenv("GITEA_URL", srv.URL)
|
||||||
|
t.Setenv("GITEA_KEY", fakeToken)
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
srv.Token = "rotated-" + fakeToken // force a 401 body that echoes the token
|
||||||
|
code := Run([]string{"repo", "list"}, &out, &errb)
|
||||||
|
if code != 2 {
|
||||||
|
t.Fatalf("code = %d, want 2", code)
|
||||||
|
}
|
||||||
|
if strings.Contains(out.String()+errb.String(), fakeToken) {
|
||||||
|
t.Fatalf("token leaked: out=%q err=%q", out.String(), errb.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMissingConfigIsUsageError(t *testing.T) {
|
||||||
|
t.Setenv("GITEA_URL", "")
|
||||||
|
t.Setenv("GITEA_KEY", "")
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
code := Run([]string{"repo", "list", "--config", filepath.Join(t.TempDir(), "absent")}, &out, &errb)
|
||||||
|
if code != 1 || !strings.Contains(errb.String(), "GITEA_URL") {
|
||||||
|
t.Fatalf("code=%d err=%q", code, errb.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLooseConfigRefused(t *testing.T) {
|
||||||
|
srv := newServer(t)
|
||||||
|
path := filepath.Join(t.TempDir(), "loose.env")
|
||||||
|
os.WriteFile(path, []byte("GITEA_URL="+srv.URL+"\nGITEA_KEY="+fakeToken+"\n"), 0o644)
|
||||||
|
|
||||||
|
t.Setenv("GITEA_URL", "")
|
||||||
|
t.Setenv("GITEA_KEY", "")
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
code := Run([]string{"repo", "list", "--config", path}, &out, &errb)
|
||||||
|
if code != 1 || !strings.Contains(errb.String(), "insecure mode") {
|
||||||
|
t.Fatalf("code=%d err=%q", code, errb.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBodyFromStdinDash(t *testing.T) {
|
||||||
|
srv := newServer(t)
|
||||||
|
srv.AddRepo("ukrrs", "MOPAC", "main", "main", "feat/quota")
|
||||||
|
|
||||||
|
t.Setenv("GITEA_URL", srv.URL)
|
||||||
|
t.Setenv("GITEA_KEY", fakeToken)
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
old := os.Stdin
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
w.WriteString("body via stdin\n")
|
||||||
|
w.Close()
|
||||||
|
os.Stdin = r
|
||||||
|
defer func() { os.Stdin = old; time.Sleep(0) }()
|
||||||
|
code := Run([]string{"pr", "create", "ukrrs/MOPAC", "--title", "stdin body",
|
||||||
|
"--body", "-", "--base", "main", "--head", "feat/quota"}, &out, &errb)
|
||||||
|
os.Stdin = old
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("pr create stdin: code=%d err=%s", code, errb.String())
|
||||||
|
}
|
||||||
|
stored, ok := srv.PullRequest("ukrrs", "MOPAC", 1)
|
||||||
|
if !ok || stored.Body != "body via stdin\n" {
|
||||||
|
t.Fatalf("stored body = %q ok=%v", stored.Body, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user