Comment on issues from editor

This commit is contained in:
Yuki Osaki 2020-09-29 00:10:11 +09:00 committed by Sam Coe
parent 86eb264277
commit 8ef2bb4d14
No known key found for this signature in database
GPG key ID: 8E322C20F811D086
4 changed files with 340 additions and 0 deletions

View file

@ -123,6 +123,25 @@ func IssueCreate(client *Client, repo *Repository, params map[string]interface{}
return &result.CreateIssue.Issue, nil
}
func CommentCreate(client *Client, repoHost string, params map[string]interface{}) error {
query := `
mutation CommentCreate($input: AddCommentInput!) {
addComment(input: $input) { clientMutationId }
}`
variables := map[string]interface{}{
"input": params,
}
err := client.GraphQL(repoHost, query, variables, nil)
if err != nil {
return err
}
return nil
}
func IssueStatus(client *Client, repo ghrepo.Interface, currentUsername string) (*IssuesPayload, error) {
type response struct {
Repository struct {

View file

@ -0,0 +1,202 @@
package comment
import (
"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"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/pkg/prompt"
"github.com/cli/cli/pkg/surveyext"
"github.com/spf13/cobra"
)
type CommentOptions struct {
HttpClient func() (*http.Client, error)
Config func() (config.Config, error)
IO *iostreams.IOStreams
BaseRepo func() (ghrepo.Interface, error)
Body string
SelectorArg string
Interactive bool
Action Action
}
func NewCmdComment(f *cmdutil.Factory, runF func(*CommentOptions) error) *cobra.Command {
opts := &CommentOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
Config: f.Config,
}
cmd := &cobra.Command{
Use: "comment {<number> | <url>}",
Short: "Create comments for the issue",
Example: heredoc.Doc(`
$ gh issue comment --body "I found a bug. Nothing works"
`),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// support `-R, --repo` override
opts.BaseRepo = f.BaseRepo
bodyProvided := cmd.Flags().Changed("body")
opts.Interactive = !(bodyProvided)
if len(args) > 0 {
opts.SelectorArg = args[0]
}
if runF != nil {
return runF(opts)
}
return commentRun(opts)
},
}
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Supply a body.")
return cmd
}
type Action int
const (
SubmitAction Action = iota
CancelAction
)
func titleBodySurvey(editorCommand string, issueState *CommentOptions, apiClient *api.Client, repo ghrepo.Interface, providedBody string) error {
bodyQuestion := &survey.Question{
Name: "body",
Prompt: &surveyext.GhEditor{
BlankAllowed: false,
EditorCommand: editorCommand,
Editor: &survey.Editor{
Message: "Body",
FileName: "*.md",
Default: issueState.Body,
},
},
}
var qs []*survey.Question
if providedBody == "" {
qs = append(qs, bodyQuestion)
}
err := prompt.SurveyAsk(qs, issueState)
if err != nil {
panic(fmt.Sprintf("could not prompt: %w", err))
}
confirmA, err := confirmSubmission()
if err != nil {
panic(fmt.Sprintf("unable to confirm: %w", err))
}
issueState.Action = confirmA
return nil
}
func confirmSubmission() (Action, error) {
const (
submitLabel = "Submit"
cancelLabel = "Cancel"
)
options := []string{submitLabel, cancelLabel}
confirmAnswers := struct {
Confirmation int
}{}
confirmQs := []*survey.Question{
{
Name: "confirmation",
Prompt: &survey.Select{
Message: "What's next?",
Options: options,
},
},
}
err := prompt.SurveyAsk(confirmQs, &confirmAnswers)
if err != nil {
return -1, fmt.Errorf("could not prompt: %w", err)
}
switch options[confirmAnswers.Confirmation] {
case submitLabel:
return SubmitAction, nil
case cancelLabel:
return CancelAction, nil
default:
return -1, fmt.Errorf("invalid index: %d", confirmAnswers.Confirmation)
}
}
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
}
isTerminal := opts.IO.IsStdoutTTY()
if isTerminal {
fmt.Fprintf(opts.IO.ErrOut, "\nMake a comment for %s in %s\n\n", issue.Title, ghrepo.FullName(baseRepo))
}
action := SubmitAction
body := opts.Body
if opts.Interactive {
editorCommand, err := cmdutil.DetermineEditor(opts.Config)
if err != nil {
return err
}
err = titleBodySurvey(editorCommand, opts, apiClient, baseRepo, body)
if err != nil {
return err
}
action = opts.Action
}
if action == CancelAction {
return nil
} else if action == SubmitAction {
params := map[string]interface{}{
"subjectId": issue.ID,
"body": opts.Body,
}
err = api.CommentCreate(apiClient, baseRepo.RepoHost(), params)
if err != nil {
return err
}
fmt.Fprintln(opts.IO.Out, issue.URL)
return nil
}
return fmt.Errorf("unexpected action state: %v", action)
}

