Merge branch 'trunk' into trunk
This commit is contained in:
commit
232c024987
14 changed files with 730 additions and 15 deletions
2
.github/workflows/deployment.yml
vendored
2
.github/workflows/deployment.yml
vendored
|
|
@ -309,7 +309,7 @@ jobs:
|
|||
rpmsign --addsign dist/*.rpm
|
||||
- name: Attest release artifacts
|
||||
if: inputs.environment == 'production'
|
||||
uses: actions/attest-build-provenance@bd77c077858b8d561b7a36cbe48ef4cc642ca39d # v2.2.2
|
||||
uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
|
||||
with:
|
||||
subject-path: "dist/gh_*"
|
||||
- name: Run createrepo
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -17,7 +17,7 @@ require (
|
|||
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24
|
||||
github.com/cli/oauth v1.1.1
|
||||
github.com/cli/safeexec v1.0.1
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7
|
||||
github.com/distribution/reference v0.6.0
|
||||
|
|
|
|||
3
go.sum
3
go.sum
|
|
@ -150,8 +150,9 @@ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUo
|
|||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
|
|
|
|||
|
|
@ -76,6 +76,27 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
assert.Equal(t, []int{0, 1}, multiSelectValue)
|
||||
})
|
||||
|
||||
t.Run("MultiSelect - default values are respected by being pre-selected", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Select a number")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Don't select anything because the default should be selected.
|
||||
|
||||
// This confirms selections
|
||||
_, err = console.SendLine("0")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
multiSelectValue, err := p.MultiSelect("Select a number", []string{"2"}, []string{"1", "2", "3"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{1}, multiSelectValue)
|
||||
})
|
||||
|
||||
t.Run("Input", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package prompter
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
|
|
@ -100,6 +101,14 @@ func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, optio
|
|||
var result []int
|
||||
formOptions := make([]huh.Option[int], len(options))
|
||||
for i, o := range options {
|
||||
// If this option is in the defaults slice,
|
||||
// let's add its index to the result slice and huh
|
||||
// will treat it as a default selection.
|
||||
// TODO: does an invalid default value constitute a panic?
|
||||
if slices.Contains(defaults, o) {
|
||||
result = append(result, i)
|
||||
}
|
||||
|
||||
formOptions[i] = huh.NewOption(o, i)
|
||||
}
|
||||
|
||||
|
|
|
|||
142
pkg/cmd/accessibility/accessibility.go
Normal file
142
pkg/cmd/accessibility/accessibility.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
package accessibility
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
webURL = "https://accessibility.github.com/conformance/cli/"
|
||||
)
|
||||
|
||||
type AccessibilityOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
Browser browser.Browser
|
||||
Web bool
|
||||
}
|
||||
|
||||
func NewCmdAccessibility(f *cmdutil.Factory) *cobra.Command {
|
||||
opts := AccessibilityOptions{
|
||||
IO: f.IOStreams,
|
||||
Browser: f.Browser,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "accessibility",
|
||||
Aliases: []string{"a11y"},
|
||||
Short: "Learn about GitHub CLI's accessibility experiences",
|
||||
Long: longDescription(opts.IO),
|
||||
Hidden: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if opts.Web {
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(webURL))
|
||||
}
|
||||
return opts.Browser.Browse(webURL)
|
||||
}
|
||||
|
||||
return cmd.Help()
|
||||
},
|
||||
Example: heredoc.Doc(`
|
||||
# Open the GitHub Accessibility site in your browser
|
||||
$ gh accessibility --web
|
||||
|
||||
# Display color using customizable, 4-bit accessible colors
|
||||
$ gh config set accessible_colors enabled
|
||||
|
||||
# Use input prompts without redrawing the screen
|
||||
$ gh config set accessible_prompter enabled
|
||||
|
||||
# Disable motion-based spinners for progress indicators in favor of text
|
||||
$ gh config set spinner disabled
|
||||
`),
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open the GitHub Accessibility site in your browser")
|
||||
cmdutil.DisableAuthCheck(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func longDescription(io *iostreams.IOStreams) string {
|
||||
cs := io.ColorScheme()
|
||||
title := cs.Bold("Learn about GitHub CLI's accessibility experiences")
|
||||
color := cs.Bold("Customizable and contrasting colors")
|
||||
prompter := cs.Bold("Non-interactive user input prompting")
|
||||
spinner := cs.Bold("Text-based spinners")
|
||||
feedback := cs.Bold("Join the conversation")
|
||||
|
||||
return heredoc.Docf(`
|
||||
%[2]s
|
||||
|
||||
As the home for all developers, we want every developer to feel welcome in our
|
||||
community and be empowered to contribute to the future of global software
|
||||
development with everything GitHub has to offer including the GitHub CLI.
|
||||
|
||||
%[3]s
|
||||
|
||||
Text interfaces often use color for various purposes, but insufficient contrast
|
||||
or customizability can leave some users unable to benefit.
|
||||
|
||||
For a more accessible experience, the GitHub CLI can use color palettes
|
||||
based on terminal background appearance and limit colors to 4-bit ANSI color
|
||||
palettes, which users can customize within terminal preferences.
|
||||
|
||||
With this new experience, the GitHub CLI provides multiple options to address
|
||||
color usage:
|
||||
|
||||
1. The GitHub CLI will use 4-bit color palette for increased color contrast based
|
||||
on dark and light backgrounds including rendering Markdown based on the
|
||||
GitHub Primer design system.
|
||||
|
||||
To enable this experience, use one of the following methods:
|
||||
- Run %[1]sgh config set accessible_colors enabled%[1]s
|
||||
- Set %[1]sGH_ACCESSIBLE_COLORS=enabled%[1]s environment variable
|
||||
|
||||
2. The GitHub CLI will display issue and pull request labels' custom RGB colors
|
||||
in terminals with true color support.
|
||||
|
||||
To enable this experience, use one of the following methods:
|
||||
- Run %[1]sgh config set color_labels enabled%[1]s
|
||||
- Set %[1]sGH_COLOR_LABELS=enabled%[1]s environment variable
|
||||
|
||||
%[4]s
|
||||
|
||||
Interactive text user interfaces manipulate the terminal cursor to redraw parts
|
||||
of the screen, which can be difficult for speech synthesizers or braille displays
|
||||
to accurately detect and read.
|
||||
|
||||
For a more accessible experience, the GitHub CLI can provide a similar experience using
|
||||
non-interactive prompts for user input.
|
||||
|
||||
To enable this experience, use one of the following methods:
|
||||
- Run %[1]sgh config set accessible_prompter enabled%[1]s
|
||||
- Set %[1]sGH_ACCESSIBLE_PROMPTER=enabled%[1]s environment variable
|
||||
|
||||
%[5]s
|
||||
|
||||
Motion-based spinners communicate in-progress activity by manipulating the
|
||||
terminal cursor to create a spinning effect, which may cause discomfort to users
|
||||
with motion sensitivity or miscommunicate information to speech synthesizers.
|
||||
|
||||
For a more accessible experience, this interactivity can be disabled in favor
|
||||
of text-based progress indicators.
|
||||
|
||||
To enable this experience, use one of the following methods:
|
||||
- Run %[1]sgh config set spinner disabled%[1]s
|
||||
- Set %[1]sGH_SPINNER_DISABLED=yes%[1]s environment variable
|
||||
|
||||
%[6]s
|
||||
|
||||
We invite you to join us in improving GitHub CLI accessibility by sharing your
|
||||
feedback and ideas through GitHub Accessibility feedback channels:
|
||||
|
||||
%[7]s
|
||||
`, "`", title, color, prompter, spinner, feedback, webURL)
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -109,8 +109,6 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, _ []string) {
|
|||
return
|
||||
}
|
||||
|
||||
namePadding := 12
|
||||
|
||||
type helpEntry struct {
|
||||
Title string
|
||||
Body string
|
||||
|
|
@ -135,6 +133,12 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, _ []string) {
|
|||
helpEntries = append(helpEntries, helpEntry{"ALIASES", strings.Join(BuildAliasList(command, command.Aliases), ", ") + "\n"})
|
||||
}
|
||||
|
||||
// Statically calculated padding for non-extension commands,
|
||||
// longest is `gh accessibility` with 13 characters + 1 space.
|
||||
//
|
||||
// Should consider novel way to calculate this in the future [AF]
|
||||
namePadding := 14
|
||||
|
||||
for _, g := range GroupedCommands(command) {
|
||||
var names []string
|
||||
for _, c := range g.Commands {
|
||||
|
|
@ -148,6 +152,9 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, _ []string) {
|
|||
|
||||
if isRootCmd(command) {
|
||||
var helpTopics []string
|
||||
if c := findCommand(command, "accessibility"); c != nil {
|
||||
helpTopics = append(helpTopics, rpad(c.Name()+":", namePadding)+c.Short)
|
||||
}
|
||||
if c := findCommand(command, "actions"); c != nil {
|
||||
helpTopics = append(helpTopics, rpad(c.Name()+":", namePadding)+c.Short)
|
||||
}
|
||||
|
|
@ -183,6 +190,7 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, _ []string) {
|
|||
Use %[1]sgh <command> <subcommand> --help%[1]s for more information about a command.
|
||||
Read the manual at https://cli.github.com/manual
|
||||
Learn about exit codes using %[1]sgh help exit-codes%[1]s
|
||||
Learn about accessibility experiences using %[1]sgh help accessibility%[1]s
|
||||
`, "`")})
|
||||
|
||||
out := f.IOStreams.Out
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
accessibilityCmd "github.com/cli/cli/v2/pkg/cmd/accessibility"
|
||||
actionsCmd "github.com/cli/cli/v2/pkg/cmd/actions"
|
||||
aliasCmd "github.com/cli/cli/v2/pkg/cmd/alias"
|
||||
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
|
||||
|
|
@ -122,6 +123,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command,
|
|||
|
||||
// Child commands
|
||||
cmd.AddCommand(versionCmd.NewCmdVersion(f, version, buildDate))
|
||||
cmd.AddCommand(accessibilityCmd.NewCmdAccessibility(f))
|
||||
cmd.AddCommand(actionsCmd.NewCmdActions(f))
|
||||
cmd.AddCommand(aliasCmd.NewCmdAlias(f))
|
||||
cmd.AddCommand(authCmd.NewCmdAuth(f))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue