github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/pr/shared/survey.go (about)

     1  package shared
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/AlecAivazis/survey/v2"
     8  	"github.com/cli/cli/api"
     9  	"github.com/cli/cli/git"
    10  	"github.com/cli/cli/internal/ghrepo"
    11  	"github.com/cli/cli/pkg/githubtemplate"
    12  	"github.com/cli/cli/pkg/iostreams"
    13  	"github.com/cli/cli/pkg/prompt"
    14  	"github.com/cli/cli/pkg/surveyext"
    15  )
    16  
    17  type Action int
    18  
    19  const (
    20  	SubmitAction Action = iota
    21  	PreviewAction
    22  	CancelAction
    23  	MetadataAction
    24  	EditCommitMessageAction
    25  
    26  	noMilestone = "(none)"
    27  )
    28  
    29  func ConfirmSubmission(allowPreview bool, allowMetadata bool) (Action, error) {
    30  	const (
    31  		submitLabel   = "Submit"
    32  		previewLabel  = "Continue in browser"
    33  		metadataLabel = "Add metadata"
    34  		cancelLabel   = "Cancel"
    35  	)
    36  
    37  	options := []string{submitLabel}
    38  	if allowPreview {
    39  		options = append(options, previewLabel)
    40  	}
    41  	if allowMetadata {
    42  		options = append(options, metadataLabel)
    43  	}
    44  	options = append(options, cancelLabel)
    45  
    46  	confirmAnswers := struct {
    47  		Confirmation int
    48  	}{}
    49  	confirmQs := []*survey.Question{
    50  		{
    51  			Name: "confirmation",
    52  			Prompt: &survey.Select{
    53  				Message: "What's next?",
    54  				Options: options,
    55  			},
    56  		},
    57  	}
    58  
    59  	err := prompt.SurveyAsk(confirmQs, &confirmAnswers)
    60  	if err != nil {
    61  		return -1, fmt.Errorf("could not prompt: %w", err)
    62  	}
    63  
    64  	switch options[confirmAnswers.Confirmation] {
    65  	case submitLabel:
    66  		return SubmitAction, nil
    67  	case previewLabel:
    68  		return PreviewAction, nil
    69  	case metadataLabel:
    70  		return MetadataAction, nil
    71  	case cancelLabel:
    72  		return CancelAction, nil
    73  	default:
    74  		return -1, fmt.Errorf("invalid index: %d", confirmAnswers.Confirmation)
    75  	}
    76  }
    77  
    78  func BodySurvey(state *IssueMetadataState, templateContent, editorCommand string) error {
    79  	if templateContent != "" {
    80  		if state.Body != "" {
    81  			// prevent excessive newlines between default body and template
    82  			state.Body = strings.TrimRight(state.Body, "\n")
    83  			state.Body += "\n\n"
    84  		}
    85  		state.Body += templateContent
    86  	}
    87  
    88  	preBody := state.Body
    89  
    90  	// TODO should just be an AskOne but ran into problems with the stubber
    91  	qs := []*survey.Question{
    92  		{
    93  			Name: "Body",
    94  			Prompt: &surveyext.GhEditor{
    95  				BlankAllowed:  true,
    96  				EditorCommand: editorCommand,
    97  				Editor: &survey.Editor{
    98  					Message:       "Body",
    99  					FileName:      "*.md",
   100  					Default:       state.Body,
   101  					HideDefault:   true,
   102  					AppendDefault: true,
   103  				},
   104  			},
   105  		},
   106  	}
   107  
   108  	err := prompt.SurveyAsk(qs, state)
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	if preBody != state.Body {
   114  		state.MarkDirty()
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  func TitleSurvey(state *IssueMetadataState) error {
   121  	preTitle := state.Title
   122  
   123  	// TODO should just be an AskOne but ran into problems with the stubber
   124  	qs := []*survey.Question{
   125  		{
   126  			Name: "Title",
   127  			Prompt: &survey.Input{
   128  				Message: "Title",
   129  				Default: state.Title,
   130  			},
   131  		},
   132  	}
   133  
   134  	err := prompt.SurveyAsk(qs, state)
   135  	if err != nil {
   136  		return err
   137  	}
   138  
   139  	if preTitle != state.Title {
   140  		state.MarkDirty()
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  type MetadataFetcher struct {
   147  	IO        *iostreams.IOStreams
   148  	APIClient *api.Client
   149  	Repo      ghrepo.Interface
   150  	State     *IssueMetadataState
   151  }
   152  
   153  func (mf *MetadataFetcher) RepoMetadataFetch(input api.RepoMetadataInput) (*api.RepoMetadataResult, error) {
   154  	mf.IO.StartProgressIndicator()
   155  	metadataResult, err := api.RepoMetadata(mf.APIClient, mf.Repo, input)
   156  	mf.IO.StopProgressIndicator()
   157  	mf.State.MetadataResult = metadataResult
   158  	return metadataResult, err
   159  }
   160  
   161  type RepoMetadataFetcher interface {
   162  	RepoMetadataFetch(api.RepoMetadataInput) (*api.RepoMetadataResult, error)
   163  }
   164  
   165  func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher RepoMetadataFetcher, state *IssueMetadataState) error {
   166  	isChosen := func(m string) bool {
   167  		for _, c := range state.Metadata {
   168  			if m == c {
   169  				return true
   170  			}
   171  		}
   172  		return false
   173  	}
   174  
   175  	allowReviewers := state.Type == PRMetadata
   176  
   177  	extraFieldsOptions := []string{}
   178  	if allowReviewers {
   179  		extraFieldsOptions = append(extraFieldsOptions, "Reviewers")
   180  	}
   181  	extraFieldsOptions = append(extraFieldsOptions, "Assignees", "Labels", "Projects", "Milestone")
   182  
   183  	err := prompt.SurveyAsk([]*survey.Question{
   184  		{
   185  			Name: "metadata",
   186  			Prompt: &survey.MultiSelect{
   187  				Message: "What would you like to add?",
   188  				Options: extraFieldsOptions,
   189  			},
   190  		},
   191  	}, state)
   192  	if err != nil {
   193  		return fmt.Errorf("could not prompt: %w", err)
   194  	}
   195  
   196  	metadataInput := api.RepoMetadataInput{
   197  		Reviewers:  isChosen("Reviewers"),
   198  		Assignees:  isChosen("Assignees"),
   199  		Labels:     isChosen("Labels"),
   200  		Projects:   isChosen("Projects"),
   201  		Milestones: isChosen("Milestone"),
   202  	}
   203  	metadataResult, err := fetcher.RepoMetadataFetch(metadataInput)
   204  	if err != nil {
   205  		return fmt.Errorf("error fetching metadata options: %w", err)
   206  	}
   207  
   208  	var users []string
   209  	for _, u := range metadataResult.AssignableUsers {
   210  		users = append(users, u.Login)
   211  	}
   212  	var teams []string
   213  	for _, t := range metadataResult.Teams {
   214  		teams = append(teams, fmt.Sprintf("%s/%s", baseRepo.RepoOwner(), t.Slug))
   215  	}
   216  	var labels []string
   217  	for _, l := range metadataResult.Labels {
   218  		labels = append(labels, l.Name)
   219  	}
   220  	var projects []string
   221  	for _, l := range metadataResult.Projects {
   222  		projects = append(projects, l.Name)
   223  	}
   224  	milestones := []string{noMilestone}
   225  	for _, m := range metadataResult.Milestones {
   226  		milestones = append(milestones, m.Title)
   227  	}
   228  
   229  	var mqs []*survey.Question
   230  	if isChosen("Reviewers") {
   231  		if len(users) > 0 || len(teams) > 0 {
   232  			mqs = append(mqs, &survey.Question{
   233  				Name: "reviewers",
   234  				Prompt: &survey.MultiSelect{
   235  					Message: "Reviewers",
   236  					Options: append(users, teams...),
   237  					Default: state.Reviewers,
   238  				},
   239  			})
   240  		} else {
   241  			fmt.Fprintln(io.ErrOut, "warning: no available reviewers")
   242  		}
   243  	}
   244  	if isChosen("Assignees") {
   245  		if len(users) > 0 {
   246  			mqs = append(mqs, &survey.Question{
   247  				Name: "assignees",
   248  				Prompt: &survey.MultiSelect{
   249  					Message: "Assignees",
   250  					Options: users,
   251  					Default: state.Assignees,
   252  				},
   253  			})
   254  		} else {
   255  			fmt.Fprintln(io.ErrOut, "warning: no assignable users")
   256  		}
   257  	}
   258  	if isChosen("Labels") {
   259  		if len(labels) > 0 {
   260  			mqs = append(mqs, &survey.Question{
   261  				Name: "labels",
   262  				Prompt: &survey.MultiSelect{
   263  					Message: "Labels",
   264  					Options: labels,
   265  					Default: state.Labels,
   266  				},
   267  			})
   268  		} else {
   269  			fmt.Fprintln(io.ErrOut, "warning: no labels in the repository")
   270  		}
   271  	}
   272  	if isChosen("Projects") {
   273  		if len(projects) > 0 {
   274  			mqs = append(mqs, &survey.Question{
   275  				Name: "projects",
   276  				Prompt: &survey.MultiSelect{
   277  					Message: "Projects",
   278  					Options: projects,
   279  					Default: state.Projects,
   280  				},
   281  			})
   282  		} else {
   283  			fmt.Fprintln(io.ErrOut, "warning: no projects to choose from")
   284  		}
   285  	}
   286  	if isChosen("Milestone") {
   287  		if len(milestones) > 1 {
   288  			var milestoneDefault string
   289  			if len(state.Milestones) > 0 {
   290  				milestoneDefault = state.Milestones[0]
   291  			}
   292  			mqs = append(mqs, &survey.Question{
   293  				Name: "milestone",
   294  				Prompt: &survey.Select{
   295  					Message: "Milestone",
   296  					Options: milestones,
   297  					Default: milestoneDefault,
   298  				},
   299  			})
   300  		} else {
   301  			fmt.Fprintln(io.ErrOut, "warning: no milestones in the repository")
   302  		}
   303  	}
   304  
   305  	values := struct {
   306  		Reviewers []string
   307  		Assignees []string
   308  		Labels    []string
   309  		Projects  []string
   310  		Milestone string
   311  	}{}
   312  
   313  	err = prompt.SurveyAsk(mqs, &values, survey.WithKeepFilter(true))
   314  	if err != nil {
   315  		return fmt.Errorf("could not prompt: %w", err)
   316  	}
   317  
   318  	if isChosen("Reviewers") {
   319  		state.Reviewers = values.Reviewers
   320  	}
   321  	if isChosen("Assignees") {
   322  		state.Assignees = values.Assignees
   323  	}
   324  	if isChosen("Labels") {
   325  		state.Labels = values.Labels
   326  	}
   327  	if isChosen("Projects") {
   328  		state.Projects = values.Projects
   329  	}
   330  	if isChosen("Milestone") {
   331  		if values.Milestone != "" && values.Milestone != noMilestone {
   332  			state.Milestones = []string{values.Milestone}
   333  		} else {
   334  			state.Milestones = []string{}
   335  		}
   336  	}
   337  
   338  	return nil
   339  }
   340  
   341  func FindTemplates(dir, path string) ([]string, string) {
   342  	if dir == "" {
   343  		rootDir, err := git.ToplevelDir()
   344  		if err != nil {
   345  			return []string{}, ""
   346  		}
   347  		dir = rootDir
   348  	}
   349  
   350  	templateFiles := githubtemplate.FindNonLegacy(dir, path)
   351  	legacyTemplate := githubtemplate.FindLegacy(dir, path)
   352  
   353  	return templateFiles, legacyTemplate
   354  }