github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/pr/shared/commentable.go (about) 1 package shared 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 10 "github.com/AlecAivazis/survey/v2" 11 "github.com/ungtb10d/cli/v2/api" 12 "github.com/ungtb10d/cli/v2/internal/config" 13 "github.com/ungtb10d/cli/v2/internal/ghrepo" 14 "github.com/ungtb10d/cli/v2/internal/text" 15 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 16 "github.com/ungtb10d/cli/v2/pkg/iostreams" 17 "github.com/ungtb10d/cli/v2/pkg/surveyext" 18 "github.com/spf13/cobra" 19 ) 20 21 type InputType int 22 23 const ( 24 InputTypeEditor InputType = iota 25 InputTypeInline 26 InputTypeWeb 27 ) 28 29 type Commentable interface { 30 Link() string 31 Identifier() string 32 CurrentUserComments() []api.Comment 33 } 34 35 type CommentableOptions struct { 36 IO *iostreams.IOStreams 37 HttpClient func() (*http.Client, error) 38 RetrieveCommentable func() (Commentable, ghrepo.Interface, error) 39 EditSurvey func(string) (string, error) 40 InteractiveEditSurvey func(string) (string, error) 41 ConfirmSubmitSurvey func() (bool, error) 42 OpenInBrowser func(string) error 43 Interactive bool 44 InputType InputType 45 Body string 46 EditLast bool 47 Quiet bool 48 Host string 49 } 50 51 func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error { 52 inputFlags := 0 53 if cmd.Flags().Changed("body") { 54 opts.InputType = InputTypeInline 55 inputFlags++ 56 } 57 if cmd.Flags().Changed("body-file") { 58 opts.InputType = InputTypeInline 59 inputFlags++ 60 } 61 if web, _ := cmd.Flags().GetBool("web"); web { 62 opts.InputType = InputTypeWeb 63 inputFlags++ 64 } 65 if editor, _ := cmd.Flags().GetBool("editor"); editor { 66 opts.InputType = InputTypeEditor 67 inputFlags++ 68 } 69 70 if inputFlags == 0 { 71 if !opts.IO.CanPrompt() { 72 return cmdutil.FlagErrorf("flags required when not running interactively") 73 } 74 opts.Interactive = true 75 } else if inputFlags > 1 { 76 return cmdutil.FlagErrorf("specify only one of `--body`, `--body-file`, `--editor`, or `--web`") 77 } 78 79 return nil 80 } 81 82 func CommentableRun(opts *CommentableOptions) error { 83 commentable, repo, err := opts.RetrieveCommentable() 84 if err != nil { 85 return err 86 } 87 opts.Host = repo.RepoHost() 88 if opts.EditLast { 89 return updateComment(commentable, opts) 90 } 91 return createComment(commentable, opts) 92 } 93 94 func createComment(commentable Commentable, opts *CommentableOptions) error { 95 switch opts.InputType { 96 case InputTypeWeb: 97 openURL := commentable.Link() + "#issuecomment-new" 98 if opts.IO.IsStdoutTTY() && !opts.Quiet { 99 fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL)) 100 } 101 return opts.OpenInBrowser(openURL) 102 case InputTypeEditor: 103 var body string 104 var err error 105 if opts.Interactive { 106 body, err = opts.InteractiveEditSurvey("") 107 } else { 108 body, err = opts.EditSurvey("") 109 } 110 if err != nil { 111 return err 112 } 113 opts.Body = body 114 } 115 116 if opts.Interactive { 117 cont, err := opts.ConfirmSubmitSurvey() 118 if err != nil { 119 return err 120 } 121 if !cont { 122 return errors.New("Discarding...") 123 } 124 } 125 126 httpClient, err := opts.HttpClient() 127 if err != nil { 128 return err 129 } 130 131 apiClient := api.NewClientFromHTTP(httpClient) 132 params := api.CommentCreateInput{Body: opts.Body, SubjectId: commentable.Identifier()} 133 url, err := api.CommentCreate(apiClient, opts.Host, params) 134 if err != nil { 135 return err 136 } 137 138 if !opts.Quiet { 139 fmt.Fprintln(opts.IO.Out, url) 140 } 141 142 return nil 143 } 144 145 func updateComment(commentable Commentable, opts *CommentableOptions) error { 146 comments := commentable.CurrentUserComments() 147 if len(comments) == 0 { 148 return fmt.Errorf("no comments found for current user") 149 } 150 151 lastComment := &comments[len(comments)-1] 152 153 switch opts.InputType { 154 case InputTypeWeb: 155 openURL := lastComment.Link() 156 if opts.IO.IsStdoutTTY() && !opts.Quiet { 157 fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL)) 158 } 159 return opts.OpenInBrowser(openURL) 160 case InputTypeEditor: 161 var body string 162 var err error 163 initialValue := lastComment.Content() 164 if opts.Interactive { 165 body, err = opts.InteractiveEditSurvey(initialValue) 166 } else { 167 body, err = opts.EditSurvey(initialValue) 168 } 169 if err != nil { 170 return err 171 } 172 opts.Body = body 173 } 174 175 if opts.Interactive { 176 cont, err := opts.ConfirmSubmitSurvey() 177 if err != nil { 178 return err 179 } 180 if !cont { 181 return errors.New("Discarding...") 182 } 183 } 184 185 httpClient, err := opts.HttpClient() 186 if err != nil { 187 return err 188 } 189 190 apiClient := api.NewClientFromHTTP(httpClient) 191 params := api.CommentUpdateInput{Body: opts.Body, CommentId: lastComment.Identifier()} 192 url, err := api.CommentUpdate(apiClient, opts.Host, params) 193 if err != nil { 194 return err 195 } 196 197 if !opts.Quiet { 198 fmt.Fprintln(opts.IO.Out, url) 199 } 200 201 return nil 202 } 203 204 func CommentableConfirmSubmitSurvey() (bool, error) { 205 var confirm bool 206 submit := &survey.Confirm{ 207 Message: "Submit?", 208 Default: true, 209 } 210 err := survey.AskOne(submit, &confirm) 211 return confirm, err 212 } 213 214 func CommentableInteractiveEditSurvey(cf func() (config.Config, error), io *iostreams.IOStreams) func(string) (string, error) { 215 return func(initialValue string) (string, error) { 216 editorCommand, err := cmdutil.DetermineEditor(cf) 217 if err != nil { 218 return "", err 219 } 220 cs := io.ColorScheme() 221 fmt.Fprintf(io.Out, "- %s to draft your comment in %s... ", cs.Bold("Press Enter"), cs.Bold(surveyext.EditorName(editorCommand))) 222 _ = waitForEnter(io.In) 223 return surveyext.Edit(editorCommand, "*.md", initialValue, io.In, io.Out, io.ErrOut) 224 } 225 } 226 227 func CommentableEditSurvey(cf func() (config.Config, error), io *iostreams.IOStreams) func(string) (string, error) { 228 return func(initialValue string) (string, error) { 229 editorCommand, err := cmdutil.DetermineEditor(cf) 230 if err != nil { 231 return "", err 232 } 233 return surveyext.Edit(editorCommand, "*.md", initialValue, io.In, io.Out, io.ErrOut) 234 } 235 } 236 237 func waitForEnter(r io.Reader) error { 238 scanner := bufio.NewScanner(r) 239 scanner.Scan() 240 return scanner.Err() 241 }