Add --delete-last option to pr comment and issue comment (#10596)
* deletion for issues with confirmation flag * add handling for interaction case * finish implementation for issues * finish the implementation for issues * finalize the implementation for PR * fix missing --yes flag for PR * address PR comments related to feedbacks * improve CommentablePreRun for pre checks * improve confirmation prompt and truncate long comment body * address PR comments on tests * Truncate comment for confirmation prompt Signed-off-by: Babak K. Shandiz <babakks@github.com> * Improve test case descriptions Signed-off-by: Babak K. Shandiz <babakks@github.com> * Fix mock comment body Signed-off-by: Babak K. Shandiz <babakks@github.com> * Remove irrelevant prompt stub Signed-off-by: Babak K. Shandiz <babakks@github.com> * Use `opts.Interactive` as TTY indicator Signed-off-by: Babak K. Shandiz <babakks@github.com> * Fix expected `Interactive` value Signed-off-by: Babak K. Shandiz <babakks@github.com> * Polish `TestNewCmdComment` Signed-off-by: Babak K. Shandiz <babakks@github.com> --------- Signed-off-by: Babak K. Shandiz <babakks@github.com> Co-authored-by: Babak K. Shandiz <babakks@github.com>
This commit is contained in:
parent
913d13539d
commit
0a1e7a1fdc
6 changed files with 542 additions and 10 deletions
|
|
@ -44,6 +44,10 @@ type CommentCreateInput struct {
|
|||
SubjectId string
|
||||
}
|
||||
|
||||
type CommentDeleteInput struct {
|
||||
CommentId string
|
||||
}
|
||||
|
||||
type CommentUpdateInput struct {
|
||||
Body string
|
||||
CommentId string
|
||||
|
|
@ -99,6 +103,27 @@ func CommentUpdate(client *Client, repoHost string, params CommentUpdateInput) (
|
|||
return mutation.UpdateIssueComment.IssueComment.URL, nil
|
||||
}
|
||||
|
||||
func CommentDelete(client *Client, repoHost string, params CommentDeleteInput) error {
|
||||
var mutation struct {
|
||||
DeleteIssueComment struct {
|
||||
ClientMutationID string
|
||||
} `graphql:"deleteIssueComment(input: $input)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"input": githubv4.DeleteIssueCommentInput{
|
||||
ID: githubv4.ID(params.CommentId),
|
||||
},
|
||||
}
|
||||
|
||||
err := client.Mutate(repoHost, "CommentDelete", &mutation, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Comment) Identifier() string {
|
||||
return c.ID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e
|
|||
InteractiveEditSurvey: prShared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams),
|
||||
ConfirmSubmitSurvey: prShared.CommentableConfirmSubmitSurvey(f.Prompter),
|
||||
ConfirmCreateIfNoneSurvey: prShared.CommentableInteractiveCreateIfNoneSurvey(f.Prompter),
|
||||
ConfirmDeleteLastComment: prShared.CommentableConfirmDeleteLastComment(f.Prompter),
|
||||
OpenInBrowser: f.Browser.Browse,
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +64,7 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e
|
|||
}
|
||||
|
||||
fields := []string{"id", "url"}
|
||||
if opts.EditLast {
|
||||
if opts.EditLast || opts.DeleteLast {
|
||||
fields = append(fields, "comments")
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +97,9 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e
|
|||
cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file` (use \"-\" to read from standard input)")
|
||||
cmd.Flags().BoolP("editor", "e", false, "Skip prompts and open the text editor to write the body in")
|
||||
cmd.Flags().BoolP("web", "w", false, "Open the web browser to write the comment")
|
||||
cmd.Flags().BoolVar(&opts.EditLast, "edit-last", false, "Edit the last comment of the same author")
|
||||
cmd.Flags().BoolVar(&opts.EditLast, "edit-last", false, "Edit the last comment of the current user")
|
||||
cmd.Flags().BoolVar(&opts.DeleteLast, "delete-last", false, "Delete the last comment of the current user")
|
||||
cmd.Flags().BoolVar(&opts.DeleteLastConfirmed, "yes", false, "Skip the delete confirmation prompt when --delete-last is provided")
|
||||
cmd.Flags().BoolVar(&opts.CreateIfNone, "create-if-none", false, "Create a new comment if no comments are found. Can be used only with --edit-last")
|
||||
|
||||
return cmd
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package comment
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
|
@ -31,11 +32,13 @@ func TestNewCmdComment(t *testing.T) {
|
|||
stdin string
|
||||
output shared.CommentableOptions
|
||||
wantsErr bool
|
||||
isTTY bool
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
input: "",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -46,6 +49,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -56,6 +60,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -66,6 +71,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeInline,
|
||||
Body: "test",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -77,6 +83,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeInline,
|
||||
Body: "this is on standard input",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -87,6 +94,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeInline,
|
||||
Body: "a body from file",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -97,6 +105,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -107,6 +116,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -118,6 +128,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
Body: "",
|
||||
EditLast: true,
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -130,42 +141,110 @@ func TestNewCmdComment(t *testing.T) {
|
|||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete last flag non-interactive",
|
||||
input: "1 --delete-last",
|
||||
isTTY: false,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation non-interactive",
|
||||
input: "1 --delete-last --yes",
|
||||
output: shared.CommentableOptions{
|
||||
DeleteLast: true,
|
||||
DeleteLastConfirmed: true,
|
||||
},
|
||||
isTTY: false,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete last flag interactive",
|
||||
input: "1 --delete-last",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation interactive",
|
||||
input: "1 --delete-last --yes",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
DeleteLastConfirmed: true,
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation with web flag",
|
||||
input: "1 --delete-last --yes --web",
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation with editor flag",
|
||||
input: "1 --delete-last --yes --editor",
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation with body flag",
|
||||
input: "1 --delete-last --yes --body",
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "delete pre-confirmation without delete last flag",
|
||||
input: "1 --yes",
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "body and body-file flags",
|
||||
input: "1 --body 'test' --body-file 'test-file.txt'",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor and web flags",
|
||||
input: "1 --editor --web",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor and body flags",
|
||||
input: "1 --editor --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "web and body flags",
|
||||
input: "1 --web --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor, web, and body flags",
|
||||
input: "1 --editor --web --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "create-if-none flag without edit-last",
|
||||
input: "1 --create-if-none",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
|
|
@ -173,9 +252,10 @@ func TestNewCmdComment(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, stdin, _, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStdinTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
isTTY := tt.isTTY
|
||||
ios.SetStdoutTTY(isTTY)
|
||||
ios.SetStdinTTY(isTTY)
|
||||
ios.SetStderrTTY(isTTY)
|
||||
|
||||
if tt.stdin != "" {
|
||||
_, _ = stdin.WriteString(tt.stdin)
|
||||
|
|
@ -211,6 +291,8 @@ func TestNewCmdComment(t *testing.T) {
|
|||
assert.Equal(t, tt.output.Interactive, gotOpts.Interactive)
|
||||
assert.Equal(t, tt.output.InputType, gotOpts.InputType)
|
||||
assert.Equal(t, tt.output.Body, gotOpts.Body)
|
||||
assert.Equal(t, tt.output.DeleteLast, gotOpts.DeleteLast)
|
||||
assert.Equal(t, tt.output.DeleteLastConfirmed, gotOpts.DeleteLastConfirmed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -220,6 +302,7 @@ func Test_commentRun(t *testing.T) {
|
|||
name string
|
||||
input *shared.CommentableOptions
|
||||
emptyComments bool
|
||||
comments api.Comments
|
||||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
|
|
@ -255,6 +338,7 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "updating last comment with interactive editor succeeds if there are comments",
|
||||
|
|
@ -331,6 +415,7 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "creating new comment with non-interactive editor succeeds",
|
||||
|
|
@ -358,6 +443,7 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "updating last comment with non-interactive editor succeeds if there are comments",
|
||||
|
|
@ -433,6 +519,117 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment non-interactively without any comment",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
DeleteLast: true,
|
||||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively without any comment",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment non-interactively and pre-confirmed",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
DeleteLast: true,
|
||||
DeleteLastConfirmed: true,
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentDelete(t, reg)
|
||||
},
|
||||
stderr: "Comment deleted\n",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively and pre-confirmed",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
DeleteLastConfirmed: true,
|
||||
},
|
||||
comments: api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true, Body: "comment body"},
|
||||
}},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentDelete(t, reg)
|
||||
},
|
||||
stderr: "Comment deleted\n",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively and confirmed",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
|
||||
ConfirmDeleteLastComment: func(body string) (bool, error) {
|
||||
if body != "comment body" {
|
||||
return false, errors.New("unexpected comment body")
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
},
|
||||
comments: api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true, Body: "comment body"},
|
||||
}},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentDelete(t, reg)
|
||||
},
|
||||
stdout: "! Deleted comments cannot be recovered.\n",
|
||||
stderr: "Comment deleted\n",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively and confirmation declined",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
|
||||
ConfirmDeleteLastComment: func(body string) (bool, error) {
|
||||
if body != "comment body" {
|
||||
return false, errors.New("unexpected comment body")
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
},
|
||||
comments: api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true, Body: "comment body"},
|
||||
}},
|
||||
wantsErr: true,
|
||||
stdout: "deletion not confirmed",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively and confirmed with long comment body",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
|
||||
ConfirmDeleteLastComment: func(body string) (bool, error) {
|
||||
if body != "Lorem ipsum dolor sit amet, consectet lo..." {
|
||||
return false, errors.New("unexpected comment body")
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentDelete(t, reg)
|
||||
},
|
||||
comments: api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true, Body: "Lorem ipsum dolor sit amet, consectet lorem ipsum again"},
|
||||
}},
|
||||
wantsErr: false,
|
||||
stdout: "! Deleted comments cannot be recovered.\n",
|
||||
stderr: "Comment deleted\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
|
@ -458,6 +655,8 @@ func Test_commentRun(t *testing.T) {
|
|||
|
||||
if tt.emptyComments {
|
||||
comments.Nodes = []api.Comment{}
|
||||
} else if len(tt.comments.Nodes) > 0 {
|
||||
comments = tt.comments
|
||||
}
|
||||
|
||||
tt.input.RetrieveCommentable = func() (shared.Commentable, ghrepo.Interface, error) {
|
||||
|
|
@ -472,6 +671,7 @@ func Test_commentRun(t *testing.T) {
|
|||
err := shared.CommentableRun(tt.input)
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.stderr, stderr.String())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -508,3 +708,15 @@ func mockCommentUpdate(t *testing.T, reg *httpmock.Registry) {
|
|||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func mockCommentDelete(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation CommentDelete\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "deleteIssueComment": {} } }`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, "id1", inputs["id"])
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) err
|
|||
InteractiveEditSurvey: shared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams),
|
||||
ConfirmSubmitSurvey: shared.CommentableConfirmSubmitSurvey(f.Prompter),
|
||||
ConfirmCreateIfNoneSurvey: shared.CommentableInteractiveCreateIfNoneSurvey(f.Prompter),
|
||||
ConfirmDeleteLastComment: shared.CommentableConfirmDeleteLastComment(f.Prompter),
|
||||
OpenInBrowser: f.Browser.Browse,
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) err
|
|||
selector = args[0]
|
||||
}
|
||||
fields := []string{"id", "url"}
|
||||
if opts.EditLast {
|
||||
if opts.EditLast || opts.DeleteLast {
|
||||
fields = append(fields, "comments")
|
||||
}
|
||||
finder := shared.NewFinder(f)
|
||||
|
|
@ -75,7 +76,9 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) err
|
|||
cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file` (use \"-\" to read from standard input)")
|
||||
cmd.Flags().BoolP("editor", "e", false, "Skip prompts and open the text editor to write the body in")
|
||||
cmd.Flags().BoolP("web", "w", false, "Open the web browser to write the comment")
|
||||
cmd.Flags().BoolVar(&opts.EditLast, "edit-last", false, "Edit the last comment of the same author")
|
||||
cmd.Flags().BoolVar(&opts.EditLast, "edit-last", false, "Edit the last comment of the current user")
|
||||
cmd.Flags().BoolVar(&opts.DeleteLast, "delete-last", false, "Delete the last comment of the current user")
|
||||
cmd.Flags().BoolVar(&opts.DeleteLastConfirmed, "yes", false, "Skip the delete confirmation prompt when --delete-last is provided")
|
||||
cmd.Flags().BoolVar(&opts.CreateIfNone, "create-if-none", false, "Create a new comment if no comments are found. Can be used only with --edit-last")
|
||||
|
||||
return cmd
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package comment
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
|
@ -31,6 +32,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
stdin string
|
||||
output shared.CommentableOptions
|
||||
wantsErr bool
|
||||
isTTY bool
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
|
|
@ -40,12 +42,14 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "two arguments",
|
||||
input: "1 2",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -56,6 +60,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -66,6 +71,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -76,6 +82,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -86,6 +93,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeInline,
|
||||
Body: "test",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -97,6 +105,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeInline,
|
||||
Body: "this is on standard input",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -107,6 +116,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeInline,
|
||||
Body: "a body from file",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -117,6 +127,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -127,6 +138,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -138,6 +150,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
Body: "",
|
||||
EditLast: true,
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -150,42 +163,110 @@ func TestNewCmdComment(t *testing.T) {
|
|||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete last flag non-interactive",
|
||||
input: "1 --delete-last",
|
||||
isTTY: false,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation non-interactive",
|
||||
input: "1 --delete-last --yes",
|
||||
output: shared.CommentableOptions{
|
||||
DeleteLast: true,
|
||||
DeleteLastConfirmed: true,
|
||||
},
|
||||
isTTY: false,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete last flag interactive",
|
||||
input: "1 --delete-last",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation interactive",
|
||||
input: "1 --delete-last --yes",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
DeleteLastConfirmed: true,
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation with web flag",
|
||||
input: "1 --delete-last --yes --web",
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation with editor flag",
|
||||
input: "1 --delete-last --yes --editor",
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation with body flag",
|
||||
input: "1 --delete-last --yes --body",
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "delete pre-confirmation without delete last flag",
|
||||
input: "1 --yes",
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "body and body-file flags",
|
||||
input: "1 --body 'test' --body-file 'test-file.txt'",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor and web flags",
|
||||
input: "1 --editor --web",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor and body flags",
|
||||
input: "1 --editor --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "web and body flags",
|
||||
input: "1 --web --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor, web, and body flags",
|
||||
input: "1 --editor --web --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "create-if-none flag without edit-last",
|
||||
input: "1 --create-if-none",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
|
|
@ -193,9 +274,10 @@ func TestNewCmdComment(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, stdin, _, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStdinTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
isTTY := tt.isTTY
|
||||
ios.SetStdoutTTY(isTTY)
|
||||
ios.SetStdinTTY(isTTY)
|
||||
ios.SetStderrTTY(isTTY)
|
||||
|
||||
if tt.stdin != "" {
|
||||
_, _ = stdin.WriteString(tt.stdin)
|
||||
|
|
@ -231,6 +313,8 @@ func TestNewCmdComment(t *testing.T) {
|
|||
assert.Equal(t, tt.output.Interactive, gotOpts.Interactive)
|
||||
assert.Equal(t, tt.output.InputType, gotOpts.InputType)
|
||||
assert.Equal(t, tt.output.Body, gotOpts.Body)
|
||||
assert.Equal(t, tt.output.DeleteLast, gotOpts.DeleteLast)
|
||||
assert.Equal(t, tt.output.DeleteLastConfirmed, gotOpts.DeleteLastConfirmed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -240,6 +324,7 @@ func Test_commentRun(t *testing.T) {
|
|||
name string
|
||||
input *shared.CommentableOptions
|
||||
emptyComments bool
|
||||
comments api.Comments
|
||||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
|
|
@ -274,6 +359,7 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "updating last comment with interactive editor succeeds if there are comments",
|
||||
|
|
@ -350,6 +436,7 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "creating new comment with non-interactive editor succeeds",
|
||||
|
|
@ -377,6 +464,7 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "updating last comment with non-interactive editor succeeds if there are comments",
|
||||
|
|
@ -451,6 +539,117 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment non-interactively without any comment",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
DeleteLast: true,
|
||||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively without any comment",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment non-interactively and pre-confirmed",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
DeleteLast: true,
|
||||
DeleteLastConfirmed: true,
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentDelete(t, reg)
|
||||
},
|
||||
stderr: "Comment deleted\n",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively and pre-confirmed",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
DeleteLastConfirmed: true,
|
||||
},
|
||||
comments: api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true, Body: "comment body"},
|
||||
}},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentDelete(t, reg)
|
||||
},
|
||||
stderr: "Comment deleted\n",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively and confirmed",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
|
||||
ConfirmDeleteLastComment: func(body string) (bool, error) {
|
||||
if body != "comment body" {
|
||||
return false, errors.New("unexpected comment body")
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
},
|
||||
comments: api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true, Body: "comment body"},
|
||||
}},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentDelete(t, reg)
|
||||
},
|
||||
stdout: "! Deleted comments cannot be recovered.\n",
|
||||
stderr: "Comment deleted\n",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively and confirmation declined",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
|
||||
ConfirmDeleteLastComment: func(body string) (bool, error) {
|
||||
if body != "comment body" {
|
||||
return false, errors.New("unexpected comment body")
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
},
|
||||
comments: api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true, Body: "comment body"},
|
||||
}},
|
||||
wantsErr: true,
|
||||
stdout: "deletion not confirmed",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively and confirmed with long comment body",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
|
||||
ConfirmDeleteLastComment: func(body string) (bool, error) {
|
||||
if body != "Lorem ipsum dolor sit amet, consectet lo..." {
|
||||
return false, errors.New("unexpected comment body")
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentDelete(t, reg)
|
||||
},
|
||||
comments: api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true, Body: "Lorem ipsum dolor sit amet, consectet lorem ipsum again"},
|
||||
}},
|
||||
wantsErr: false,
|
||||
stdout: "! Deleted comments cannot be recovered.\n",
|
||||
stderr: "Comment deleted\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
|
@ -475,6 +674,8 @@ func Test_commentRun(t *testing.T) {
|
|||
}}
|
||||
if tt.emptyComments {
|
||||
comments.Nodes = []api.Comment{}
|
||||
} else if len(tt.comments.Nodes) > 0 {
|
||||
comments = tt.comments
|
||||
}
|
||||
|
||||
tt.input.RetrieveCommentable = func() (shared.Commentable, ghrepo.Interface, error) {
|
||||
|
|
@ -489,6 +690,7 @@ func Test_commentRun(t *testing.T) {
|
|||
err := shared.CommentableRun(tt.input)
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.stderr, stderr.String())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -524,3 +726,15 @@ func mockCommentUpdate(t *testing.T, reg *httpmock.Registry) {
|
|||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func mockCommentDelete(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation CommentDelete\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "deleteIssueComment": {} } }`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, "id1", inputs["id"])
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
)
|
||||
|
||||
var errNoUserComments = errors.New("no comments found for current user")
|
||||
var errDeleteNotConfirmed = errors.New("deletion not confirmed")
|
||||
|
||||
type InputType int
|
||||
|
||||
|
|
@ -41,11 +42,14 @@ type CommentableOptions struct {
|
|||
InteractiveEditSurvey func(string) (string, error)
|
||||
ConfirmSubmitSurvey func() (bool, error)
|
||||
ConfirmCreateIfNoneSurvey func() (bool, error)
|
||||
ConfirmDeleteLastComment func(string) (bool, error)
|
||||
OpenInBrowser func(string) error
|
||||
Interactive bool
|
||||
InputType InputType
|
||||
Body string
|
||||
EditLast bool
|
||||
DeleteLast bool
|
||||
DeleteLastConfirmed bool
|
||||
CreateIfNone bool
|
||||
Quiet bool
|
||||
Host string
|
||||
|
|
@ -74,6 +78,21 @@ func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error {
|
|||
return cmdutil.FlagErrorf("`--create-if-none` can only be used with `--edit-last`")
|
||||
}
|
||||
|
||||
if opts.DeleteLastConfirmed && !opts.DeleteLast {
|
||||
return cmdutil.FlagErrorf("`--yes` should only be used with `--delete-last`")
|
||||
}
|
||||
|
||||
if opts.DeleteLast {
|
||||
if inputFlags > 0 {
|
||||
return cmdutil.FlagErrorf("should not provide comment body when using `--delete-last`")
|
||||
}
|
||||
if opts.IO.CanPrompt() || opts.DeleteLastConfirmed {
|
||||
opts.Interactive = opts.IO.CanPrompt()
|
||||
return nil
|
||||
}
|
||||
return cmdutil.FlagErrorf("should provide `--yes` to confirm deletion in non-interactive mode")
|
||||
}
|
||||
|
||||
if inputFlags == 0 {
|
||||
if !opts.IO.CanPrompt() {
|
||||
return cmdutil.FlagErrorf("flags required when not running interactively")
|
||||
|
|
@ -92,6 +111,9 @@ func CommentableRun(opts *CommentableOptions) error {
|
|||
return err
|
||||
}
|
||||
opts.Host = repo.RepoHost()
|
||||
if opts.DeleteLast {
|
||||
return deleteComment(commentable, opts)
|
||||
}
|
||||
|
||||
// Create new comment, bail before complexities of updating the last comment
|
||||
if !opts.EditLast {
|
||||
|
|
@ -236,6 +258,53 @@ func updateComment(commentable Commentable, opts *CommentableOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func deleteComment(commentable Commentable, opts *CommentableOptions) error {
|
||||
comments := commentable.CurrentUserComments()
|
||||
if len(comments) == 0 {
|
||||
return errNoUserComments
|
||||
}
|
||||
|
||||
lastComment := comments[len(comments)-1]
|
||||
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
if opts.Interactive && !opts.DeleteLastConfirmed {
|
||||
// This is not an ideal way of truncating a random string that may
|
||||
// contain emojis or other kind of wide chars.
|
||||
truncated := lastComment.Body
|
||||
if len(lastComment.Body) > 40 {
|
||||
truncated = lastComment.Body[:40] + "..."
|
||||
}
|
||||
|
||||
fmt.Fprintf(opts.IO.Out, "%s Deleted comments cannot be recovered.\n", cs.WarningIcon())
|
||||
ok, err := opts.ConfirmDeleteLastComment(truncated)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return errDeleteNotConfirmed
|
||||
}
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
params := api.CommentDeleteInput{CommentId: lastComment.Identifier()}
|
||||
deletionErr := api.CommentDelete(apiClient, opts.Host, params)
|
||||
if deletionErr != nil {
|
||||
return deletionErr
|
||||
}
|
||||
|
||||
if !opts.Quiet {
|
||||
fmt.Fprintln(opts.IO.ErrOut, "Comment deleted")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CommentableConfirmSubmitSurvey(p Prompt) func() (bool, error) {
|
||||
return func() (bool, error) {
|
||||
return p.Confirm("Submit?", true)
|
||||
|
|
@ -271,6 +340,12 @@ func CommentableEditSurvey(cf func() (gh.Config, error), io *iostreams.IOStreams
|
|||
}
|
||||
}
|
||||
|
||||
func CommentableConfirmDeleteLastComment(p Prompt) func(string) (bool, error) {
|
||||
return func(body string) (bool, error) {
|
||||
return p.Confirm(fmt.Sprintf("Delete the comment: %q?", body), true)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForEnter(r io.Reader) error {
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Scan()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue