github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/issue/create/create.go (about) 1 package create 2 3 import ( 4 "fmt" 5 "net/http" 6 7 "github.com/MakeNowJust/heredoc" 8 "github.com/ungtb10d/cli/v2/api" 9 "github.com/ungtb10d/cli/v2/internal/browser" 10 "github.com/ungtb10d/cli/v2/internal/config" 11 "github.com/ungtb10d/cli/v2/internal/ghrepo" 12 "github.com/ungtb10d/cli/v2/internal/text" 13 "github.com/ungtb10d/cli/v2/pkg/cmd/pr/shared" 14 prShared "github.com/ungtb10d/cli/v2/pkg/cmd/pr/shared" 15 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 16 "github.com/ungtb10d/cli/v2/pkg/iostreams" 17 "github.com/spf13/cobra" 18 ) 19 20 type CreateOptions struct { 21 HttpClient func() (*http.Client, error) 22 Config func() (config.Config, error) 23 IO *iostreams.IOStreams 24 BaseRepo func() (ghrepo.Interface, error) 25 Browser browser.Browser 26 27 RootDirOverride string 28 29 HasRepoOverride bool 30 WebMode bool 31 RecoverFile string 32 33 Title string 34 Body string 35 Interactive bool 36 37 Assignees []string 38 Labels []string 39 Projects []string 40 Milestone string 41 } 42 43 func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { 44 opts := &CreateOptions{ 45 IO: f.IOStreams, 46 HttpClient: f.HttpClient, 47 Config: f.Config, 48 Browser: f.Browser, 49 } 50 51 var bodyFile string 52 53 cmd := &cobra.Command{ 54 Use: "create", 55 Short: "Create a new issue", 56 Example: heredoc.Doc(` 57 $ gh issue create --title "I found a bug" --body "Nothing works" 58 $ gh issue create --label "bug,help wanted" 59 $ gh issue create --label bug --label "help wanted" 60 $ gh issue create --assignee monalisa,hubot 61 $ gh issue create --assignee "@me" 62 $ gh issue create --project "Roadmap" 63 `), 64 Args: cmdutil.NoArgsQuoteReminder, 65 Aliases: []string{"new"}, 66 RunE: func(cmd *cobra.Command, args []string) error { 67 // support `-R, --repo` override 68 opts.BaseRepo = f.BaseRepo 69 opts.HasRepoOverride = cmd.Flags().Changed("repo") 70 71 titleProvided := cmd.Flags().Changed("title") 72 bodyProvided := cmd.Flags().Changed("body") 73 if bodyFile != "" { 74 b, err := cmdutil.ReadFile(bodyFile, opts.IO.In) 75 if err != nil { 76 return err 77 } 78 opts.Body = string(b) 79 bodyProvided = true 80 } 81 82 if !opts.IO.CanPrompt() && opts.RecoverFile != "" { 83 return cmdutil.FlagErrorf("`--recover` only supported when running interactively") 84 } 85 86 opts.Interactive = !(titleProvided && bodyProvided) 87 88 if opts.Interactive && !opts.IO.CanPrompt() { 89 return cmdutil.FlagErrorf("must provide `--title` and `--body` when not running interactively") 90 } 91 92 if runF != nil { 93 return runF(opts) 94 } 95 return createRun(opts) 96 }, 97 } 98 99 cmd.Flags().StringVarP(&opts.Title, "title", "t", "", "Supply a title. Will prompt for one otherwise.") 100 cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Supply a body. Will prompt for one otherwise.") 101 cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file` (use \"-\" to read from standard input)") 102 cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the browser to create an issue") 103 cmd.Flags().StringSliceVarP(&opts.Assignees, "assignee", "a", nil, "Assign people by their `login`. Use \"@me\" to self-assign.") 104 cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Add labels by `name`") 105 cmd.Flags().StringSliceVarP(&opts.Projects, "project", "p", nil, "Add the issue to projects by `name`") 106 cmd.Flags().StringVarP(&opts.Milestone, "milestone", "m", "", "Add the issue to a milestone by `name`") 107 cmd.Flags().StringVar(&opts.RecoverFile, "recover", "", "Recover input from a failed run of create") 108 109 return cmd 110 } 111 112 func createRun(opts *CreateOptions) (err error) { 113 httpClient, err := opts.HttpClient() 114 if err != nil { 115 return 116 } 117 apiClient := api.NewClientFromHTTP(httpClient) 118 119 baseRepo, err := opts.BaseRepo() 120 if err != nil { 121 return 122 } 123 124 isTerminal := opts.IO.IsStdoutTTY() 125 126 var milestones []string 127 if opts.Milestone != "" { 128 milestones = []string{opts.Milestone} 129 } 130 131 meReplacer := shared.NewMeReplacer(apiClient, baseRepo.RepoHost()) 132 assignees, err := meReplacer.ReplaceSlice(opts.Assignees) 133 if err != nil { 134 return err 135 } 136 137 tb := prShared.IssueMetadataState{ 138 Type: prShared.IssueMetadata, 139 Assignees: assignees, 140 Labels: opts.Labels, 141 Projects: opts.Projects, 142 Milestones: milestones, 143 Title: opts.Title, 144 Body: opts.Body, 145 } 146 147 if opts.RecoverFile != "" { 148 err = prShared.FillFromJSON(opts.IO, opts.RecoverFile, &tb) 149 if err != nil { 150 err = fmt.Errorf("failed to recover input: %w", err) 151 return 152 } 153 } 154 155 tpl := shared.NewTemplateManager(httpClient, baseRepo, opts.RootDirOverride, !opts.HasRepoOverride, false) 156 157 if opts.WebMode { 158 var openURL string 159 if opts.Title != "" || opts.Body != "" || tb.HasMetadata() { 160 openURL, err = generatePreviewURL(apiClient, baseRepo, tb) 161 if err != nil { 162 return 163 } 164 if !prShared.ValidURL(openURL) { 165 err = fmt.Errorf("cannot open in browser: maximum URL length exceeded") 166 return 167 } 168 } else if ok, _ := tpl.HasTemplates(); ok { 169 openURL = ghrepo.GenerateRepoURL(baseRepo, "issues/new/choose") 170 } else { 171 openURL = ghrepo.GenerateRepoURL(baseRepo, "issues/new") 172 } 173 if isTerminal { 174 fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL)) 175 } 176 return opts.Browser.Browse(openURL) 177 } 178 179 if isTerminal { 180 fmt.Fprintf(opts.IO.ErrOut, "\nCreating issue in %s\n\n", ghrepo.FullName(baseRepo)) 181 } 182 183 repo, err := api.GitHubRepo(apiClient, baseRepo) 184 if err != nil { 185 return 186 } 187 if !repo.HasIssuesEnabled { 188 err = fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(baseRepo)) 189 return 190 } 191 192 action := prShared.SubmitAction 193 templateNameForSubmit := "" 194 var openURL string 195 196 if opts.Interactive { 197 var editorCommand string 198 editorCommand, err = cmdutil.DetermineEditor(opts.Config) 199 if err != nil { 200 return 201 } 202 203 defer prShared.PreserveInput(opts.IO, &tb, &err)() 204 205 if opts.Title == "" { 206 err = prShared.TitleSurvey(&tb) 207 if err != nil { 208 return 209 } 210 } 211 212 if opts.Body == "" { 213 templateContent := "" 214 215 if opts.RecoverFile == "" { 216 var template shared.Template 217 template, err = tpl.Choose() 218 if err != nil { 219 return 220 } 221 222 if template != nil { 223 templateContent = string(template.Body()) 224 templateNameForSubmit = template.NameForSubmit() 225 } else { 226 templateContent = string(tpl.LegacyBody()) 227 } 228 } 229 230 err = prShared.BodySurvey(&tb, templateContent, editorCommand) 231 if err != nil { 232 return 233 } 234 } 235 236 openURL, err = generatePreviewURL(apiClient, baseRepo, tb) 237 if err != nil { 238 return 239 } 240 241 allowPreview := !tb.HasMetadata() && prShared.ValidURL(openURL) 242 action, err = prShared.ConfirmIssueSubmission(allowPreview, repo.ViewerCanTriage()) 243 if err != nil { 244 err = fmt.Errorf("unable to confirm: %w", err) 245 return 246 } 247 248 if action == prShared.MetadataAction { 249 fetcher := &prShared.MetadataFetcher{ 250 IO: opts.IO, 251 APIClient: apiClient, 252 Repo: baseRepo, 253 State: &tb, 254 } 255 err = prShared.MetadataSurvey(opts.IO, baseRepo, fetcher, &tb) 256 if err != nil { 257 return 258 } 259 260 action, err = prShared.ConfirmIssueSubmission(!tb.HasMetadata(), false) 261 if err != nil { 262 return 263 } 264 } 265 266 if action == prShared.CancelAction { 267 fmt.Fprintln(opts.IO.ErrOut, "Discarding.") 268 err = cmdutil.CancelError 269 return 270 } 271 } else { 272 if tb.Title == "" { 273 err = fmt.Errorf("title can't be blank") 274 return 275 } 276 } 277 278 if action == prShared.PreviewAction { 279 if isTerminal { 280 fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL)) 281 } 282 return opts.Browser.Browse(openURL) 283 } else if action == prShared.SubmitAction { 284 params := map[string]interface{}{ 285 "title": tb.Title, 286 "body": tb.Body, 287 } 288 if templateNameForSubmit != "" { 289 params["issueTemplate"] = templateNameForSubmit 290 } 291 292 err = prShared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb) 293 if err != nil { 294 return 295 } 296 297 var newIssue *api.Issue 298 newIssue, err = api.IssueCreate(apiClient, repo, params) 299 if err != nil { 300 return 301 } 302 303 fmt.Fprintln(opts.IO.Out, newIssue.URL) 304 } else { 305 panic("Unreachable state") 306 } 307 308 return 309 } 310 311 func generatePreviewURL(apiClient *api.Client, baseRepo ghrepo.Interface, tb shared.IssueMetadataState) (string, error) { 312 openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new") 313 return prShared.WithPrAndIssueQueryParams(apiClient, baseRepo, openURL, tb) 314 }