1
0
mirror of https://gitea.com/jolheiser/sip synced 2024-11-22 11:41:59 +01:00

Split up funcs, add PR create/status/checkout

Signed-off-by: jolheiser <john.olheiser@gmail.com>
This commit is contained in:
jolheiser 2020-02-16 23:28:11 -06:00
parent 20d5d36202
commit 8958c4a312
No known key found for this signature in database
GPG Key ID: B853ADA5DA7BBF7A
12 changed files with 439 additions and 206 deletions

@ -1,2 +1,29 @@
# Tea (alternative)
CLI for interacting with Gitea
CLI for interacting with Gitea
### Features
Understands the concepts of an origin vs remote repository.
By default uses remotes `origin` and `upstream`.
If no `upstream` repository is found, `upstream` becomes synonymous with `origin` for the sake of defaults.
* Configuration `tea config`
* Change the default `origin` remote name `tea config origin`
* Change the default `upstream` remote name `tea config upstrea`
* Login `tea login`
* Add a user token for API usage
* Generate a new token from CLI `tea login auto`
* Authenticate with username/password to get a new token without leaving the terminal
* List available logins `tea login list`
* Logout `tea logout`
* Remove user tokens
* Repository status `tea repo`
* Get basic information about the `upstream` repository
* Issue search `tea issues`
* Search issues based on keyword(s)
* Create a new issue `tea issues create`
* Pull request search `tea pulls`
* Search pull requests based on keyword(s)
* Create a new pull request `tea pulls create`
* Check pull request status (default based on current branch) `tea pulls status`
* Checkout a pull request to test locally `tea pulls checkout`

