WIP works, probably some title/body input edge cases
This commit is contained in:
parent
6fde02df1c
commit
6671106448
6 changed files with 898 additions and 779 deletions
|
|
@ -7,12 +7,10 @@ import (
|
|||
|
||||
"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"
|
||||
|
|
@ -101,14 +99,7 @@ func createRun(opts *CreateOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
var nonLegacyTemplateFiles []string
|
||||
if opts.RootDirOverride != "" {
|
||||
nonLegacyTemplateFiles = githubtemplate.FindNonLegacy(opts.RootDirOverride, "ISSUE_TEMPLATE")
|
||||
} else if opts.RepoOverride == "" {
|
||||
if rootDir, err := git.ToplevelDir(); err == nil {
|
||||
nonLegacyTemplateFiles = githubtemplate.FindNonLegacy(rootDir, "ISSUE_TEMPLATE")
|
||||
}
|
||||
}
|
||||
templateFiles, legacyTemplate := prShared.FindTemplates(opts.RootDirOverride, "ISSUE_TEMPLATE")
|
||||
|
||||
isTerminal := opts.IO.IsStdoutTTY()
|
||||
|
||||
|
|
@ -117,14 +108,24 @@ func createRun(opts *CreateOptions) error {
|
|||
milestones = []string{opts.Milestone}
|
||||
}
|
||||
|
||||
tb := prShared.IssueMetadataState{
|
||||
Type: prShared.IssueMetadata,
|
||||
Assignees: opts.Assignees,
|
||||
Labels: opts.Labels,
|
||||
Projects: opts.Projects,
|
||||
Milestones: milestones,
|
||||
Title: opts.Title,
|
||||
Body: opts.Body,
|
||||
}
|
||||
|
||||
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)
|
||||
openURL, err = prShared.WithPrAndIssueQueryParams(openURL, tb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if len(nonLegacyTemplateFiles) > 1 {
|
||||
} else if len(templateFiles) > 1 {
|
||||
openURL += "/choose"
|
||||
}
|
||||
if isTerminal {
|
||||
|
|
@ -146,59 +147,65 @@ func createRun(opts *CreateOptions) error {
|
|||
}
|
||||
|
||||
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
|
||||
|
||||
if opts.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())
|
||||
err = prShared.TitleSurvey(&tb)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not collect title and/or body: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
action = tb.Action
|
||||
templateContent := ""
|
||||
|
||||
if tb.Action == prShared.CancelAction {
|
||||
templateContent, err = prShared.TemplateSurvey(templateFiles, legacyTemplate, tb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = prShared.BodySurvey(&tb, templateContent, editorCommand)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tb.Body == "" {
|
||||
tb.Body = templateContent
|
||||
}
|
||||
|
||||
action, err := prShared.ConfirmSubmission(!tb.HasMetadata(), repo.ViewerCanTriage())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to confirm: %w", err)
|
||||
}
|
||||
|
||||
if action == prShared.MetadataAction {
|
||||
err = prShared.MetadataSurvey(opts.IO, apiClient, baseRepo, &tb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
action, err = prShared.ConfirmSubmission(!tb.HasMetadata(), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if action == prShared.CancelAction {
|
||||
fmt.Fprintln(opts.IO.ErrOut, "Discarding.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
title = tb.Title
|
||||
}
|
||||
if body == "" {
|
||||
body = tb.Body
|
||||
}
|
||||
} else {
|
||||
if title == "" {
|
||||
if tb.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)
|
||||
openURL, err = prShared.WithPrAndIssueQueryParams(openURL, tb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -208,8 +215,8 @@ func createRun(opts *CreateOptions) error {
|
|||
return utils.OpenInBrowser(openURL)
|
||||
} else if action == prShared.SubmitAction {
|
||||
params := map[string]interface{}{
|
||||
"title": title,
|
||||
"body": body,
|
||||
"title": tb.Title,
|
||||
"body": tb.Body,
|
||||
}
|
||||
|
||||
err = prShared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import (
|
|||
"github.com/cli/cli/internal/ghrepo"
|
||||
"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/pkg/prompt"
|
||||
"github.com/cli/cli/utils"
|
||||
|
|
@ -26,6 +25,7 @@ import (
|
|||
)
|
||||
|
||||
type CreateOptions struct {
|
||||
// This struct stores user input and factory functions
|
||||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
|
|
@ -56,6 +56,20 @@ type CreateOptions struct {
|
|||
Milestone string
|
||||
}
|
||||
|
||||
type CreateContext struct {
|
||||
// This struct stores contextual data about the creation process and is for building up enough
|
||||
// data to create a pull request
|
||||
RepoContext *context.ResolvedRemotes
|
||||
BaseRepo *api.Repository
|
||||
HeadRepo ghrepo.Interface
|
||||
BaseTrackingBranch string
|
||||
BaseBranch string
|
||||
HeadBranch string
|
||||
HeadBranchLabel string
|
||||
HeadRemote *context.Remote
|
||||
IsPushEnabled bool
|
||||
}
|
||||
|
||||
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
|
||||
opts := &CreateOptions{
|
||||
IO: f.IOStreams,
|
||||
|
|
@ -92,10 +106,16 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
|
||||
opts.Interactive = !(opts.TitleProvided && opts.BodyProvided)
|
||||
|
||||
// TODO check on edge cases around title/body provision
|
||||
|
||||
if !opts.IO.CanPrompt() && !opts.WebMode && !opts.TitleProvided && !opts.Autofill {
|
||||
return &cmdutil.FlagError{Err: errors.New("--title or --fill required when not running interactively")}
|
||||
}
|
||||
|
||||
if !opts.IO.CanPrompt() {
|
||||
opts.Interactive = false
|
||||
}
|
||||
|
||||
if opts.IsDraft && opts.WebMode {
|
||||
return errors.New("the --draft flag is not supported with --web")
|
||||
}
|
||||
|
|
@ -128,148 +148,20 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
}
|
||||
|
||||
func createRun(opts *CreateOptions) (err error) {
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
remotes, err := opts.Remotes()
|
||||
ctx, err := NewCreateContext(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoContext, err := context.ResolveRemotesToRepos(remotes, client, opts.RepoOverride)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var baseRepo *api.Repository
|
||||
if br, err := repoContext.BaseRepo(opts.IO); err == nil {
|
||||
if r, ok := br.(*api.Repository); ok {
|
||||
baseRepo = r
|
||||
} else {
|
||||
// TODO: if RepoNetwork is going to be requested anyway in `repoContext.HeadRepos()`,
|
||||
// consider piggybacking on that result instead of performing a separate lookup
|
||||
baseRepo, err = api.GitHubRepo(client, br)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("could not determine base repository: %w", err)
|
||||
}
|
||||
|
||||
isPushEnabled := false
|
||||
headBranch := opts.HeadBranch
|
||||
headBranchLabel := opts.HeadBranch
|
||||
if headBranch == "" {
|
||||
headBranch, err = opts.Branch()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not determine the current branch: %w", err)
|
||||
}
|
||||
headBranchLabel = headBranch
|
||||
isPushEnabled = true
|
||||
} else if idx := strings.IndexRune(headBranch, ':'); idx >= 0 {
|
||||
headBranch = headBranch[idx+1:]
|
||||
}
|
||||
|
||||
if ucc, err := git.UncommittedChangeCount(); err == nil && ucc > 0 {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Warning: %s\n", utils.Pluralize(ucc, "uncommitted change"))
|
||||
}
|
||||
|
||||
var headRepo ghrepo.Interface
|
||||
var headRemote *context.Remote
|
||||
|
||||
if isPushEnabled {
|
||||
// determine whether the head branch is already pushed to a remote
|
||||
if pushedTo := determineTrackingBranch(remotes, headBranch); pushedTo != nil {
|
||||
isPushEnabled = false
|
||||
if r, err := remotes.FindByName(pushedTo.RemoteName); err == nil {
|
||||
headRepo = r
|
||||
headRemote = r
|
||||
headBranchLabel = pushedTo.BranchName
|
||||
if !ghrepo.IsSame(baseRepo, headRepo) {
|
||||
headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), pushedTo.BranchName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, ask the user for the head repository using info obtained from the API
|
||||
if headRepo == nil && isPushEnabled && opts.IO.CanPrompt() {
|
||||
pushableRepos, err := repoContext.HeadRepos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pushableRepos) == 0 {
|
||||
pushableRepos, err = api.RepoFindForks(client, baseRepo, 3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
currentLogin, err := api.CurrentLoginName(client, baseRepo.RepoHost())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasOwnFork := false
|
||||
var pushOptions []string
|
||||
for _, r := range pushableRepos {
|
||||
pushOptions = append(pushOptions, ghrepo.FullName(r))
|
||||
if r.RepoOwner() == currentLogin {
|
||||
hasOwnFork = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasOwnFork {
|
||||
pushOptions = append(pushOptions, "Create a fork of "+ghrepo.FullName(baseRepo))
|
||||
}
|
||||
pushOptions = append(pushOptions, "Skip pushing the branch")
|
||||
pushOptions = append(pushOptions, "Cancel")
|
||||
|
||||
var selectedOption int
|
||||
err = prompt.SurveyAskOne(&survey.Select{
|
||||
Message: fmt.Sprintf("Where should we push the '%s' branch?", headBranch),
|
||||
Options: pushOptions,
|
||||
}, &selectedOption)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if selectedOption < len(pushableRepos) {
|
||||
headRepo = pushableRepos[selectedOption]
|
||||
if !ghrepo.IsSame(baseRepo, headRepo) {
|
||||
headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), headBranch)
|
||||
}
|
||||
} else if pushOptions[selectedOption] == "Skip pushing the branch" {
|
||||
isPushEnabled = false
|
||||
} else if pushOptions[selectedOption] == "Cancel" {
|
||||
return cmdutil.SilentError
|
||||
} else {
|
||||
// "Create a fork of ..."
|
||||
if baseRepo.IsPrivate {
|
||||
return fmt.Errorf("cannot fork private repository %s", ghrepo.FullName(baseRepo))
|
||||
}
|
||||
headBranchLabel = fmt.Sprintf("%s:%s", currentLogin, headBranch)
|
||||
}
|
||||
}
|
||||
|
||||
if headRepo == nil && isPushEnabled && !opts.IO.CanPrompt() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "aborted: you must first push the current branch to a remote, or use the --head flag")
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
|
||||
baseBranch := opts.BaseBranch
|
||||
if baseBranch == "" {
|
||||
baseBranch = baseRepo.DefaultBranchRef.Name
|
||||
}
|
||||
if headBranch == baseBranch && headRepo != nil && ghrepo.IsSame(baseRepo, headRepo) {
|
||||
return fmt.Errorf("must be on a branch named differently than %q", baseBranch)
|
||||
defs, defaultsErr := computeDefaults(*ctx)
|
||||
if defaultsErr != nil && (opts.Autofill || opts.WebMode || !opts.Interactive) {
|
||||
return fmt.Errorf("could not compute title or body defaults: %w", defaultsErr)
|
||||
}
|
||||
|
||||
var milestoneTitles []string
|
||||
|
|
@ -277,64 +169,9 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
milestoneTitles = []string{opts.Milestone}
|
||||
}
|
||||
|
||||
baseTrackingBranch := baseBranch
|
||||
if baseRemote, err := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName()); err == nil {
|
||||
baseTrackingBranch = fmt.Sprintf("%s/%s", baseRemote.Name, baseBranch)
|
||||
}
|
||||
defs, defaultsErr := computeDefaults(baseTrackingBranch, headBranch)
|
||||
|
||||
title := opts.Title
|
||||
body := opts.Body
|
||||
|
||||
action := shared.SubmitAction
|
||||
if opts.WebMode {
|
||||
action = shared.PreviewAction
|
||||
if (title == "" || body == "") && defaultsErr != nil {
|
||||
return fmt.Errorf("could not compute title or body defaults: %w", defaultsErr)
|
||||
}
|
||||
} else if opts.Autofill {
|
||||
if defaultsErr != nil && !(opts.TitleProvided || opts.BodyProvided) {
|
||||
return fmt.Errorf("could not compute title or body defaults: %w", defaultsErr)
|
||||
}
|
||||
if !opts.TitleProvided {
|
||||
title = defs.Title
|
||||
}
|
||||
if !opts.BodyProvided {
|
||||
body = defs.Body
|
||||
}
|
||||
}
|
||||
|
||||
if !opts.WebMode {
|
||||
existingPR, err := api.PullRequestForBranch(client, baseRepo, baseBranch, headBranchLabel, []string{"OPEN"})
|
||||
var notFound *api.NotFoundError
|
||||
if err != nil && !errors.As(err, ¬Found) {
|
||||
return fmt.Errorf("error checking for existing pull request: %w", err)
|
||||
}
|
||||
if err == nil {
|
||||
return fmt.Errorf("a pull request for branch %q into branch %q already exists:\n%s", headBranchLabel, baseBranch, existingPR.URL)
|
||||
}
|
||||
}
|
||||
|
||||
isTerminal := opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY()
|
||||
|
||||
if !opts.WebMode && !opts.Autofill {
|
||||
message := "\nCreating pull request for %s into %s in %s\n\n"
|
||||
if opts.IsDraft {
|
||||
message = "\nCreating draft pull request for %s into %s in %s\n\n"
|
||||
}
|
||||
|
||||
if isTerminal {
|
||||
fmt.Fprintf(opts.IO.ErrOut, message,
|
||||
cs.Cyan(headBranchLabel),
|
||||
cs.Cyan(baseBranch),
|
||||
ghrepo.FullName(baseRepo))
|
||||
if (title == "" || body == "") && defaultsErr != nil {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s warning: could not compute title or body defaults: %s\n", cs.Yellow("!"), defaultsErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tb := shared.IssueMetadataState{
|
||||
state := shared.IssueMetadataState{
|
||||
Title: defs.Title,
|
||||
Body: defs.Body,
|
||||
Type: shared.PRMetadata,
|
||||
Reviewers: opts.Reviewers,
|
||||
Assignees: opts.Assignees,
|
||||
|
|
@ -343,162 +180,126 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
Milestones: milestoneTitles,
|
||||
}
|
||||
|
||||
if !opts.WebMode && !opts.Autofill && opts.Interactive {
|
||||
var nonLegacyTemplateFiles []string
|
||||
var legacyTemplateFile *string
|
||||
if opts.TitleProvided {
|
||||
state.Title = opts.Title
|
||||
}
|
||||
|
||||
if opts.RootDirOverride != "" {
|
||||
nonLegacyTemplateFiles = githubtemplate.FindNonLegacy(opts.RootDirOverride, "PULL_REQUEST_TEMPLATE")
|
||||
legacyTemplateFile = githubtemplate.FindLegacy(opts.RootDirOverride, "PULL_REQUEST_TEMPLATE")
|
||||
} else if rootDir, err := git.ToplevelDir(); err == nil {
|
||||
nonLegacyTemplateFiles = githubtemplate.FindNonLegacy(rootDir, "PULL_REQUEST_TEMPLATE")
|
||||
legacyTemplateFile = githubtemplate.FindLegacy(rootDir, "PULL_REQUEST_TEMPLATE")
|
||||
if opts.BodyProvided {
|
||||
state.Body = opts.Body
|
||||
}
|
||||
|
||||
if opts.WebMode {
|
||||
err := handlePush(*opts, *ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return previewPR(*opts, *ctx, state)
|
||||
}
|
||||
|
||||
editorCommand, err := cmdutil.DetermineEditor(opts.Config)
|
||||
if opts.Autofill || !opts.Interactive {
|
||||
return submitPR(*opts, *ctx, state)
|
||||
}
|
||||
|
||||
existingPR, err := api.PullRequestForBranch(
|
||||
client, ctx.BaseRepo, ctx.BaseBranch, ctx.HeadBranchLabel, []string{"OPEN"})
|
||||
var notFound *api.NotFoundError
|
||||
if err != nil && !errors.As(err, ¬Found) {
|
||||
return fmt.Errorf("error checking for existing pull request: %w", err)
|
||||
}
|
||||
if err == nil {
|
||||
return fmt.Errorf("a pull request for branch %q into branch %q already exists:\n%s",
|
||||
ctx.HeadBranchLabel, ctx.BaseBranch, existingPR.URL)
|
||||
}
|
||||
|
||||
message := "\nCreating pull request for %s into %s in %s\n\n"
|
||||
if opts.IsDraft {
|
||||
message = "\nCreating draft pull request for %s into %s in %s\n\n"
|
||||
}
|
||||
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
fmt.Fprintf(opts.IO.ErrOut, message,
|
||||
cs.Cyan(ctx.HeadBranchLabel),
|
||||
cs.Cyan(ctx.BaseBranch),
|
||||
ghrepo.FullName(ctx.BaseRepo))
|
||||
if (state.Title == "" || state.Body == "") && defaultsErr != nil {
|
||||
fmt.Fprintf(opts.IO.ErrOut,
|
||||
"%s warning: could not compute title or body defaults: %s\n", cs.Yellow("!"), defaultsErr)
|
||||
}
|
||||
|
||||
if !opts.TitleProvided {
|
||||
err = shared.TitleSurvey(&state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
editorCommand, err := cmdutil.DetermineEditor(opts.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
templateContent := ""
|
||||
if !opts.BodyProvided {
|
||||
templateFiles, legacyTemplate := shared.FindTemplates(opts.RootDirOverride, "PULL_REQUEST_TEMPLATE")
|
||||
|
||||
templateContent, err = shared.TemplateSurvey(templateFiles, legacyTemplate, state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = shared.TitleBodySurvey(opts.IO, editorCommand, &tb, client, baseRepo, title, body, defs, nonLegacyTemplateFiles, legacyTemplateFile, true, baseRepo.ViewerCanTriage())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not collect title and/or body: %w", err)
|
||||
}
|
||||
|
||||
action = tb.Action
|
||||
|
||||
if action == shared.CancelAction {
|
||||
fmt.Fprintln(opts.IO.ErrOut, "Discarding.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
title = tb.Title
|
||||
}
|
||||
if body == "" {
|
||||
body = tb.Body
|
||||
}
|
||||
}
|
||||
|
||||
if action == shared.SubmitAction && title == "" {
|
||||
return errors.New("pull request title must not be blank")
|
||||
}
|
||||
|
||||
didForkRepo := false
|
||||
// if a head repository could not be determined so far, automatically create
|
||||
// one by forking the base repository
|
||||
if headRepo == nil && isPushEnabled {
|
||||
headRepo, err = api.ForkRepo(client, baseRepo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error forking repo: %w", err)
|
||||
}
|
||||
didForkRepo = true
|
||||
}
|
||||
|
||||
if headRemote == nil && headRepo != nil {
|
||||
headRemote, _ = repoContext.RemoteForRepo(headRepo)
|
||||
}
|
||||
|
||||
// There are two cases when an existing remote for the head repo will be
|
||||
// missing:
|
||||
// 1. the head repo was just created by auto-forking;
|
||||
// 2. an existing fork was discovered by querying the API.
|
||||
//
|
||||
// In either case, we want to add the head repo as a new git remote so we
|
||||
// can push to it.
|
||||
if headRemote == nil && isPushEnabled {
|
||||
cfg, err := opts.Config()
|
||||
err = shared.BodySurvey(&state, templateContent, editorCommand)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cloneProtocol, _ := cfg.Get(headRepo.RepoHost(), "git_protocol")
|
||||
|
||||
headRepoURL := ghrepo.FormatRemoteURL(headRepo, cloneProtocol)
|
||||
|
||||
// TODO: prevent clashes with another remote of a same name
|
||||
gitRemote, err := git.AddRemote("fork", headRepoURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding remote: %w", err)
|
||||
}
|
||||
headRemote = &context.Remote{
|
||||
Remote: gitRemote,
|
||||
Repo: headRepo,
|
||||
if state.Body == "" {
|
||||
state.Body = templateContent
|
||||
}
|
||||
}
|
||||
|
||||
// automatically push the branch if it hasn't been pushed anywhere yet
|
||||
if isPushEnabled {
|
||||
pushBranch := func() error {
|
||||
pushTries := 0
|
||||
maxPushTries := 3
|
||||
for {
|
||||
r := NewRegexpWriter(opts.IO.ErrOut, gitPushRegexp, "")
|
||||
defer r.Flush()
|
||||
cmdErr := r
|
||||
cmdOut := opts.IO.Out
|
||||
if err := git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch), cmdOut, cmdErr); err != nil {
|
||||
if didForkRepo && pushTries < maxPushTries {
|
||||
pushTries++
|
||||
// first wait 2 seconds after forking, then 4s, then 6s
|
||||
waitSeconds := 2 * pushTries
|
||||
fmt.Fprintf(opts.IO.ErrOut, "waiting %s before retrying...\n", utils.Pluralize(waitSeconds, "second"))
|
||||
time.Sleep(time.Duration(waitSeconds) * time.Second)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
return nil
|
||||
}
|
||||
allowMetadata := ctx.BaseRepo.ViewerCanTriage()
|
||||
action, err := shared.ConfirmSubmission(!state.HasMetadata(), allowMetadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to confirm: %w", err)
|
||||
}
|
||||
|
||||
err := pushBranch()
|
||||
if action == shared.MetadataAction {
|
||||
err = shared.MetadataSurvey(opts.IO, client, ctx.BaseRepo, &state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
action, err = shared.ConfirmSubmission(!state.HasMetadata(), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if action == shared.CancelAction {
|
||||
fmt.Fprintln(opts.IO.ErrOut, "Discarding.")
|
||||
return nil
|
||||
}
|
||||
|
||||
err = handlePush(*opts, *ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if action == shared.PreviewAction {
|
||||
return previewPR(*opts, *ctx, state)
|
||||
}
|
||||
|
||||
if action == shared.SubmitAction {
|
||||
params := map[string]interface{}{
|
||||
"title": title,
|
||||
"body": body,
|
||||
"draft": opts.IsDraft,
|
||||
"baseRefName": baseBranch,
|
||||
"headRefName": headBranchLabel,
|
||||
}
|
||||
|
||||
err = shared.AddMetadataToIssueParams(client, baseRepo, params, &tb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr, err := api.CreatePullRequest(client, baseRepo, params)
|
||||
if pr != nil {
|
||||
fmt.Fprintln(opts.IO.Out, pr.URL)
|
||||
}
|
||||
if err != nil {
|
||||
if pr != nil {
|
||||
return fmt.Errorf("pull request update failed: %w", err)
|
||||
}
|
||||
return fmt.Errorf("pull request create failed: %w", err)
|
||||
}
|
||||
} else if action == shared.PreviewAction {
|
||||
openURL, err := generateCompareURL(baseRepo, baseBranch, headBranchLabel, 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 {
|
||||
panic("Unreachable state")
|
||||
return submitPR(*opts, *ctx, state)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errors.New("expected to cancel, preview, or submit")
|
||||
}
|
||||
|
||||
func computeDefaults(baseRef, headRef string) (shared.Defaults, error) {
|
||||
func computeDefaults(createCtx CreateContext) (shared.Defaults, error) {
|
||||
baseRef := createCtx.BaseTrackingBranch
|
||||
headRef := createCtx.HeadBranch
|
||||
out := shared.Defaults{}
|
||||
|
||||
commits, err := git.Commits(baseRef, headRef)
|
||||
|
|
@ -567,9 +368,311 @@ func determineTrackingBranch(remotes context.Remotes, headBranch string) *git.Tr
|
|||
return nil
|
||||
}
|
||||
|
||||
func generateCompareURL(r ghrepo.Interface, base, head, title, body string, assignees, labels, projects []string, milestones []string) (string, error) {
|
||||
u := ghrepo.GenerateRepoURL(r, "compare/%s...%s?expand=1", url.QueryEscape(base), url.QueryEscape(head))
|
||||
url, err := shared.WithPrAndIssueQueryParams(u, title, body, assignees, labels, projects, milestones)
|
||||
func NewCreateContext(opts *CreateOptions) (*CreateContext, error) {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
remotes, err := opts.Remotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoContext, err := context.ResolveRemotesToRepos(remotes, client, opts.RepoOverride)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var baseRepo *api.Repository
|
||||
if br, err := repoContext.BaseRepo(opts.IO); err == nil {
|
||||
if r, ok := br.(*api.Repository); ok {
|
||||
baseRepo = r
|
||||
} else {
|
||||
// TODO: if RepoNetwork is going to be requested anyway in `repoContext.HeadRepos()`,
|
||||
// consider piggybacking on that result instead of performing a separate lookup
|
||||
baseRepo, err = api.GitHubRepo(client, br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("could not determine base repository: %w", err)
|
||||
}
|
||||
|
||||
isPushEnabled := false
|
||||
headBranch := opts.HeadBranch
|
||||
headBranchLabel := opts.HeadBranch
|
||||
if headBranch == "" {
|
||||
headBranch, err = opts.Branch()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not determine the current branch: %w", err)
|
||||
}
|
||||
headBranchLabel = headBranch
|
||||
isPushEnabled = true
|
||||
} else if idx := strings.IndexRune(headBranch, ':'); idx >= 0 {
|
||||
headBranch = headBranch[idx+1:]
|
||||
}
|
||||
|
||||
if ucc, err := git.UncommittedChangeCount(); err == nil && ucc > 0 {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Warning: %s\n", utils.Pluralize(ucc, "uncommitted change"))
|
||||
}
|
||||
|
||||
var headRepo ghrepo.Interface
|
||||
var headRemote *context.Remote
|
||||
|
||||
if isPushEnabled {
|
||||
// determine whether the head branch is already pushed to a remote
|
||||
if pushedTo := determineTrackingBranch(remotes, headBranch); pushedTo != nil {
|
||||
isPushEnabled = false
|
||||
if r, err := remotes.FindByName(pushedTo.RemoteName); err == nil {
|
||||
headRepo = r
|
||||
headRemote = r
|
||||
headBranchLabel = pushedTo.BranchName
|
||||
if !ghrepo.IsSame(baseRepo, headRepo) {
|
||||
headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), pushedTo.BranchName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, ask the user for the head repository using info obtained from the API
|
||||
if headRepo == nil && isPushEnabled && opts.IO.CanPrompt() {
|
||||
pushableRepos, err := repoContext.HeadRepos()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pushableRepos) == 0 {
|
||||
pushableRepos, err = api.RepoFindForks(client, baseRepo, 3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
currentLogin, err := api.CurrentLoginName(client, baseRepo.RepoHost())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasOwnFork := false
|
||||
var pushOptions []string
|
||||
for _, r := range pushableRepos {
|
||||
pushOptions = append(pushOptions, ghrepo.FullName(r))
|
||||
if r.RepoOwner() == currentLogin {
|
||||
hasOwnFork = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasOwnFork {
|
||||
pushOptions = append(pushOptions, "Create a fork of "+ghrepo.FullName(baseRepo))
|
||||
}
|
||||
pushOptions = append(pushOptions, "Skip pushing the branch")
|
||||
pushOptions = append(pushOptions, "Cancel")
|
||||
|
||||
var selectedOption int
|
||||
err = prompt.SurveyAskOne(&survey.Select{
|
||||
Message: fmt.Sprintf("Where should we push the '%s' branch?", headBranch),
|
||||
Options: pushOptions,
|
||||
}, &selectedOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if selectedOption < len(pushableRepos) {
|
||||
headRepo = pushableRepos[selectedOption]
|
||||
if !ghrepo.IsSame(baseRepo, headRepo) {
|
||||
headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), headBranch)
|
||||
}
|
||||
} else if pushOptions[selectedOption] == "Skip pushing the branch" {
|
||||
isPushEnabled = false
|
||||
} else if pushOptions[selectedOption] == "Cancel" {
|
||||
return nil, cmdutil.SilentError
|
||||
} else {
|
||||
// "Create a fork of ..."
|
||||
if baseRepo.IsPrivate {
|
||||
return nil, fmt.Errorf("cannot fork private repository %s", ghrepo.FullName(baseRepo))
|
||||
}
|
||||
headBranchLabel = fmt.Sprintf("%s:%s", currentLogin, headBranch)
|
||||
}
|
||||
}
|
||||
|
||||
if headRepo == nil && isPushEnabled && !opts.IO.CanPrompt() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "aborted: you must first push the current branch to a remote, or use the --head flag")
|
||||
return nil, cmdutil.SilentError
|
||||
}
|
||||
|
||||
baseBranch := opts.BaseBranch
|
||||
if baseBranch == "" {
|
||||
baseBranch = baseRepo.DefaultBranchRef.Name
|
||||
}
|
||||
if headBranch == baseBranch && headRepo != nil && ghrepo.IsSame(baseRepo, headRepo) {
|
||||
return nil, fmt.Errorf("must be on a branch named differently than %q", baseBranch)
|
||||
}
|
||||
|
||||
baseTrackingBranch := baseBranch
|
||||
if baseRemote, err := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName()); err == nil {
|
||||
baseTrackingBranch = fmt.Sprintf("%s/%s", baseRemote.Name, baseBranch)
|
||||
}
|
||||
|
||||
return &CreateContext{
|
||||
BaseRepo: baseRepo,
|
||||
HeadRepo: headRepo,
|
||||
BaseBranch: baseBranch,
|
||||
BaseTrackingBranch: baseTrackingBranch,
|
||||
HeadBranch: headBranch,
|
||||
HeadBranchLabel: headBranchLabel,
|
||||
HeadRemote: headRemote,
|
||||
IsPushEnabled: isPushEnabled,
|
||||
RepoContext: repoContext,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func submitPR(opts CreateOptions, createCtx CreateContext, state shared.IssueMetadataState) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
params := map[string]interface{}{
|
||||
"title": state.Title,
|
||||
"body": state.Body,
|
||||
"draft": opts.IsDraft,
|
||||
"baseRefName": createCtx.BaseBranch,
|
||||
"headRefName": createCtx.HeadBranchLabel,
|
||||
}
|
||||
|
||||
if params["title"] == "" {
|
||||
return errors.New("pull request title must not be blank")
|
||||
}
|
||||
|
||||
err = shared.AddMetadataToIssueParams(client, createCtx.BaseRepo, params, &state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr, err := api.CreatePullRequest(client, createCtx.BaseRepo, params)
|
||||
if pr != nil {
|
||||
fmt.Fprintln(opts.IO.Out, pr.URL)
|
||||
}
|
||||
if err != nil {
|
||||
if pr != nil {
|
||||
return fmt.Errorf("pull request update failed: %w", err)
|
||||
}
|
||||
return fmt.Errorf("pull request create failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func previewPR(opts CreateOptions, createCtx CreateContext, state shared.IssueMetadataState) error {
|
||||
openURL, err := generateCompareURL(createCtx, state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
||||
}
|
||||
return utils.OpenInBrowser(openURL)
|
||||
|
||||
}
|
||||
|
||||
func handlePush(opts CreateOptions, ctx CreateContext) error {
|
||||
didForkRepo := false
|
||||
headRepo := ctx.HeadRepo
|
||||
headRemote := ctx.HeadRemote
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
// if a head repository could not be determined so far, automatically create
|
||||
// one by forking the base repository
|
||||
if headRepo == nil && ctx.IsPushEnabled {
|
||||
headRepo, err = api.ForkRepo(client, ctx.BaseRepo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error forking repo: %w", err)
|
||||
}
|
||||
didForkRepo = true
|
||||
}
|
||||
|
||||
if headRemote == nil && headRepo != nil {
|
||||
headRemote, _ = ctx.RepoContext.RemoteForRepo(headRepo)
|
||||
}
|
||||
|
||||
// There are two cases when an existing remote for the head repo will be
|
||||
// missing:
|
||||
// 1. the head repo was just created by auto-forking;
|
||||
// 2. an existing fork was discovered by querying the API.
|
||||
//
|
||||
// In either case, we want to add the head repo as a new git remote so we
|
||||
// can push to it.
|
||||
if headRemote == nil && ctx.IsPushEnabled {
|
||||
cfg, err := opts.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cloneProtocol, _ := cfg.Get(headRepo.RepoHost(), "git_protocol")
|
||||
|
||||
headRepoURL := ghrepo.FormatRemoteURL(headRepo, cloneProtocol)
|
||||
|
||||
// TODO: prevent clashes with another remote of a same name
|
||||
gitRemote, err := git.AddRemote("fork", headRepoURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding remote: %w", err)
|
||||
}
|
||||
headRemote = &context.Remote{
|
||||
Remote: gitRemote,
|
||||
Repo: headRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// automatically push the branch if it hasn't been pushed anywhere yet
|
||||
if ctx.IsPushEnabled {
|
||||
pushBranch := func() error {
|
||||
pushTries := 0
|
||||
maxPushTries := 3
|
||||
for {
|
||||
r := NewRegexpWriter(opts.IO.ErrOut, gitPushRegexp, "")
|
||||
defer r.Flush()
|
||||
cmdErr := r
|
||||
cmdOut := opts.IO.Out
|
||||
if err := git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", ctx.HeadBranch), cmdOut, cmdErr); err != nil {
|
||||
if didForkRepo && pushTries < maxPushTries {
|
||||
pushTries++
|
||||
// first wait 2 seconds after forking, then 4s, then 6s
|
||||
waitSeconds := 2 * pushTries
|
||||
fmt.Fprintf(opts.IO.ErrOut, "waiting %s before retrying...\n", utils.Pluralize(waitSeconds, "second"))
|
||||
time.Sleep(time.Duration(waitSeconds) * time.Second)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err := pushBranch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateCompareURL(createCtx CreateContext, state shared.IssueMetadataState) (string, error) {
|
||||
u := ghrepo.GenerateRepoURL(
|
||||
createCtx.BaseRepo,
|
||||
"compare/%s...%s?expand=1",
|
||||
url.QueryEscape(createCtx.BaseBranch), url.QueryEscape(createCtx.HeadBranch))
|
||||
url, err := shared.WithPrAndIssueQueryParams(u, state)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,29 +9,29 @@ import (
|
|||
"github.com/cli/cli/internal/ghrepo"
|
||||
)
|
||||
|
||||
func WithPrAndIssueQueryParams(baseURL, title, body string, assignees, labels, projects []string, milestones []string) (string, error) {
|
||||
func WithPrAndIssueQueryParams(baseURL string, state IssueMetadataState) (string, error) {
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
q := u.Query()
|
||||
if title != "" {
|
||||
q.Set("title", title)
|
||||
if state.Title != "" {
|
||||
q.Set("title", state.Title)
|
||||
}
|
||||
if body != "" {
|
||||
q.Set("body", body)
|
||||
if state.Body != "" {
|
||||
q.Set("body", state.Body)
|
||||
}
|
||||
if len(assignees) > 0 {
|
||||
q.Set("assignees", strings.Join(assignees, ","))
|
||||
if len(state.Assignees) > 0 {
|
||||
q.Set("assignees", strings.Join(state.Assignees, ","))
|
||||
}
|
||||
if len(labels) > 0 {
|
||||
q.Set("labels", strings.Join(labels, ","))
|
||||
if len(state.Labels) > 0 {
|
||||
q.Set("labels", strings.Join(state.Labels, ","))
|
||||
}
|
||||
if len(projects) > 0 {
|
||||
q.Set("projects", strings.Join(projects, ","))
|
||||
if len(state.Projects) > 0 {
|
||||
q.Set("projects", strings.Join(state.Projects, ","))
|
||||
}
|
||||
if len(milestones) > 0 {
|
||||
q.Set("milestone", milestones[0])
|
||||
if len(state.Milestones) > 0 {
|
||||
q.Set("milestone", state.Milestones[0])
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String(), nil
|
||||
|
|
|
|||
404
pkg/cmd/pr/shared/survey.go
Normal file
404
pkg/cmd/pr/shared/survey.go
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/githubtemplate"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/prompt"
|
||||
"github.com/cli/cli/pkg/surveyext"
|
||||
"github.com/cli/cli/utils"
|
||||
)
|
||||
|
||||
type Defaults struct {
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
|
||||
type Action int
|
||||
type metadataStateType int
|
||||
|
||||
const (
|
||||
IssueMetadata metadataStateType = iota
|
||||
PRMetadata
|
||||
)
|
||||
|
||||
type IssueMetadataState struct {
|
||||
Type metadataStateType
|
||||
|
||||
Body string
|
||||
Title string
|
||||
|
||||
Metadata []string
|
||||
Reviewers []string
|
||||
Assignees []string
|
||||
Labels []string
|
||||
Projects []string
|
||||
Milestones []string
|
||||
|
||||
MetadataResult *api.RepoMetadataResult
|
||||
}
|
||||
|
||||
func (tb *IssueMetadataState) HasMetadata() bool {
|
||||
return len(tb.Reviewers) > 0 ||
|
||||
len(tb.Assignees) > 0 ||
|
||||
len(tb.Labels) > 0 ||
|
||||
len(tb.Projects) > 0 ||
|
||||
len(tb.Milestones) > 0
|
||||
}
|
||||
|
||||
const (
|
||||
SubmitAction Action = iota
|
||||
PreviewAction
|
||||
CancelAction
|
||||
MetadataAction
|
||||
|
||||
noMilestone = "(none)"
|
||||
)
|
||||
|
||||
func ConfirmSubmission(allowPreview bool, allowMetadata bool) (Action, error) {
|
||||
const (
|
||||
submitLabel = "Submit"
|
||||
previewLabel = "Continue in browser"
|
||||
metadataLabel = "Add metadata"
|
||||
cancelLabel = "Cancel"
|
||||
)
|
||||
|
||||
options := []string{submitLabel}
|
||||
if allowPreview {
|
||||
options = append(options, previewLabel)
|
||||
}
|
||||
if allowMetadata {
|
||||
options = append(options, metadataLabel)
|
||||
}
|
||||
options = append(options, cancelLabel)
|
||||
|
||||
confirmAnswers := struct {
|
||||
Confirmation int
|
||||
}{}
|
||||
confirmQs := []*survey.Question{
|
||||
{
|
||||
Name: "confirmation",
|
||||
Prompt: &survey.Select{
|
||||
Message: "What's next?",
|
||||
Options: options,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := prompt.SurveyAsk(confirmQs, &confirmAnswers)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
switch options[confirmAnswers.Confirmation] {
|
||||
case submitLabel:
|
||||
return SubmitAction, nil
|
||||
case previewLabel:
|
||||
return PreviewAction, nil
|
||||
case metadataLabel:
|
||||
return MetadataAction, nil
|
||||
case cancelLabel:
|
||||
return CancelAction, nil
|
||||
default:
|
||||
return -1, fmt.Errorf("invalid index: %d", confirmAnswers.Confirmation)
|
||||
}
|
||||
}
|
||||
|
||||
func TemplateSurvey(templateFiles []string, legacyTemplate string, state IssueMetadataState) (templateContent string, err error) {
|
||||
if len(templateFiles) == 0 && legacyTemplate == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if len(templateFiles) > 0 {
|
||||
templateContent, err = selectTemplate(templateFiles, legacyTemplate, state.Type)
|
||||
} else {
|
||||
templateContent = string(githubtemplate.ExtractContents(legacyTemplate))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func selectTemplate(nonLegacyTemplatePaths []string, legacyTemplatePath string, metadataType metadataStateType) (string, error) {
|
||||
templateResponse := struct {
|
||||
Index int
|
||||
}{}
|
||||
templateNames := make([]string, 0, len(nonLegacyTemplatePaths))
|
||||
for _, p := range nonLegacyTemplatePaths {
|
||||
templateNames = append(templateNames, githubtemplate.ExtractName(p))
|
||||
}
|
||||
if metadataType == IssueMetadata {
|
||||
templateNames = append(templateNames, "Open a blank issue")
|
||||
} else if metadataType == PRMetadata {
|
||||
templateNames = append(templateNames, "Open a blank pull request")
|
||||
}
|
||||
|
||||
selectQs := []*survey.Question{
|
||||
{
|
||||
Name: "index",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Choose a template",
|
||||
Options: templateNames,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := prompt.SurveyAsk(selectQs, &templateResponse); err != nil {
|
||||
return "", fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
if templateResponse.Index == len(nonLegacyTemplatePaths) { // the user has selected the blank template
|
||||
if legacyTemplatePath != "" {
|
||||
templateContents := githubtemplate.ExtractContents(legacyTemplatePath)
|
||||
return string(templateContents), nil
|
||||
} else {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
templateContents := githubtemplate.ExtractContents(nonLegacyTemplatePaths[templateResponse.Index])
|
||||
return string(templateContents), nil
|
||||
}
|
||||
|
||||
func BodySurvey(state *IssueMetadataState, templateContent, editorCommand string) error {
|
||||
if templateContent != "" {
|
||||
if state.Body != "" {
|
||||
// prevent excessive newlines between default body and template
|
||||
state.Body = strings.TrimRight(state.Body, "\n")
|
||||
state.Body += "\n\n"
|
||||
}
|
||||
state.Body += templateContent
|
||||
}
|
||||
|
||||
p := &surveyext.GhEditor{
|
||||
BlankAllowed: true,
|
||||
EditorCommand: editorCommand,
|
||||
Editor: &survey.Editor{
|
||||
Message: "Body",
|
||||
FileName: "*.md",
|
||||
Default: state.Body,
|
||||
HideDefault: true,
|
||||
AppendDefault: true,
|
||||
},
|
||||
}
|
||||
|
||||
body := ""
|
||||
|
||||
err := prompt.SurveyAskOne(p, &body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state.Body = body
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TitleSurvey(state *IssueMetadataState) error {
|
||||
p := &survey.Input{
|
||||
Message: "Title",
|
||||
Default: state.Title,
|
||||
}
|
||||
|
||||
title := ""
|
||||
|
||||
err := prompt.SurveyAskOne(p, &title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state.Title = title
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func MetadataSurvey(io *iostreams.IOStreams, client *api.Client, baseRepo ghrepo.Interface, state *IssueMetadataState) error {
|
||||
isChosen := func(m string) bool {
|
||||
for _, c := range state.Metadata {
|
||||
if m == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
allowReviewers := state.Type == PRMetadata
|
||||
|
||||
extraFieldsOptions := []string{}
|
||||
if allowReviewers {
|
||||
extraFieldsOptions = append(extraFieldsOptions, "Reviewers")
|
||||
}
|
||||
extraFieldsOptions = append(extraFieldsOptions, "Assignees", "Labels", "Projects", "Milestone")
|
||||
|
||||
err := prompt.SurveyAsk([]*survey.Question{
|
||||
{
|
||||
Name: "metadata",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "What would you like to add?",
|
||||
Options: extraFieldsOptions,
|
||||
},
|
||||
},
|
||||
}, state)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
metadataInput := api.RepoMetadataInput{
|
||||
Reviewers: isChosen("Reviewers"),
|
||||
Assignees: isChosen("Assignees"),
|
||||
Labels: isChosen("Labels"),
|
||||
Projects: isChosen("Projects"),
|
||||
Milestones: isChosen("Milestone"),
|
||||
}
|
||||
s := utils.Spinner(io.ErrOut)
|
||||
utils.StartSpinner(s)
|
||||
state.MetadataResult, err = api.RepoMetadata(client, baseRepo, metadataInput)
|
||||
utils.StopSpinner(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching metadata options: %w", err)
|
||||
}
|
||||
|
||||
var users []string
|
||||
for _, u := range state.MetadataResult.AssignableUsers {
|
||||
users = append(users, u.Login)
|
||||
}
|
||||
var teams []string
|
||||
for _, t := range state.MetadataResult.Teams {
|
||||
teams = append(teams, fmt.Sprintf("%s/%s", baseRepo.RepoOwner(), t.Slug))
|
||||
}
|
||||
var labels []string
|
||||
for _, l := range state.MetadataResult.Labels {
|
||||
labels = append(labels, l.Name)
|
||||
}
|
||||
var projects []string
|
||||
for _, l := range state.MetadataResult.Projects {
|
||||
projects = append(projects, l.Name)
|
||||
}
|
||||
milestones := []string{noMilestone}
|
||||
for _, m := range state.MetadataResult.Milestones {
|
||||
milestones = append(milestones, m.Title)
|
||||
}
|
||||
|
||||
type metadataValues struct {
|
||||
Reviewers []string
|
||||
Assignees []string
|
||||
Labels []string
|
||||
Projects []string
|
||||
Milestone string
|
||||
}
|
||||
var mqs []*survey.Question
|
||||
if isChosen("Reviewers") {
|
||||
if len(users) > 0 || len(teams) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "reviewers",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Reviewers",
|
||||
Options: append(users, teams...),
|
||||
Default: state.Reviewers,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no available reviewers")
|
||||
}
|
||||
}
|
||||
if isChosen("Assignees") {
|
||||
if len(users) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "assignees",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Assignees",
|
||||
Options: users,
|
||||
Default: state.Assignees,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no assignable users")
|
||||
}
|
||||
}
|
||||
if isChosen("Labels") {
|
||||
if len(labels) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "labels",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Labels",
|
||||
Options: labels,
|
||||
Default: state.Labels,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no labels in the repository")
|
||||
}
|
||||
}
|
||||
if isChosen("Projects") {
|
||||
if len(projects) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "projects",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Projects",
|
||||
Options: projects,
|
||||
Default: state.Projects,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no projects to choose from")
|
||||
}
|
||||
}
|
||||
if isChosen("Milestone") {
|
||||
if len(milestones) > 1 {
|
||||
var milestoneDefault string
|
||||
if len(state.Milestones) > 0 {
|
||||
milestoneDefault = state.Milestones[0]
|
||||
}
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "milestone",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Milestone",
|
||||
Options: milestones,
|
||||
Default: milestoneDefault,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no milestones in the repository")
|
||||
}
|
||||
}
|
||||
values := metadataValues{}
|
||||
err = prompt.SurveyAsk(mqs, &values, survey.WithKeepFilter(true))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
state.Reviewers = values.Reviewers
|
||||
state.Assignees = values.Assignees
|
||||
state.Labels = values.Labels
|
||||
state.Projects = values.Projects
|
||||
if values.Milestone != "" && values.Milestone != noMilestone {
|
||||
state.Milestones = []string{values.Milestone}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindTemplates(dir, path string) ([]string, string) {
|
||||
if dir == "" {
|
||||
rootDir, err := git.ToplevelDir()
|
||||
if err != nil {
|
||||
return []string{}, ""
|
||||
}
|
||||
dir = rootDir
|
||||
}
|
||||
|
||||
templateFiles := githubtemplate.FindNonLegacy(dir, path)
|
||||
legacyTemplate := githubtemplate.FindLegacy(dir, path)
|
||||
|
||||
// TODO stop using string pointer
|
||||
|
||||
lt := ""
|
||||
|
||||
if legacyTemplate != nil {
|
||||
lt = *legacyTemplate
|
||||
}
|
||||
|
||||
return templateFiles, lt
|
||||
}
|
||||
|
|
@ -1,396 +0,0 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/githubtemplate"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/prompt"
|
||||
"github.com/cli/cli/pkg/surveyext"
|
||||
"github.com/cli/cli/utils"
|
||||
)
|
||||
|
||||
type Defaults struct {
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
|
||||
type Action int
|
||||
type metadataStateType int
|
||||
|
||||
const (
|
||||
IssueMetadata metadataStateType = iota
|
||||
PRMetadata
|
||||
)
|
||||
|
||||
type IssueMetadataState struct {
|
||||
Type metadataStateType
|
||||
|
||||
Body string
|
||||
Title string
|
||||
Action Action
|
||||
|
||||
Metadata []string
|
||||
Reviewers []string
|
||||
Assignees []string
|
||||
Labels []string
|
||||
Projects []string
|
||||
Milestones []string
|
||||
|
||||
MetadataResult *api.RepoMetadataResult
|
||||
}
|
||||
|
||||
func (tb *IssueMetadataState) HasMetadata() bool {
|
||||
return len(tb.Reviewers) > 0 ||
|
||||
len(tb.Assignees) > 0 ||
|
||||
len(tb.Labels) > 0 ||
|
||||
len(tb.Projects) > 0 ||
|
||||
len(tb.Milestones) > 0
|
||||
}
|
||||
|
||||
const (
|
||||
SubmitAction Action = iota
|
||||
PreviewAction
|
||||
CancelAction
|
||||
MetadataAction
|
||||
|
||||
noMilestone = "(none)"
|
||||
)
|
||||
|
||||
func confirmSubmission(allowPreview bool, allowMetadata bool) (Action, error) {
|
||||
const (
|
||||
submitLabel = "Submit"
|
||||
previewLabel = "Continue in browser"
|
||||
metadataLabel = "Add metadata"
|
||||
cancelLabel = "Cancel"
|
||||
)
|
||||
|
||||
options := []string{submitLabel}
|
||||
if allowPreview {
|
||||
options = append(options, previewLabel)
|
||||
}
|
||||
if allowMetadata {
|
||||
options = append(options, metadataLabel)
|
||||
}
|
||||
options = append(options, cancelLabel)
|
||||
|
||||
confirmAnswers := struct {
|
||||
Confirmation int
|
||||
}{}
|
||||
confirmQs := []*survey.Question{
|
||||
{
|
||||
Name: "confirmation",
|
||||
Prompt: &survey.Select{
|
||||
Message: "What's next?",
|
||||
Options: options,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := prompt.SurveyAsk(confirmQs, &confirmAnswers)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
switch options[confirmAnswers.Confirmation] {
|
||||
case submitLabel:
|
||||
return SubmitAction, nil
|
||||
case previewLabel:
|
||||
return PreviewAction, nil
|
||||
case metadataLabel:
|
||||
return MetadataAction, nil
|
||||
case cancelLabel:
|
||||
return CancelAction, nil
|
||||
default:
|
||||
return -1, fmt.Errorf("invalid index: %d", confirmAnswers.Confirmation)
|
||||
}
|
||||
}
|
||||
|
||||
func selectTemplate(nonLegacyTemplatePaths []string, legacyTemplatePath *string, metadataType metadataStateType) (string, error) {
|
||||
templateResponse := struct {
|
||||
Index int
|
||||
}{}
|
||||
templateNames := make([]string, 0, len(nonLegacyTemplatePaths))
|
||||
for _, p := range nonLegacyTemplatePaths {
|
||||
templateNames = append(templateNames, githubtemplate.ExtractName(p))
|
||||
}
|
||||
if metadataType == IssueMetadata {
|
||||
templateNames = append(templateNames, "Open a blank issue")
|
||||
} else if metadataType == PRMetadata {
|
||||
templateNames = append(templateNames, "Open a blank pull request")
|
||||
}
|
||||
|
||||
selectQs := []*survey.Question{
|
||||
{
|
||||
Name: "index",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Choose a template",
|
||||
Options: templateNames,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := prompt.SurveyAsk(selectQs, &templateResponse); err != nil {
|
||||
return "", fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
if templateResponse.Index == len(nonLegacyTemplatePaths) { // the user has selected the blank template
|
||||
if legacyTemplatePath != nil {
|
||||
templateContents := githubtemplate.ExtractContents(*legacyTemplatePath)
|
||||
return string(templateContents), nil
|
||||
} else {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
templateContents := githubtemplate.ExtractContents(nonLegacyTemplatePaths[templateResponse.Index])
|
||||
return string(templateContents), nil
|
||||
}
|
||||
|
||||
// FIXME: this command has too many parameters and responsibilities
|
||||
func TitleBodySurvey(io *iostreams.IOStreams, editorCommand string, issueState *IssueMetadataState, apiClient *api.Client, repo ghrepo.Interface, providedTitle, providedBody string, defs Defaults, nonLegacyTemplatePaths []string, legacyTemplatePath *string, allowReviewers, allowMetadata bool) error {
|
||||
issueState.Title = defs.Title
|
||||
templateContents := ""
|
||||
|
||||
if providedBody == "" {
|
||||
issueState.Body = defs.Body
|
||||
|
||||
if len(nonLegacyTemplatePaths) > 0 {
|
||||
var err error
|
||||
templateContents, err = selectTemplate(nonLegacyTemplatePaths, legacyTemplatePath, issueState.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
} else if legacyTemplatePath != nil {
|
||||
templateContents = string(githubtemplate.ExtractContents(*legacyTemplatePath))
|
||||
}
|
||||
|
||||
if templateContents != "" {
|
||||
if issueState.Body != "" {
|
||||
// prevent excessive newlines between default body and template
|
||||
issueState.Body = strings.TrimRight(issueState.Body, "\n")
|
||||
issueState.Body += "\n\n"
|
||||
}
|
||||
issueState.Body += templateContents
|
||||
}
|
||||
}
|
||||
|
||||
titleQuestion := &survey.Question{
|
||||
Name: "title",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Title",
|
||||
Default: issueState.Title,
|
||||
},
|
||||
}
|
||||
bodyQuestion := &survey.Question{
|
||||
Name: "body",
|
||||
Prompt: &surveyext.GhEditor{
|
||||
BlankAllowed: true,
|
||||
EditorCommand: editorCommand,
|
||||
Editor: &survey.Editor{
|
||||
Message: "Body",
|
||||
FileName: "*.md",
|
||||
Default: issueState.Body,
|
||||
HideDefault: true,
|
||||
AppendDefault: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var qs []*survey.Question
|
||||
if providedTitle == "" {
|
||||
qs = append(qs, titleQuestion)
|
||||
}
|
||||
if providedBody == "" {
|
||||
qs = append(qs, bodyQuestion)
|
||||
}
|
||||
|
||||
err := prompt.SurveyAsk(qs, issueState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
if issueState.Body == "" {
|
||||
issueState.Body = templateContents
|
||||
}
|
||||
|
||||
allowPreview := !issueState.HasMetadata()
|
||||
confirmA, err := confirmSubmission(allowPreview, allowMetadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to confirm: %w", err)
|
||||
}
|
||||
|
||||
if confirmA == MetadataAction {
|
||||
isChosen := func(m string) bool {
|
||||
for _, c := range issueState.Metadata {
|
||||
if m == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
extraFieldsOptions := []string{}
|
||||
if allowReviewers {
|
||||
extraFieldsOptions = append(extraFieldsOptions, "Reviewers")
|
||||
}
|
||||
extraFieldsOptions = append(extraFieldsOptions, "Assignees", "Labels", "Projects", "Milestone")
|
||||
|
||||
err = prompt.SurveyAsk([]*survey.Question{
|
||||
{
|
||||
Name: "metadata",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "What would you like to add?",
|
||||
Options: extraFieldsOptions,
|
||||
},
|
||||
},
|
||||
}, issueState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
metadataInput := api.RepoMetadataInput{
|
||||
Reviewers: isChosen("Reviewers"),
|
||||
Assignees: isChosen("Assignees"),
|
||||
Labels: isChosen("Labels"),
|
||||
Projects: isChosen("Projects"),
|
||||
Milestones: isChosen("Milestone"),
|
||||
}
|
||||
s := utils.Spinner(io.ErrOut)
|
||||
utils.StartSpinner(s)
|
||||
issueState.MetadataResult, err = api.RepoMetadata(apiClient, repo, metadataInput)
|
||||
utils.StopSpinner(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching metadata options: %w", err)
|
||||
}
|
||||
|
||||
var users []string
|
||||
for _, u := range issueState.MetadataResult.AssignableUsers {
|
||||
users = append(users, u.Login)
|
||||
}
|
||||
var teams []string
|
||||
for _, t := range issueState.MetadataResult.Teams {
|
||||
teams = append(teams, fmt.Sprintf("%s/%s", repo.RepoOwner(), t.Slug))
|
||||
}
|
||||
var labels []string
|
||||
for _, l := range issueState.MetadataResult.Labels {
|
||||
labels = append(labels, l.Name)
|
||||
}
|
||||
var projects []string
|
||||
for _, l := range issueState.MetadataResult.Projects {
|
||||
projects = append(projects, l.Name)
|
||||
}
|
||||
milestones := []string{noMilestone}
|
||||
for _, m := range issueState.MetadataResult.Milestones {
|
||||
milestones = append(milestones, m.Title)
|
||||
}
|
||||
|
||||
type metadataValues struct {
|
||||
Reviewers []string
|
||||
Assignees []string
|
||||
Labels []string
|
||||
Projects []string
|
||||
Milestone string
|
||||
}
|
||||
var mqs []*survey.Question
|
||||
if isChosen("Reviewers") {
|
||||
if len(users) > 0 || len(teams) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "reviewers",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Reviewers",
|
||||
Options: append(users, teams...),
|
||||
Default: issueState.Reviewers,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no available reviewers")
|
||||
}
|
||||
}
|
||||
if isChosen("Assignees") {
|
||||
if len(users) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "assignees",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Assignees",
|
||||
Options: users,
|
||||
Default: issueState.Assignees,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no assignable users")
|
||||
}
|
||||
}
|
||||
if isChosen("Labels") {
|
||||
if len(labels) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "labels",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Labels",
|
||||
Options: labels,
|
||||
Default: issueState.Labels,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no labels in the repository")
|
||||
}
|
||||
}
|
||||
if isChosen("Projects") {
|
||||
if len(projects) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "projects",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Projects",
|
||||
Options: projects,
|
||||
Default: issueState.Projects,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no projects to choose from")
|
||||
}
|
||||
}
|
||||
if isChosen("Milestone") {
|
||||
if len(milestones) > 1 {
|
||||
var milestoneDefault string
|
||||
if len(issueState.Milestones) > 0 {
|
||||
milestoneDefault = issueState.Milestones[0]
|
||||
}
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "milestone",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Milestone",
|
||||
Options: milestones,
|
||||
Default: milestoneDefault,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no milestones in the repository")
|
||||
}
|
||||
}
|
||||
values := metadataValues{}
|
||||
err = prompt.SurveyAsk(mqs, &values, survey.WithKeepFilter(true))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
issueState.Reviewers = values.Reviewers
|
||||
issueState.Assignees = values.Assignees
|
||||
issueState.Labels = values.Labels
|
||||
issueState.Projects = values.Projects
|
||||
if values.Milestone != "" && values.Milestone != noMilestone {
|
||||
issueState.Milestones = []string{values.Milestone}
|
||||
}
|
||||
|
||||
allowPreview = !issueState.HasMetadata()
|
||||
allowMetadata = false
|
||||
confirmA, err = confirmSubmission(allowPreview, allowMetadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to confirm: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
issueState.Action = confirmA
|
||||
return nil
|
||||
}
|
||||
|
|
@ -54,6 +54,7 @@ mainLoop:
|
|||
|
||||
// FindLegacy returns the file path of the default(legacy) template
|
||||
func FindLegacy(rootDir string, name string) *string {
|
||||
// TODO why does this return a pointer to string??
|
||||
namePattern := regexp.MustCompile(fmt.Sprintf(`(?i)^%s(\.|$)`, strings.ReplaceAll(name, "_", "[_-]")))
|
||||
|
||||
// https://help.github.com/en/github/building-a-strong-community/creating-a-pull-request-template-for-your-repository
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue