228 lines
6.1 KiB
Go
228 lines
6.1 KiB
Go
package create
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/api"
|
|
"github.com/cli/cli/git"
|
|
"github.com/cli/cli/internal/config"
|
|
"github.com/cli/cli/internal/ghrepo"
|
|
prShared "github.com/cli/cli/pkg/cmd/pr/shared"
|
|
"github.com/cli/cli/pkg/cmdutil"
|
|
"github.com/cli/cli/pkg/githubtemplate"
|
|
"github.com/cli/cli/pkg/iostreams"
|
|
"github.com/cli/cli/utils"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type CreateOptions struct {
|
|
HttpClient func() (*http.Client, error)
|
|
Config func() (config.Config, error)
|
|
IO *iostreams.IOStreams
|
|
BaseRepo func() (ghrepo.Interface, error)
|
|
|
|
RepoOverride string
|
|
WebMode bool
|
|
|
|
Title string
|
|
TitleProvided bool
|
|
Body string
|
|
BodyProvided bool
|
|
|
|
Assignees []string
|
|
Labels []string
|
|
Projects []string
|
|
Milestone string
|
|
}
|
|
|
|
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
|
|
opts := &CreateOptions{
|
|
IO: f.IOStreams,
|
|
HttpClient: f.HttpClient,
|
|
Config: f.Config,
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "create",
|
|
Short: "Create a new issue",
|
|
Example: heredoc.Doc(`
|
|
$ gh issue create --title "I found a bug" --body "Nothing works"
|
|
$ gh issue create --label "bug,help wanted"
|
|
$ gh issue create --label bug --label "help wanted"
|
|
$ gh issue create --assignee monalisa,hubot
|
|
$ gh issue create --project "Roadmap"
|
|
`),
|
|
Args: cmdutil.NoArgsQuoteReminder,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// support `-R, --repo` override
|
|
opts.BaseRepo = f.BaseRepo
|
|
|
|
opts.TitleProvided = cmd.Flags().Changed("title")
|
|
opts.BodyProvided = cmd.Flags().Changed("body")
|
|
opts.RepoOverride, _ = cmd.Flags().GetString("repo")
|
|
|
|
if runF != nil {
|
|
return runF(opts)
|
|
}
|
|
return createRun(opts)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVarP(&opts.Title, "title", "t", "", "Supply a title. Will prompt for one otherwise.")
|
|
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Supply a body. Will prompt for one otherwise.")
|
|
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the browser to create an issue")
|
|
cmd.Flags().StringSliceVarP(&opts.Assignees, "assignee", "a", nil, "Assign people by their `login`")
|
|
cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Add labels by `name`")
|
|
cmd.Flags().StringSliceVarP(&opts.Projects, "project", "p", nil, "Add the issue to projects by `name`")
|
|
cmd.Flags().StringVarP(&opts.Milestone, "milestone", "m", "", "Add the issue to a milestone by `name`")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func createRun(opts *CreateOptions) error {
|
|
httpClient, err := opts.HttpClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
apiClient := api.NewClientFromHTTP(httpClient)
|
|
|
|
baseRepo, err := opts.BaseRepo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var nonLegacyTemplateFiles []string
|
|
if opts.RepoOverride == "" {
|
|
if rootDir, err := git.ToplevelDir(); err == nil {
|
|
// TODO: figure out how to stub this in tests
|
|
nonLegacyTemplateFiles = githubtemplate.FindNonLegacy(rootDir, "ISSUE_TEMPLATE")
|
|
}
|
|
}
|
|
|
|
isTerminal := opts.IO.IsStdoutTTY()
|
|
|
|
var milestones []string
|
|
if opts.Milestone != "" {
|
|
milestones = []string{opts.Milestone}
|
|
}
|
|
|
|
if opts.WebMode {
|
|
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
|
|
if opts.Title != "" || opts.Body != "" {
|
|
openURL, err = prShared.WithPrAndIssueQueryParams(openURL, opts.Title, opts.Body, opts.Assignees, opts.Labels, opts.Projects, milestones)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if len(nonLegacyTemplateFiles) > 1 {
|
|
openURL += "/choose"
|
|
}
|
|
if isTerminal {
|
|
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
|
}
|
|
return utils.OpenInBrowser(openURL)
|
|
}
|
|
|
|
if isTerminal {
|
|
fmt.Fprintf(opts.IO.ErrOut, "\nCreating issue in %s\n\n", ghrepo.FullName(baseRepo))
|
|
}
|
|
|
|
repo, err := api.GitHubRepo(apiClient, baseRepo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !repo.HasIssuesEnabled {
|
|
return fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(baseRepo))
|
|
}
|
|
|
|
action := prShared.SubmitAction
|
|
tb := prShared.IssueMetadataState{
|
|
Type: prShared.IssueMetadata,
|
|
Assignees: opts.Assignees,
|
|
Labels: opts.Labels,
|
|
Projects: opts.Projects,
|
|
Milestones: milestones,
|
|
}
|
|
|
|
title := opts.Title
|
|
body := opts.Body
|
|
|
|
interactive := !(opts.TitleProvided && opts.BodyProvided)
|
|
|
|
if interactive && !isTerminal {
|
|
return fmt.Errorf("must provide --title and --body when not attached to a terminal")
|
|
}
|
|
|
|
if interactive {
|
|
var legacyTemplateFile *string
|
|
if opts.RepoOverride == "" {
|
|
if rootDir, err := git.ToplevelDir(); err == nil {
|
|
// TODO: figure out how to stub this in tests
|
|
legacyTemplateFile = githubtemplate.FindLegacy(rootDir, "ISSUE_TEMPLATE")
|
|
}
|
|
}
|
|
|
|
editorCommand, err := cmdutil.DetermineEditor(opts.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = prShared.TitleBodySurvey(opts.IO, editorCommand, &tb, apiClient, baseRepo, title, body, prShared.Defaults{}, nonLegacyTemplateFiles, legacyTemplateFile, false, repo.ViewerCanTriage())
|
|
if err != nil {
|
|
return fmt.Errorf("could not collect title and/or body: %w", err)
|
|
}
|
|
|
|
action = tb.Action
|
|
|
|
if tb.Action == prShared.CancelAction {
|
|
fmt.Fprintln(opts.IO.ErrOut, "Discarding.")
|
|
|
|
return nil
|
|
}
|
|
|
|
if title == "" {
|
|
title = tb.Title
|
|
}
|
|
if body == "" {
|
|
body = tb.Body
|
|
}
|
|
} else {
|
|
if title == "" {
|
|
return fmt.Errorf("title can't be blank")
|
|
}
|
|
}
|
|
|
|
if action == prShared.PreviewAction {
|
|
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
|
|
openURL, err = prShared.WithPrAndIssueQueryParams(openURL, title, body, tb.Assignees, tb.Labels, tb.Projects, tb.Milestones)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isTerminal {
|
|
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
|
}
|
|
return utils.OpenInBrowser(openURL)
|
|
} else if action == prShared.SubmitAction {
|
|
params := map[string]interface{}{
|
|
"title": title,
|
|
"body": body,
|
|
}
|
|
|
|
err = prShared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newIssue, err := api.IssueCreate(apiClient, repo, params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintln(opts.IO.Out, newIssue.URL)
|
|
} else {
|
|
panic("Unreachable state")
|
|
}
|
|
|
|
return nil
|
|
}
|