github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/pr/shared/editable.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/internal/ghrepo"
    10  	"github.com/cli/cli/pkg/set"
    11  	"github.com/cli/cli/pkg/surveyext"
    12  )
    13  
    14  type Editable struct {
    15  	Title     EditableString
    16  	Body      EditableString
    17  	Base      EditableString
    18  	Reviewers EditableSlice
    19  	Assignees EditableSlice
    20  	Labels    EditableSlice
    21  	Projects  EditableSlice
    22  	Milestone EditableString
    23  	Metadata  api.RepoMetadataResult
    24  }
    25  
    26  type EditableString struct {
    27  	Value   string
    28  	Default string
    29  	Options []string
    30  	Edited  bool
    31  }
    32  
    33  type EditableSlice struct {
    34  	Value   []string
    35  	Add     []string
    36  	Remove  []string
    37  	Default []string
    38  	Options []string
    39  	Edited  bool
    40  	Allowed bool
    41  }
    42  
    43  func (e Editable) Dirty() bool {
    44  	return e.Title.Edited ||
    45  		e.Body.Edited ||
    46  		e.Base.Edited ||
    47  		e.Reviewers.Edited ||
    48  		e.Assignees.Edited ||
    49  		e.Labels.Edited ||
    50  		e.Projects.Edited ||
    51  		e.Milestone.Edited
    52  }
    53  
    54  func (e Editable) TitleValue() *string {
    55  	if !e.Title.Edited {
    56  		return nil
    57  	}
    58  	return &e.Title.Value
    59  }
    60  
    61  func (e Editable) BodyValue() *string {
    62  	if !e.Body.Edited {
    63  		return nil
    64  	}
    65  	return &e.Body.Value
    66  }
    67  
    68  func (e Editable) ReviewerIds() (*[]string, *[]string, error) {
    69  	if !e.Reviewers.Edited {
    70  		return nil, nil, nil
    71  	}
    72  	if len(e.Reviewers.Add) != 0 || len(e.Reviewers.Remove) != 0 {
    73  		s := set.NewStringSet()
    74  		s.AddValues(e.Reviewers.Default)
    75  		s.AddValues(e.Reviewers.Add)
    76  		s.RemoveValues(e.Reviewers.Remove)
    77  		e.Reviewers.Value = s.ToSlice()
    78  	}
    79  	var userReviewers []string
    80  	var teamReviewers []string
    81  	for _, r := range e.Reviewers.Value {
    82  		if strings.ContainsRune(r, '/') {
    83  			teamReviewers = append(teamReviewers, r)
    84  		} else {
    85  			userReviewers = append(userReviewers, r)
    86  		}
    87  	}
    88  	userIds, err := e.Metadata.MembersToIDs(userReviewers)
    89  	if err != nil {
    90  		return nil, nil, err
    91  	}
    92  	teamIds, err := e.Metadata.TeamsToIDs(teamReviewers)
    93  	if err != nil {
    94  		return nil, nil, err
    95  	}
    96  	return &userIds, &teamIds, nil
    97  }
    98  
    99  func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]string, error) {
   100  	if !e.Assignees.Edited {
   101  		return nil, nil
   102  	}
   103  	if len(e.Assignees.Add) != 0 || len(e.Assignees.Remove) != 0 {
   104  		meReplacer := NewMeReplacer(client, repo.RepoHost())
   105  		s := set.NewStringSet()
   106  		s.AddValues(e.Assignees.Default)
   107  		add, err := meReplacer.ReplaceSlice(e.Assignees.Add)
   108  		if err != nil {
   109  			return nil, err
   110  		}
   111  		s.AddValues(add)
   112  		remove, err := meReplacer.ReplaceSlice(e.Assignees.Remove)
   113  		if err != nil {
   114  			return nil, err
   115  		}
   116  		s.RemoveValues(remove)
   117  		e.Assignees.Value = s.ToSlice()
   118  	}
   119  	a, err := e.Metadata.MembersToIDs(e.Assignees.Value)
   120  	return &a, err
   121  }
   122  
   123  func (e Editable) LabelIds() (*[]string, error) {
   124  	if !e.Labels.Edited {
   125  		return nil, nil
   126  	}
   127  	if len(e.Labels.Add) != 0 || len(e.Labels.Remove) != 0 {
   128  		s := set.NewStringSet()
   129  		s.AddValues(e.Labels.Default)
   130  		s.AddValues(e.Labels.Add)
   131  		s.RemoveValues(e.Labels.Remove)
   132  		e.Labels.Value = s.ToSlice()
   133  	}
   134  	l, err := e.Metadata.LabelsToIDs(e.Labels.Value)
   135  	return &l, err
   136  }
   137  
   138  func (e Editable) ProjectIds() (*[]string, error) {
   139  	if !e.Projects.Edited {
   140  		return nil, nil
   141  	}
   142  	if len(e.Projects.Add) != 0 || len(e.Projects.Remove) != 0 {
   143  		s := set.NewStringSet()
   144  		s.AddValues(e.Projects.Default)
   145  		s.AddValues(e.Projects.Add)
   146  		s.RemoveValues(e.Projects.Remove)
   147  		e.Projects.Value = s.ToSlice()
   148  	}
   149  	p, err := e.Metadata.ProjectsToIDs(e.Projects.Value)
   150  	return &p, err
   151  }
   152  
   153  func (e Editable) MilestoneId() (*string, error) {
   154  	if !e.Milestone.Edited {
   155  		return nil, nil
   156  	}
   157  	if e.Milestone.Value == noMilestone || e.Milestone.Value == "" {
   158  		s := ""
   159  		return &s, nil
   160  	}
   161  	m, err := e.Metadata.MilestoneToID(e.Milestone.Value)
   162  	return &m, err
   163  }
   164  
   165  func EditFieldsSurvey(editable *Editable, editorCommand string) error {
   166  	var err error
   167  	if editable.Title.Edited {
   168  		editable.Title.Value, err = titleSurvey(editable.Title.Default)
   169  		if err != nil {
   170  			return err
   171  		}
   172  	}
   173  	if editable.Body.Edited {
   174  		editable.Body.Value, err = bodySurvey(editable.Body.Default, editorCommand)
   175  		if err != nil {
   176  			return err
   177  		}
   178  	}
   179  	if editable.Reviewers.Edited {
   180  		editable.Reviewers.Value, err = multiSelectSurvey("Reviewers", editable.Reviewers.Default, editable.Reviewers.Options)
   181  		if err != nil {
   182  			return err
   183  		}
   184  	}
   185  	if editable.Assignees.Edited {
   186  		editable.Assignees.Value, err = multiSelectSurvey("Assignees", editable.Assignees.Default, editable.Assignees.Options)
   187  		if err != nil {
   188  			return err
   189  		}
   190  	}
   191  	if editable.Labels.Edited {
   192  		editable.Labels.Value, err = multiSelectSurvey("Labels", editable.Labels.Default, editable.Labels.Options)
   193  		if err != nil {
   194  			return err
   195  		}
   196  	}
   197  	if editable.Projects.Edited {
   198  		editable.Projects.Value, err = multiSelectSurvey("Projects", editable.Projects.Default, editable.Projects.Options)
   199  		if err != nil {
   200  			return err
   201  		}
   202  	}
   203  	if editable.Milestone.Edited {
   204  		editable.Milestone.Value, err = milestoneSurvey(editable.Milestone.Default, editable.Milestone.Options)
   205  		if err != nil {
   206  			return err
   207  		}
   208  	}
   209  	confirm, err := confirmSurvey()
   210  	if err != nil {
   211  		return err
   212  	}
   213  	if !confirm {
   214  		return fmt.Errorf("Discarding...")
   215  	}
   216  
   217  	return nil
   218  }
   219  
   220  func FieldsToEditSurvey(editable *Editable) error {
   221  	contains := func(s []string, str string) bool {
   222  		for _, v := range s {
   223  			if v == str {
   224  				return true
   225  			}
   226  		}
   227  		return false
   228  	}
   229  
   230  	opts := []string{"Title", "Body"}
   231  	if editable.Reviewers.Allowed {
   232  		opts = append(opts, "Reviewers")
   233  	}
   234  	opts = append(opts, "Assignees", "Labels", "Projects", "Milestone")
   235  	results, err := multiSelectSurvey("What would you like to edit?", []string{}, opts)
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	if contains(results, "Title") {
   241  		editable.Title.Edited = true
   242  	}
   243  	if contains(results, "Body") {
   244  		editable.Body.Edited = true
   245  	}
   246  	if contains(results, "Reviewers") {
   247  		editable.Reviewers.Edited = true
   248  	}
   249  	if contains(results, "Assignees") {
   250  		editable.Assignees.Edited = true
   251  	}
   252  	if contains(results, "Labels") {
   253  		editable.Labels.Edited = true
   254  	}
   255  	if contains(results, "Projects") {
   256  		editable.Projects.Edited = true
   257  	}
   258  	if contains(results, "Milestone") {
   259  		editable.Milestone.Edited = true
   260  	}
   261  
   262  	return nil
   263  }
   264  
   265  func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) error {
   266  	input := api.RepoMetadataInput{
   267  		Reviewers:  editable.Reviewers.Edited,
   268  		Assignees:  editable.Assignees.Edited,
   269  		Labels:     editable.Labels.Edited,
   270  		Projects:   editable.Projects.Edited,
   271  		Milestones: editable.Milestone.Edited,
   272  	}
   273  	metadata, err := api.RepoMetadata(client, repo, input)
   274  	if err != nil {
   275  		return err
   276  	}
   277  
   278  	var users []string
   279  	for _, u := range metadata.AssignableUsers {
   280  		users = append(users, u.Login)
   281  	}
   282  	var teams []string
   283  	for _, t := range metadata.Teams {
   284  		teams = append(teams, fmt.Sprintf("%s/%s", repo.RepoOwner(), t.Slug))
   285  	}
   286  	var labels []string
   287  	for _, l := range metadata.Labels {
   288  		labels = append(labels, l.Name)
   289  	}
   290  	var projects []string
   291  	for _, l := range metadata.Projects {
   292  		projects = append(projects, l.Name)
   293  	}
   294  	milestones := []string{noMilestone}
   295  	for _, m := range metadata.Milestones {
   296  		milestones = append(milestones, m.Title)
   297  	}
   298  
   299  	editable.Metadata = *metadata
   300  	editable.Reviewers.Options = append(users, teams...)
   301  	editable.Assignees.Options = users
   302  	editable.Labels.Options = labels
   303  	editable.Projects.Options = projects
   304  	editable.Milestone.Options = milestones
   305  
   306  	return nil
   307  }
   308  
   309  func titleSurvey(title string) (string, error) {
   310  	var result string
   311  	q := &survey.Input{
   312  		Message: "Title",
   313  		Default: title,
   314  	}
   315  	err := survey.AskOne(q, &result)
   316  	return result, err
   317  }
   318  
   319  func bodySurvey(body, editorCommand string) (string, error) {
   320  	var result string
   321  	q := &surveyext.GhEditor{
   322  		EditorCommand: editorCommand,
   323  		Editor: &survey.Editor{
   324  			Message:       "Body",
   325  			FileName:      "*.md",
   326  			Default:       body,
   327  			HideDefault:   true,
   328  			AppendDefault: true,
   329  		},
   330  	}
   331  	err := survey.AskOne(q, &result)
   332  	return result, err
   333  }
   334  
   335  func multiSelectSurvey(message string, defaults, options []string) ([]string, error) {
   336  	if len(options) == 0 {
   337  		return nil, nil
   338  	}
   339  	var results []string
   340  	q := &survey.MultiSelect{
   341  		Message: message,
   342  		Options: options,
   343  		Default: defaults,
   344  	}
   345  	err := survey.AskOne(q, &results)
   346  	return results, err
   347  }
   348  
   349  func milestoneSurvey(title string, opts []string) (string, error) {
   350  	if len(opts) == 0 {
   351  		return "", nil
   352  	}
   353  	var result string
   354  	q := &survey.Select{
   355  		Message: "Milestone",
   356  		Options: opts,
   357  		Default: title,
   358  	}
   359  	err := survey.AskOne(q, &result)
   360  	return result, err
   361  }
   362  
   363  func confirmSurvey() (bool, error) {
   364  	var result bool
   365  	q := &survey.Confirm{
   366  		Message: "Submit?",
   367  		Default: true,
   368  	}
   369  	err := survey.AskOne(q, &result)
   370  	return result, err
   371  }