View file

@ -0,0 +1,117 @@
package comment
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"reflect"
"regexp"
"testing"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/httpmock"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/test"
"github.com/google/shlex"
)
func eq(t *testing.T, got interface{}, expected interface{}) {
t.Helper()
if !reflect.DeepEqual(got, expected) {
t.Errorf("expected: %v, got: %v", expected, got)
}
}
func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) {
io, _, stdout, stderr := iostreams.Test()
io.SetStdoutTTY(isTTY)
io.SetStdinTTY(isTTY)
io.SetStderrTTY(isTTY)
factory := &cmdutil.Factory{
IOStreams: io,
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: rt}, nil
},
Config: func() (config.Config, error) {
return config.NewBlankConfig(), nil
},
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
}
cmd := NewCmdComment(factory, func(opts *CommentOptions) error {
return commentRun(opts)
})
argv, err := shlex.Split(cli)
if err != nil {
return nil, err
}
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(ioutil.Discard)
cmd.SetErr(ioutil.Discard)
_, err = cmd.ExecuteC()
return &test.CmdOut{
OutBuf: stdout,
ErrBuf: stderr,
}, err
}
func TestCommentCreate(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
"hasIssuesEnabled": true,
"issue": { "number": 96, "title": "The title of the issue"}
} } }
`))
http.StubResponse(200, bytes.NewBufferString(`{"mutationId": "THE-ID"}`))
output, err := runCommand(http, true, `13 -b "cash rules everything around me"`)
if err != nil {
t.Errorf("error running command `issue comment`: %v", err)
}
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
reqBody := struct {
Variables struct {
Input struct {
Body string
}
}
}{}
_ = json.Unmarshal(bodyBytes, &reqBody)
eq(t, reqBody.Variables.Input.Body, "cash rules everything around me")
r := regexp.MustCompile(`Make a comment for The title of the issue`)
if !r.MatchString(output.Stderr()) {
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
}
}
func TestIssue_Disabled(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
"hasIssuesEnabled": false
} } }
`))
_, err := runCommand(http, true, "13")
if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
t.Fatalf("got error: %v", err)
}
}

View file

@ -3,6 +3,7 @@ package issue
import (
"github.com/MakeNowJust/heredoc"
cmdClose "github.com/cli/cli/pkg/cmd/issue/close"
cmdComment "github.com/cli/cli/pkg/cmd/issue/comment"
cmdCreate "github.com/cli/cli/pkg/cmd/issue/create"
cmdList "github.com/cli/cli/pkg/cmd/issue/list"
cmdReopen "github.com/cli/cli/pkg/cmd/issue/reopen"
@ -40,6 +41,7 @@ func NewCmdIssue(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(cmdReopen.NewCmdReopen(f, nil))
cmd.AddCommand(cmdStatus.NewCmdStatus(f, nil))
cmd.AddCommand(cmdView.NewCmdView(f, nil))
cmd.AddCommand(cmdComment.NewCmdComment(f, nil))
return cmd
}