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  }