@ -14,6 +14,16 @@ import (
var (
Flags = []cli.Flag{
&cli.StringFlag{
Name: "origin",
Usage: "The origin remote",
Value: config.Origin,
},
&cli.StringFlag{
Name: "upstream",
Usage: "The upstream remote",
Value: config.Upstream,
},
&cli.StringFlag{
Name: "url",
Aliases: []string{"u"},
@ -82,6 +92,9 @@ func getClient(ctx *cli.Context) *gitea.Client {
func getUpstreamRepo() []string {
upstreamOnce.Do(func() {
upstreamRepo = git.GetRepo(config.Upstream)
if upstreamRepo == nil {
upstreamRepo = git.GetRepo(config.Origin)
}
})
return upstreamRepo
}

@ -10,13 +10,13 @@ import (
var Config = cli.Command{
Name: "config",
Aliases: []string{"cfg"},
Usage: "Modify Tea config",
Usage: "Modify tea config",
Action: doConfig,
Subcommands: []*cli.Command{
{
Name: "remote",
Usage: "Specify default remote name",
Action: doConfigRemote,
Name: "origin",
Usage: "Specify default origin name",
Action: doConfigOrigin,
},
{
Name: "upstream",
@ -27,15 +27,15 @@ var Config = cli.Command{
}
func doConfig(ctx *cli.Context) error {
if err := doConfigRemote(ctx); err != nil {
if err := doConfigOrigin(ctx); err != nil {
return err
}
return doConfigUpstream(ctx)
}
func doConfigRemote(ctx *cli.Context) error {
func doConfigOrigin(ctx *cli.Context) error {
question := &survey.Input{
Message: "Default remote name",
Message: "Default origin name",
Default: "origin",
}
var answer string

@ -3,7 +3,6 @@ package cmd
import (
"code.gitea.io/sdk/gitea"
"fmt"
"gitea.com/jolheiser/beaver"
"gitea.com/jolheiser/beaver/color"
"gitea.com/jolheiser/tea/modules/markdown"
"gitea.com/jolheiser/tea/modules/sdk"
@ -20,27 +19,30 @@ var Issues = cli.Command{
Usage: "Commands for interacting with issues",
Action: doIssuesSearch,
Subcommands: []*cli.Command{
{
Name: "create",
Usage: "Create a new issue",
Action: doIssueCreate,
},
&IssuesCreate,
},
}
func issuesSearch(ctx *cli.Context, pulls bool) error {
func doIssuesSearch(ctx *cli.Context) error {
if _, err := issuesSearch(ctx, false); err != nil {
return err
}
return nil
}
func issuesSearch(ctx *cli.Context, pulls bool) (*gitea.Issue, error) {
typ := "issues"
if pulls {
typ = "pulls"
}
issues, err := queryIssues(ctx, getClient(ctx), false)
issues, err := queryIssues(ctx, getClient(ctx), pulls)
if err != nil {
return err
return nil, err
}
if len(issues) == 0 {
stdout.Red("No " + typ + " found")
return nil
return nil, nil
}
issueMap := make(map[string]*gitea.Issue)
@ -68,85 +70,19 @@ func issuesSearch(ctx *cli.Context, pulls bool) error {
for key := range issueMap {
list = append(list, key)
}
sel := &survey.Select{Options: list, Message: "Matching " + typ + ", select one to see more details"}
sel := &survey.Select{Options: list, Message: "Matching " + typ}
var selection string
if err := survey.AskOne(sel, &selection); err != nil {
return err
return nil, err
}
body, err := markdown.Render(issueMap[selection].Body)
if err != nil {
return err
return nil, err
}
fmt.Println(body)
return nil
}
func doIssuesSearch(ctx *cli.Context) error {
return issuesSearch(ctx, false)
}
func doIssueCreate(ctx *cli.Context) error {
beaver.Infof("Creating a new issue for %s/%s/%s", ctx.String("url"), ctx.String("owner"), ctx.String("repo"))
token, err := requireToken(ctx)
if err != nil {
return err
}
var confirmed bool
var title, body string
for !confirmed {
questions := []*survey.Question{
{
Name: "title",
Prompt: &survey.Input{Message: "Title", Default: title},
Validate: survey.Required,
},
{
Name: "body",
Prompt: &survey.Multiline{Message: "Description", Default: body},
Validate: survey.Required,
},
}
answers := struct {
Title string
Body string
}{}
if err := survey.Ask(questions, &answers); err != nil {
return err
}
title = answers.Title
body = answers.Body
preview, err := markdown.Render(body)
if err != nil {
return err
}
fmt.Printf("%s\n\n%s\n", title, preview)
confirm := &survey.Confirm{Message: "Preview above, enter to create or 'n' to edit", Default: true}
if err := survey.AskOne(confirm, &confirmed); err != nil {
return err
}
}
client := gitea.NewClient(ctx.String("url"), token)
issue, err := client.CreateIssue(ctx.String("owner"), ctx.String("repo"), gitea.CreateIssueOption{Title: title, Body: body})
if err != nil {
return err
}
info := color.Info
cyan := color.New(color.FgCyan)
fmt.Println(info.Format("Issue"), cyan.Format(fmt.Sprintf("#%d", issue.Index)), info.Format("created!"))
fmt.Println(cyan.Format(fmt.Sprintf("%s/%s/%s/issues/%d", ctx.String("url"), ctx.String("owner"), ctx.String("repo"), issue.Index)))
return nil
return issueMap[selection], nil
}
func queryIssues(ctx *cli.Context, client *gitea.Client, pulls bool) ([]*gitea.Issue, error) {
@ -178,8 +114,10 @@ func queryIssues(ctx *cli.Context, client *gitea.Client, pulls bool) ([]*gitea.I
filtered := make([]*gitea.Issue, 0)
for _, issue := range issues {
if issue.PullRequest != nil && pulls {
filtered = append(filtered, issue)
if pulls {
if issue.PullRequest != nil {
filtered = append(filtered, issue)
}
continue
}
filtered = append(filtered, issue)

79
cmd/issues_create.go Normal file

@ -0,0 +1,79 @@
package cmd
import (
"code.gitea.io/sdk/gitea"
"fmt"
"gitea.com/jolheiser/beaver/color"
"gitea.com/jolheiser/tea/modules/markdown"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
)
var IssuesCreate = cli.Command{
Name: "create",
Usage: "Create a new issue",
Action: doIssueCreate,
}
func doIssueCreate(ctx *cli.Context) error {
fmt.Println()
url := color.New(color.FgYellow).Format(fmt.Sprintf("%s/%s/%s", ctx.String("url"), ctx.String("owner"), ctx.String("repo")))
fmt.Println(color.New(color.FgCyan).Format("Creating a new issue for"), url)
token, err := requireToken(ctx)
if err != nil {
return err
}
var confirmed bool
var title, body string
for !confirmed {
questions := []*survey.Question{
{
Name: "title",
Prompt: &survey.Input{Message: "Title", Default: title},
Validate: survey.Required,
},
{
Name: "body",
Prompt: &survey.Multiline{Message: "Description", Default: body},
},
}
answers := struct {
Title string
Body string
}{}
if err := survey.Ask(questions, &answers); err != nil {
return err
}
title = answers.Title
body = answers.Body
preview, err := markdown.Render(body)
if err != nil {
return err
}
fmt.Printf("%s\n\n%s\n", title, preview)
confirm := &survey.Confirm{Message: "Preview above, enter to create or 'n' to edit", Default: true}
if err := survey.AskOne(confirm, &confirmed); err != nil {
return err
}
}
client := gitea.NewClient(ctx.String("url"), token)
issue, err := client.CreateIssue(ctx.String("owner"), ctx.String("repo"), gitea.CreateIssueOption{Title: title, Body: body})
if err != nil {
return err
}
info := color.Info
cyan := color.New(color.FgCyan)
fmt.Println(info.Format("Issue"), cyan.Format(fmt.Sprintf("#%d", issue.Index)), info.Format("created!"))
fmt.Println(cyan.Format(fmt.Sprintf("%s/%s/%s/issues/%d", ctx.String("url"), ctx.String("owner"), ctx.String("repo"), issue.Index)))
return nil
}

@ -1,134 +1,24 @@
package cmd
import (
"code.gitea.io/sdk/gitea"
"fmt"
"gitea.com/jolheiser/beaver"
"gitea.com/jolheiser/beaver/color"
"gitea.com/jolheiser/tea/modules/git"
"gitea.com/jolheiser/tea/modules/markdown"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
)
var Pulls = cli.Command{
Name: "pulls",
Aliases: []string{"pr"},
Aliases: []string{"pull", "pr"},
Usage: "Commands for interacting with pull requests",
Action: doPullsSearch,
Subcommands: []*cli.Command{
{
Name: "create",
Usage: "Create a new pull request",
Action: doPullCreate,
},
&PullsCreate,
&PullsStatus,
&PullsCheckout,
},
}
func doPullsSearch(ctx *cli.Context) error {
return issuesSearch(ctx, true)
}
func doPullCreate(ctx *cli.Context) error {
beaver.Infof("Creating a new pull request for %s/%s/%s", ctx.String("url"), ctx.String("owner"), ctx.String("repo"))
token, err := requireToken(ctx)
if err != nil {
if _, err := issuesSearch(ctx, true); err != nil {
return err
}
client := gitea.NewClient(ctx.String("url"), token)
upstreams, err := client.ListRepoBranches(getUpstreamRepo()[1], getUpstreamRepo()[2])
if err != nil {
return err
}
bases := make([]string, len(upstreams))
defUpstream := upstreams[0].Name
for idx, upstream := range upstreams {
if upstream.Name == "master" {
defUpstream = upstream.Name
}
bases[idx] = upstream.Name
}
origins, err := client.ListRepoBranches(getOriginRepo()[1], getOriginRepo()[2])
if err != nil {
return err
}
heads := make([]string, len(origins))
defOrigin := origins[0].Name
for idx, origin := range origins {
if origin.Name == git.Branch() {
defOrigin = origin.Name
}
heads[idx] = origin.Name
}
var confirmed bool
var title, body string
base := defUpstream
head := defOrigin
for !confirmed {
questions := []*survey.Question{
{
Name: "title",
Prompt: &survey.Input{Message: "Title", Default: title},
Validate: survey.Required,
},
{
Name: "body",
Prompt: &survey.Multiline{Message: "Description", Default: body},
Validate: survey.Required,
},
{
Name: "base",
Prompt: &survey.Select{Message: "Base target", Options: bases, Default: base},
Validate: survey.Required,
},
{
Name: "head",
Prompt: &survey.Select{Message: "Head target", Options: heads, Default: head},
Validate: survey.Required,
},
}
answers := struct {
Title string
Body string
Base string
Head string
}{}
if err := survey.Ask(questions, &answers); err != nil {
return err
}
title = answers.Title
body = answers.Body
base = answers.Base
head = answers.Head
preview, err := markdown.Render(body)
if err != nil {
return err
}
fmt.Printf("%s\n\n%s\n", title, preview)
confirm := &survey.Confirm{Message: "Preview above, enter to create or 'n' to edit", Default: true}
if err := survey.AskOne(confirm, &confirmed); err != nil {
return err
}
}
pull, err := client.CreatePullRequest(ctx.String("owner"), ctx.String("repo"), gitea.CreatePullRequestOption{Title: title, Body: body, Base: base, Head: head})
if err != nil {
return err
}
info := color.Info
cyan := color.New(color.FgCyan)
fmt.Println(info.Format("PR"), cyan.Format(fmt.Sprintf("#%d", pull.Index)), info.Format("created!"))
fmt.Println(cyan.Format(fmt.Sprintf("%s/%s/%s/pulls/%d", ctx.String("url"), ctx.String("owner"), ctx.String("repo"), pull.Index)))
return nil
}

83
cmd/pulls_checkout.go Normal file

@ -0,0 +1,83 @@
package cmd
import (
"code.gitea.io/sdk/gitea"
"errors"
"fmt"
"gitea.com/jolheiser/beaver"
"gitea.com/jolheiser/tea/modules/config"
"github.com/AlecAivazis/survey/v2"
"github.com/huandu/xstrings"
"github.com/urfave/cli/v2"
"os"
"os/exec"
"strconv"
)
var PullsCheckout = cli.Command{
Name: "checkout",
Usage: "Checkout a pull request for testing",
Action: doPullCheckout,
}
func doPullCheckout(ctx *cli.Context) error {
var issue *gitea.Issue
questions := []*survey.Question{
{
Name: "index",
Prompt: &survey.Input{Message: "Pull request number", Help: "Don't worry if you aren't sure! Just say -1 and we'll search for it instead!"},
Validate: validatePRNum,
},
}
prNum := struct {
Index int64
}{}
if err := survey.Ask(questions, &prNum); err != nil {
return err
}
if prNum.Index < 0 {
var confirmed bool
for !confirmed {
iss, err := issuesSearch(ctx, true)
if err != nil {
return err
}
issue = iss
confirmation := &survey.Confirm{Message: "Is this the pull request you want to checkout?"}
if err := survey.AskOne(confirmation, &confirmed); err != nil {
return err
}
}
} else {
iss, err := getClient(ctx).GetIssue(upstreamRepo[1], upstreamRepo[2], prNum.Index)
if err != nil {
return err
}
issue = iss
}
if issue == nil {
return errors.New("no pull request selected")
}
branch := fmt.Sprintf("pr%d-%s", issue.Index, xstrings.ToKebabCase(issue.Title))
cmd := exec.Command("git", "fetch", config.Upstream, fmt.Sprintf("pull/%d/head:%s", issue.Index, branch))
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
return err
}
beaver.Infof("Pull request successfully checked out. Switch to it using `git checkout %s`", branch)
return nil
}
func validatePRNum(ans interface{}) error {
if err := survey.Required(ans); err != nil {
return err
}
if _, err := strconv.Atoi(ans.(string)); err != nil {
return errors.New("pull request number must be an number")
}
return nil
}

142
cmd/pulls_create.go Normal file

@ -0,0 +1,142 @@
package cmd
import (
"code.gitea.io/sdk/gitea"
"fmt"
"gitea.com/jolheiser/beaver/color"
"gitea.com/jolheiser/tea/modules/git"
"gitea.com/jolheiser/tea/modules/markdown"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
)
var PullsCreate = cli.Command{
Name: "create",
Usage: "Create a new pull request",
Action: doPullCreate,
}
func doPullCreate(ctx *cli.Context) error {
fmt.Println()
url := color.New(color.FgYellow).Format(fmt.Sprintf("%s/%s/%s", ctx.String("url"), ctx.String("owner"), ctx.String("repo")))
fmt.Println(color.New(color.FgCyan).Format("Creating a new pull request for"), url)
token, err := requireToken(ctx)
if err != nil {
return err
}
client := gitea.NewClient(ctx.String("url"), token)
upstreams, err := client.ListRepoBranches(getUpstreamRepo()[1], getUpstreamRepo()[2])
if err != nil {
return err
}
bases := make([]string, len(upstreams))
defUpstream := upstreams[0].Name
for idx, upstream := range upstreams {
if upstream.Name == "master" {
defUpstream = upstream.Name
}
bases[idx] = upstream.Name
}
origins, err := client.ListRepoBranches(getOriginRepo()[1], getOriginRepo()[2])
if err != nil {
return err
}
heads := make([]string, len(origins))
defOrigin := origins[0].Name
for idx, origin := range origins {
if origin.Name == git.Branch() {
defOrigin = getOriginRepo()[1] + ":" + origin.Name
}
heads[idx] = getOriginRepo()[1] + ":" + origin.Name
}
var confirmed bool
var title, body string
base := defUpstream
head := defOrigin
for !confirmed {
questions := []*survey.Question{
{
Name: "title",
Prompt: &survey.Input{Message: "Title", Default: title},
Validate: survey.Required,
},
{
Name: "body",
Prompt: &survey.Multiline{Message: "Description", Default: body},
},
{
Name: "base",
Prompt: &survey.Select{Message: "Base target", Options: bases, Default: base},
Validate: survey.Required,
},
{
Name: "head",
Prompt: &survey.Select{Message: "Head target", Options: heads, Default: head},
Validate: survey.Required,
},
}
answers := struct {
Title string
Body string
Base string
Head string
}{}
if err := survey.Ask(questions, &answers); err != nil {
return err
}
title = answers.Title
body = answers.Body
base = answers.Base
head = answers.Head
preview, err := markdown.Render(body)
if err != nil {
return err
}
fmt.Printf("%s\n\n%s\n", title, preview)
confirm := &survey.Confirm{Message: "Preview above, enter to create or 'n' to edit", Default: true}
if err := survey.AskOne(confirm, &confirmed); err != nil {
return err
}
}
pull, err := client.CreatePullRequest(ctx.String("owner"), ctx.String("repo"), gitea.CreatePullRequestOption{Title: title, Body: body, Base: base, Head: head})
if err != nil {
if fmt.Sprint(err) == "409 Conflict" { // Hard-coded in the SDK
return existingPR(client, getUpstreamRepo()[1], getUpstreamRepo()[2], head, err)
}
return err
}
info := color.Info
cyan := color.New(color.FgCyan)
fmt.Println(info.Format("PR"), cyan.Format(fmt.Sprintf("#%d", pull.Index)), info.Format("created!"))
fmt.Println(cyan.Format(fmt.Sprintf("%s/%s/%s/pulls/%d", ctx.String("url"), ctx.String("owner"), ctx.String("repo"), pull.Index)))
return nil
}
func existingPR(client *gitea.Client, owner, repo, head string, pullErr error) error {
pulls, err := client.ListRepoPullRequests(owner, repo, gitea.ListPullRequestsOptions{State: "open"})
if err != nil {
return err
}
for _, pull := range pulls {
compare := fmt.Sprintf("%s:%s", pull.Head.Repository.Owner.UserName, pull.Head.Name)
if compare == head {
fmt.Println(color.New(color.FgCyan).Format("PR already exists at"), color.New(color.FgYellow).Format(pull.HTMLURL))
return nil
}
}
return pullErr
}

58
cmd/pulls_status.go Normal file

@ -0,0 +1,58 @@
package cmd
import (
"code.gitea.io/sdk/gitea"
"fmt"
"gitea.com/jolheiser/beaver/color"
"gitea.com/jolheiser/tea/modules/git"
"gitea.com/jolheiser/tea/modules/sdk"
"github.com/urfave/cli/v2"
"strconv"
)
var PullsStatus = cli.Command{
Name: "status",
Usage: "View the status of a pull request",
Action: doPullStatus,
}
func doPullStatus(ctx *cli.Context) error {
client := getClient(ctx)
head := fmt.Sprintf("%s:%s", getOriginRepo()[1], git.Branch())
pulls, err := sdk.GetPulls(client, getUpstreamRepo()[1], getUpstreamRepo()[2], gitea.ListPullRequestsOptions{State: "all"})
if err != nil {
return err
}
var pr *gitea.PullRequest
for _, pull := range pulls {
compare := fmt.Sprintf("%s:%s", pull.Head.Repository.Owner.UserName, pull.Head.Name)
if compare == head {
pr = pull
break
}
}
if pr == nil {
return fmt.Errorf("no pull request found with head target %s", color.New(color.FgMagenta).Format(head))
}
index := color.New(color.FgCyan).Format("#" + strconv.Itoa(int(pr.Index)))
title := color.New(color.FgYellow).Format(pr.Title)
state := color.New(color.FgGreen).Format("[open]")
if pr.HasMerged {
state = color.New(color.FgMagenta).Format("[merged]")
} else if pr.State == gitea.StateClosed {
state = color.New(color.FgRed).Format("[closed]")
}
lbls := make([]string, len(pr.Labels))
for idx, label := range pr.Labels {
lbls[idx] = label.Name
}
fmt.Println(index, title, state)
fmt.Println(color.New(color.FgCyan).Format(fmt.Sprintf("%d comments", pr.Comments)))
fmt.Println(color.New(color.FgYellow).Format(pr.HTMLURL))
return nil
}

1
go.mod

@ -8,6 +8,7 @@ require (
github.com/AlecAivazis/survey/v2 v2.0.5
github.com/BurntSushi/toml v0.3.1
github.com/charmbracelet/glamour v0.1.0
github.com/huandu/xstrings v1.3.0
github.com/kr/pretty v0.2.0 // indirect
github.com/kr/pty v1.1.8 // indirect
github.com/kyokomi/emoji v2.1.0+incompatible

2
go.sum

@ -45,6 +45,8 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/huandu/xstrings v1.3.0 h1:gvV6jG9dTgFEncxo+AF7PH6MZXi/vZl25owA/8Dg8Wo=
github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=

@ -10,7 +10,7 @@ func GetRepo(remoteName string) []string {
cmd := exec.Command("git", "remote", "get-url", remoteName)
out, err := cmd.Output()
if err != nil {
return []string{"", ""}
return []string{"https://gitea.com", "jolheiser", "tea"}
}
remote := strings.TrimSpace(string(out))