Merge pull request #2776 from cli/pr-comments

Comment on pull requests
This commit is contained in:
Sam 2021-01-21 09:55:52 -08:00 committed by GitHub
commit 948088a143
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 607 additions and 219 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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())

View 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
}
}

View 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"])
}),
)
}

View file

@ -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
}

View 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()
}

View file

@ -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)
}