commit
948088a143
9 changed files with 607 additions and 219 deletions
|
|
@ -481,3 +481,11 @@ func milestoneNodeIdToDatabaseId(nodeId string) (string, error) {
|
|||
}
|
||||
return splitted[1], nil
|
||||
}
|
||||
|
||||
func (i Issue) Link() string {
|
||||
return i.URL
|
||||
}
|
||||
|
||||
func (i Issue) Identifier() string {
|
||||
return i.ID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,6 +136,14 @@ func (pr PullRequest) HeadLabel() string {
|
|||
return pr.HeadRefName
|
||||
}
|
||||
|
||||
func (pr PullRequest) Link() string {
|
||||
return pr.URL
|
||||
}
|
||||
|
||||
func (pr PullRequest) Identifier() string {
|
||||
return pr.ID
|
||||
}
|
||||
|
||||
type PullRequestReviewStatus struct {
|
||||
ChangesRequested bool
|
||||
Approved bool
|
||||
|
|
|
|||
|
|
@ -1,59 +1,28 @@
|
|||
package comment
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmd/issue/shared"
|
||||
issueShared "github.com/cli/cli/pkg/cmd/issue/shared"
|
||||
prShared "github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/surveyext"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type CommentOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
EditSurvey func() (string, error)
|
||||
InputTypeSurvey func() (inputType, error)
|
||||
ConfirmSubmitSurvey func() (bool, error)
|
||||
OpenInBrowser func(string) error
|
||||
|
||||
SelectorArg string
|
||||
Interactive bool
|
||||
InputType inputType
|
||||
Body string
|
||||
}
|
||||
|
||||
type inputType int
|
||||
|
||||
const (
|
||||
inputTypeEditor inputType = iota
|
||||
inputTypeInline
|
||||
inputTypeWeb
|
||||
)
|
||||
|
||||
func NewCmdComment(f *cmdutil.Factory, runF func(*CommentOptions) error) *cobra.Command {
|
||||
opts := &CommentOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
EditSurvey: editSurvey(f.Config, f.IOStreams),
|
||||
InputTypeSurvey: inputTypeSurvey,
|
||||
ConfirmSubmitSurvey: confirmSubmitSurvey,
|
||||
OpenInBrowser: utils.OpenInBrowser,
|
||||
func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) error) *cobra.Command {
|
||||
opts := &prShared.CommentableOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
EditSurvey: prShared.CommentableEditSurvey(f.Config, f.IOStreams),
|
||||
InteractiveEditSurvey: prShared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams),
|
||||
ConfirmSubmitSurvey: prShared.CommentableConfirmSubmitSurvey,
|
||||
OpenInBrowser: utils.OpenInBrowser,
|
||||
}
|
||||
|
||||
var webMode bool
|
||||
var editorMode bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "comment {<number> | <url>}",
|
||||
Short: "Create a new issue comment",
|
||||
|
|
@ -61,140 +30,40 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*CommentOptions) error) *cobra.
|
|||
$ gh issue comment 22 --body "I was able to reproduce this issue, lets fix it."
|
||||
`),
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
opts.SelectorArg = args[0]
|
||||
|
||||
inputFlags := 0
|
||||
if cmd.Flags().Changed("body") {
|
||||
opts.InputType = inputTypeInline
|
||||
inputFlags++
|
||||
}
|
||||
if webMode {
|
||||
opts.InputType = inputTypeWeb
|
||||
inputFlags++
|
||||
}
|
||||
if editorMode {
|
||||
opts.InputType = inputTypeEditor
|
||||
inputFlags++
|
||||
}
|
||||
|
||||
if inputFlags == 0 {
|
||||
if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("--body or --web required when not running interactively")}
|
||||
}
|
||||
opts.Interactive = true
|
||||
} else if inputFlags == 1 {
|
||||
if !opts.IO.CanPrompt() && opts.InputType == inputTypeEditor {
|
||||
return &cmdutil.FlagError{Err: errors.New("--body or --web required when not running interactively")}
|
||||
}
|
||||
} else if inputFlags > 1 {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of --body, --editor, or --web")}
|
||||
}
|
||||
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.RetrieveCommentable = retrieveIssue(f.HttpClient, f.BaseRepo, args[0])
|
||||
return prShared.CommentablePreRun(cmd, opts)
|
||||
},
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return commentRun(opts)
|
||||
return prShared.CommentableRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Supply a body. Will prompt for one otherwise.")
|
||||
cmd.Flags().BoolVarP(&editorMode, "editor", "e", false, "Add body using editor")
|
||||
cmd.Flags().BoolVarP(&webMode, "web", "w", false, "Add body in browser")
|
||||
cmd.Flags().BoolP("editor", "e", false, "Add body using editor")
|
||||
cmd.Flags().BoolP("web", "w", false, "Add body in browser")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func commentRun(opts *CommentOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
issue, baseRepo, err := shared.IssueFromArg(apiClient, opts.BaseRepo, opts.SelectorArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
inputType, err := opts.InputTypeSurvey()
|
||||
func retrieveIssue(httpClient func() (*http.Client, error),
|
||||
baseRepo func() (ghrepo.Interface, error),
|
||||
selector string) func() (prShared.Commentable, ghrepo.Interface, error) {
|
||||
return func() (prShared.Commentable, ghrepo.Interface, error) {
|
||||
httpClient, err := httpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
opts.InputType = inputType
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
switch opts.InputType {
|
||||
case inputTypeWeb:
|
||||
openURL := issue.URL + "#issuecomment-new"
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
||||
}
|
||||
return opts.OpenInBrowser(openURL)
|
||||
case inputTypeEditor:
|
||||
body, err := opts.EditSurvey()
|
||||
issue, repo, err := issueShared.IssueFromArg(apiClient, baseRepo, selector)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
opts.Body = body
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
cont, err := opts.ConfirmSubmitSurvey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !cont {
|
||||
return fmt.Errorf("Discarding...")
|
||||
}
|
||||
}
|
||||
|
||||
params := api.CommentCreateInput{Body: opts.Body, SubjectId: issue.ID}
|
||||
url, err := api.CommentCreate(apiClient, baseRepo.RepoHost(), params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(opts.IO.Out, url)
|
||||
return nil
|
||||
}
|
||||
|
||||
var inputTypeSurvey = func() (inputType, error) {
|
||||
var result int
|
||||
inputTypeQuestion := &survey.Select{
|
||||
Message: "Where do you want to draft your comment?",
|
||||
Options: []string{"Editor", "Web"},
|
||||
}
|
||||
err := survey.AskOne(inputTypeQuestion, &result)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if result == 0 {
|
||||
return inputTypeEditor, nil
|
||||
} else {
|
||||
return inputTypeWeb, nil
|
||||
}
|
||||
}
|
||||
|
||||
var confirmSubmitSurvey = func() (bool, error) {
|
||||
var confirm bool
|
||||
submit := &survey.Confirm{
|
||||
Message: "Submit?",
|
||||
Default: true,
|
||||
}
|
||||
err := survey.AskOne(submit, &confirm)
|
||||
return confirm, err
|
||||
}
|
||||
|
||||
var editSurvey = func(cf func() (config.Config, error), io *iostreams.IOStreams) func() (string, error) {
|
||||
return func() (string, error) {
|
||||
editorCommand, err := cmdutil.DetermineEditor(cf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return surveyext.Edit(editorCommand, "*.md", "", io.In, io.Out, io.ErrOut, nil)
|
||||
return issue, repo, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -17,20 +18,19 @@ func TestNewCmdComment(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output CommentOptions
|
||||
output shared.CommentableOptions
|
||||
wantsErr bool
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
input: "",
|
||||
output: CommentOptions{},
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "issue number",
|
||||
input: "1",
|
||||
output: CommentOptions{
|
||||
SelectorArg: "1",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
|
|
@ -40,8 +40,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
{
|
||||
name: "issue url",
|
||||
input: "https://github.com/OWNER/REPO/issues/12",
|
||||
output: CommentOptions{
|
||||
SelectorArg: "https://github.com/OWNER/REPO/issues/12",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
|
|
@ -51,10 +50,9 @@ func TestNewCmdComment(t *testing.T) {
|
|||
{
|
||||
name: "body flag",
|
||||
input: "1 --body test",
|
||||
output: CommentOptions{
|
||||
SelectorArg: "1",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: inputTypeInline,
|
||||
InputType: shared.InputTypeInline,
|
||||
Body: "test",
|
||||
},
|
||||
wantsErr: false,
|
||||
|
|
@ -62,10 +60,9 @@ func TestNewCmdComment(t *testing.T) {
|
|||
{
|
||||
name: "editor flag",
|
||||
input: "1 --editor",
|
||||
output: CommentOptions{
|
||||
SelectorArg: "1",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: inputTypeEditor,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
|
|
@ -73,10 +70,9 @@ func TestNewCmdComment(t *testing.T) {
|
|||
{
|
||||
name: "web flag",
|
||||
input: "1 --web",
|
||||
output: CommentOptions{
|
||||
SelectorArg: "1",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: inputTypeWeb,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
|
|
@ -84,25 +80,25 @@ func TestNewCmdComment(t *testing.T) {
|
|||
{
|
||||
name: "editor and web flags",
|
||||
input: "1 --editor --web",
|
||||
output: CommentOptions{},
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor and body flags",
|
||||
input: "1 --editor --body test",
|
||||
output: CommentOptions{},
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "web and body flags",
|
||||
input: "1 --web --body test",
|
||||
output: CommentOptions{},
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor, web, and body flags",
|
||||
input: "1 --editor --web --body test",
|
||||
output: CommentOptions{},
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
|
|
@ -121,8 +117,8 @@ func TestNewCmdComment(t *testing.T) {
|
|||
argv, err := shlex.Split(tt.input)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts *CommentOptions
|
||||
cmd := NewCmdComment(f, func(opts *CommentOptions) error {
|
||||
var gotOpts *shared.CommentableOptions
|
||||
cmd := NewCmdComment(f, func(opts *shared.CommentableOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
|
|
@ -140,7 +136,6 @@ func TestNewCmdComment(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.output.SelectorArg, gotOpts.SelectorArg)
|
||||
assert.Equal(t, tt.output.Interactive, gotOpts.Interactive)
|
||||
assert.Equal(t, tt.output.InputType, gotOpts.InputType)
|
||||
assert.Equal(t, tt.output.Body, gotOpts.Body)
|
||||
|
|
@ -151,38 +146,20 @@ func TestNewCmdComment(t *testing.T) {
|
|||
func Test_commentRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *CommentOptions
|
||||
input *shared.CommentableOptions
|
||||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
}{
|
||||
{
|
||||
name: "interactive web",
|
||||
input: &CommentOptions{
|
||||
SelectorArg: "123",
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
|
||||
InputTypeSurvey: func() (inputType, error) { return inputTypeWeb, nil },
|
||||
OpenInBrowser: func(string) error { return nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockIssueFromNumber(t, reg)
|
||||
},
|
||||
stderr: "Opening github.com/OWNER/REPO/issues/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "interactive editor",
|
||||
input: &CommentOptions{
|
||||
SelectorArg: "123",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
|
||||
EditSurvey: func() (string, error) { return "comment body", nil },
|
||||
InputTypeSurvey: func() (inputType, error) { return inputTypeEditor, nil },
|
||||
ConfirmSubmitSurvey: func() (bool, error) { return true, nil },
|
||||
InteractiveEditSurvey: func() (string, error) { return "comment body", nil },
|
||||
ConfirmSubmitSurvey: func() (bool, error) { return true, nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockIssueFromNumber(t, reg)
|
||||
|
|
@ -192,10 +169,9 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "non-interactive web",
|
||||
input: &CommentOptions{
|
||||
SelectorArg: "123",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: inputTypeWeb,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
|
||||
OpenInBrowser: func(string) error { return nil },
|
||||
|
|
@ -207,10 +183,9 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "non-interactive editor",
|
||||
input: &CommentOptions{
|
||||
SelectorArg: "123",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: inputTypeEditor,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
|
||||
EditSurvey: func() (string, error) { return "comment body", nil },
|
||||
|
|
@ -223,10 +198,9 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "non-interactive inline",
|
||||
input: &CommentOptions{
|
||||
SelectorArg: "123",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: inputTypeInline,
|
||||
InputType: shared.InputTypeInline,
|
||||
Body: "comment body",
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
|
|
@ -246,16 +220,15 @@ func Test_commentRun(t *testing.T) {
|
|||
defer reg.Verify(t)
|
||||
tt.httpStubs(t, reg)
|
||||
|
||||
httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }
|
||||
baseRepo := func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
|
||||
|
||||
tt.input.IO = io
|
||||
tt.input.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
tt.input.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
}
|
||||
tt.input.HttpClient = httpClient
|
||||
tt.input.RetrieveCommentable = retrieveIssue(tt.input.HttpClient, baseRepo, "123")
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := commentRun(tt.input)
|
||||
err := shared.CommentableRun(tt.input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.stdout, stdout.String())
|
||||
assert.Equal(t, tt.stderr, stderr.String())
|
||||
|
|
|
|||
78
pkg/cmd/pr/comment/comment.go
Normal file
78
pkg/cmd/pr/comment/comment.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package comment
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) error) *cobra.Command {
|
||||
opts := &shared.CommentableOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
EditSurvey: shared.CommentableEditSurvey(f.Config, f.IOStreams),
|
||||
InteractiveEditSurvey: shared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams),
|
||||
ConfirmSubmitSurvey: shared.CommentableConfirmSubmitSurvey,
|
||||
OpenInBrowser: utils.OpenInBrowser,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "comment [<number> | <url> | <branch>]",
|
||||
Short: "Create a new pr comment",
|
||||
Example: heredoc.Doc(`
|
||||
$ gh pr comment 22 --body "This looks great, lets get it deployed."
|
||||
`),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
}
|
||||
var selector string
|
||||
if len(args) > 0 {
|
||||
selector = args[0]
|
||||
}
|
||||
opts.RetrieveCommentable = retrievePR(f.HttpClient, f.BaseRepo, f.Branch, f.Remotes, selector)
|
||||
return shared.CommentablePreRun(cmd, opts)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return shared.CommentableRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Supply a body. Will prompt for one otherwise.")
|
||||
cmd.Flags().BoolP("editor", "e", false, "Add body using editor")
|
||||
cmd.Flags().BoolP("web", "w", false, "Add body in browser")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func retrievePR(httpClient func() (*http.Client, error),
|
||||
baseRepo func() (ghrepo.Interface, error),
|
||||
branch func() (string, error),
|
||||
remotes func() (context.Remotes, error),
|
||||
selector string) func() (shared.Commentable, ghrepo.Interface, error) {
|
||||
return func() (shared.Commentable, ghrepo.Interface, error) {
|
||||
httpClient, err := httpClient()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
pr, repo, err := shared.PRFromArgs(apiClient, baseRepo, branch, remotes, selector)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return pr, repo, nil
|
||||
}
|
||||
}
|
||||
278
pkg/cmd/pr/comment/comment_test.go
Normal file
278
pkg/cmd/pr/comment/comment_test.go
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
package comment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdComment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output shared.CommentableOptions
|
||||
wantsErr bool
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
input: "",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "pr number",
|
||||
input: "1",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "pr url",
|
||||
input: "https://github.com/OWNER/REPO/pull/12",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "pr branch",
|
||||
input: "branch-name",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "body flag",
|
||||
input: "1 --body test",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeInline,
|
||||
Body: "test",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "editor flag",
|
||||
input: "1 --editor",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "web flag",
|
||||
input: "1 --web",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "editor and web flags",
|
||||
input: "1 --editor --web",
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor and body flags",
|
||||
input: "1 --editor --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "web and body flags",
|
||||
input: "1 --web --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor, web, and body flags",
|
||||
input: "1 --editor --web --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
io, _, _, _ := iostreams.Test()
|
||||
io.SetStdoutTTY(true)
|
||||
io.SetStdinTTY(true)
|
||||
io.SetStderrTTY(true)
|
||||
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: io,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.input)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts *shared.CommentableOptions
|
||||
cmd := NewCmdComment(f, func(opts *shared.CommentableOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.Flags().BoolP("help", "x", false, "")
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.output.Interactive, gotOpts.Interactive)
|
||||
assert.Equal(t, tt.output.InputType, gotOpts.InputType)
|
||||
assert.Equal(t, tt.output.Body, gotOpts.Body)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_commentRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *shared.CommentableOptions
|
||||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
}{
|
||||
{
|
||||
name: "interactive editor",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
|
||||
InteractiveEditSurvey: func() (string, error) { return "comment body", nil },
|
||||
ConfirmSubmitSurvey: func() (bool, error) { return true, nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockPullRequestFromNumber(t, reg)
|
||||
mockCommentCreate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
|
||||
OpenInBrowser: func(string) error { return nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockPullRequestFromNumber(t, reg)
|
||||
},
|
||||
stderr: "Opening github.com/OWNER/REPO/pull/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive editor",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
|
||||
EditSurvey: func() (string, error) { return "comment body", nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockPullRequestFromNumber(t, reg)
|
||||
mockCommentCreate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive inline",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeInline,
|
||||
Body: "comment body",
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockPullRequestFromNumber(t, reg)
|
||||
mockCommentCreate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
io.SetStdoutTTY(true)
|
||||
io.SetStdinTTY(true)
|
||||
io.SetStderrTTY(true)
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
tt.httpStubs(t, reg)
|
||||
|
||||
httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }
|
||||
baseRepo := func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
|
||||
branch := func() (string, error) { return "", nil }
|
||||
remotes := func() (context.Remotes, error) { return nil, nil }
|
||||
|
||||
tt.input.IO = io
|
||||
tt.input.HttpClient = httpClient
|
||||
tt.input.RetrieveCommentable = retrievePR(httpClient, baseRepo, branch, remotes, "123")
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := shared.CommentableRun(tt.input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.stdout, stdout.String())
|
||||
assert.Equal(t, tt.stderr, stderr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mockPullRequestFromNumber(_ *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"url": "https://github.com/OWNER/REPO/pull/123"
|
||||
} } } }`),
|
||||
)
|
||||
}
|
||||
|
||||
func mockCommentCreate(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation CommentCreate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "addComment": { "commentEdge": { "node": {
|
||||
"url": "https://github.com/OWNER/REPO/pull/123#issuecomment-456"
|
||||
} } } } }`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, "comment body", inputs["body"])
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import (
|
|||
cmdCheckout "github.com/cli/cli/pkg/cmd/pr/checkout"
|
||||
cmdChecks "github.com/cli/cli/pkg/cmd/pr/checks"
|
||||
cmdClose "github.com/cli/cli/pkg/cmd/pr/close"
|
||||
cmdComment "github.com/cli/cli/pkg/cmd/pr/comment"
|
||||
cmdCreate "github.com/cli/cli/pkg/cmd/pr/create"
|
||||
cmdDiff "github.com/cli/cli/pkg/cmd/pr/diff"
|
||||
cmdList "github.com/cli/cli/pkg/cmd/pr/list"
|
||||
|
|
@ -53,6 +54,7 @@ func NewCmdPR(f *cmdutil.Factory) *cobra.Command {
|
|||
cmd.AddCommand(cmdStatus.NewCmdStatus(f, nil))
|
||||
cmd.AddCommand(cmdView.NewCmdView(f, nil))
|
||||
cmd.AddCommand(cmdChecks.NewCmdChecks(f, nil))
|
||||
cmd.AddCommand(cmdComment.NewCmdComment(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
168
pkg/cmd/pr/shared/commentable.go
Normal file
168
pkg/cmd/pr/shared/commentable.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/surveyext"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type InputType int
|
||||
|
||||
const (
|
||||
InputTypeEditor InputType = iota
|
||||
InputTypeInline
|
||||
InputTypeWeb
|
||||
)
|
||||
|
||||
type Commentable interface {
|
||||
Link() string
|
||||
Identifier() string
|
||||
}
|
||||
|
||||
type CommentableOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
HttpClient func() (*http.Client, error)
|
||||
RetrieveCommentable func() (Commentable, ghrepo.Interface, error)
|
||||
EditSurvey func() (string, error)
|
||||
InteractiveEditSurvey func() (string, error)
|
||||
ConfirmSubmitSurvey func() (bool, error)
|
||||
OpenInBrowser func(string) error
|
||||
Interactive bool
|
||||
InputType InputType
|
||||
Body string
|
||||
}
|
||||
|
||||
func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error {
|
||||
inputFlags := 0
|
||||
if cmd.Flags().Changed("body") {
|
||||
opts.InputType = InputTypeInline
|
||||
inputFlags++
|
||||
}
|
||||
if web, _ := cmd.Flags().GetBool("web"); web {
|
||||
opts.InputType = InputTypeWeb
|
||||
inputFlags++
|
||||
}
|
||||
if editor, _ := cmd.Flags().GetBool("editor"); editor {
|
||||
opts.InputType = InputTypeEditor
|
||||
inputFlags++
|
||||
}
|
||||
|
||||
if inputFlags == 0 {
|
||||
if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("--body or --web required when not running interactively")}
|
||||
}
|
||||
opts.Interactive = true
|
||||
} else if inputFlags == 1 {
|
||||
if !opts.IO.CanPrompt() && opts.InputType == InputTypeEditor {
|
||||
return &cmdutil.FlagError{Err: errors.New("--body or --web required when not running interactively")}
|
||||
}
|
||||
} else if inputFlags > 1 {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of --body, --editor, or --web")}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CommentableRun(opts *CommentableOptions) error {
|
||||
commentable, repo, err := opts.RetrieveCommentable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch opts.InputType {
|
||||
case InputTypeWeb:
|
||||
openURL := commentable.Link() + "#issuecomment-new"
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
||||
}
|
||||
return opts.OpenInBrowser(openURL)
|
||||
case InputTypeEditor:
|
||||
var body string
|
||||
if opts.Interactive {
|
||||
body, err = opts.InteractiveEditSurvey()
|
||||
} else {
|
||||
body, err = opts.EditSurvey()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Body = body
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
cont, err := opts.ConfirmSubmitSurvey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !cont {
|
||||
return errors.New("Discarding...")
|
||||
}
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
params := api.CommentCreateInput{Body: opts.Body, SubjectId: commentable.Identifier()}
|
||||
url, err := api.CommentCreate(apiClient, repo.RepoHost(), params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(opts.IO.Out, url)
|
||||
return nil
|
||||
}
|
||||
|
||||
func CommentableConfirmSubmitSurvey() (bool, error) {
|
||||
var confirm bool
|
||||
submit := &survey.Confirm{
|
||||
Message: "Submit?",
|
||||
Default: true,
|
||||
}
|
||||
err := survey.AskOne(submit, &confirm)
|
||||
return confirm, err
|
||||
}
|
||||
|
||||
func CommentableInteractiveEditSurvey(cf func() (config.Config, error), io *iostreams.IOStreams) func() (string, error) {
|
||||
return func() (string, error) {
|
||||
editorCommand, err := cmdutil.DetermineEditor(cf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if editorCommand == "" {
|
||||
editorCommand = surveyext.DefaultEditorName()
|
||||
}
|
||||
cs := io.ColorScheme()
|
||||
fmt.Fprintf(io.Out, "- %s to draft your comment in %s... ", cs.Bold("Press Enter"), cs.Bold(editorCommand))
|
||||
_ = waitForEnter(io.In)
|
||||
return surveyext.Edit(editorCommand, "*.md", "", io.In, io.Out, io.ErrOut, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func CommentableEditSurvey(cf func() (config.Config, error), io *iostreams.IOStreams) func() (string, error) {
|
||||
return func() (string, error) {
|
||||
editorCommand, err := cmdutil.DetermineEditor(cf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return surveyext.Edit(editorCommand, "*.md", "", io.In, io.Out, io.ErrOut, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForEnter(r io.Reader) error {
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Scan()
|
||||
return scanner.Err()
|
||||
}
|
||||
|
|
@ -157,3 +157,7 @@ func (e *GhEditor) Prompt(config *survey.PromptConfig) (interface{}, error) {
|
|||
}
|
||||
return e.prompt(initialValue, config)
|
||||
}
|
||||
|
||||
func DefaultEditorName() string {
|
||||
return filepath.Base(defaultEditor)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue