1package shared 2 3import ( 4 "fmt" 5 "strings" 6 7 "github.com/AlecAivazis/survey/v2" 8 "github.com/cli/cli/v2/api" 9 "github.com/cli/cli/v2/internal/ghrepo" 10 "github.com/cli/cli/v2/pkg/set" 11 "github.com/cli/cli/v2/pkg/surveyext" 12) 13 14type 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 26type EditableString struct { 27 Value string 28 Default string 29 Options []string 30 Edited bool 31} 32 33type 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 43func (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 54func (e Editable) TitleValue() *string { 55 if !e.Title.Edited { 56 return nil 57 } 58 return &e.Title.Value 59} 60 61func (e Editable) BodyValue() *string { 62 if !e.Body.Edited { 63 return nil 64 } 65 return &e.Body.Value 66} 67 68func (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 99func (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 123func (e Editable) ProjectIds() (*[]string, error) { 124 if !e.Projects.Edited { 125 return nil, nil 126 } 127 if len(e.Projects.Add) != 0 || len(e.Projects.Remove) != 0 { 128 s := set.NewStringSet() 129 s.AddValues(e.Projects.Default) 130 s.AddValues(e.Projects.Add) 131 s.RemoveValues(e.Projects.Remove) 132 e.Projects.Value = s.ToSlice() 133 } 134 p, err := e.Metadata.ProjectsToIDs(e.Projects.Value) 135 return &p, err 136} 137 138func (e Editable) MilestoneId() (*string, error) { 139 if !e.Milestone.Edited { 140 return nil, nil 141 } 142 if e.Milestone.Value == noMilestone || e.Milestone.Value == "" { 143 s := "" 144 return &s, nil 145 } 146 m, err := e.Metadata.MilestoneToID(e.Milestone.Value) 147 return &m, err 148} 149 150func EditFieldsSurvey(editable *Editable, editorCommand string) error { 151 var err error 152 if editable.Title.Edited { 153 editable.Title.Value, err = titleSurvey(editable.Title.Default) 154 if err != nil { 155 return err 156 } 157 } 158 if editable.Body.Edited { 159 editable.Body.Value, err = bodySurvey(editable.Body.Default, editorCommand) 160 if err != nil { 161 return err 162 } 163 } 164 if editable.Reviewers.Edited { 165 editable.Reviewers.Value, err = multiSelectSurvey("Reviewers", editable.Reviewers.Default, editable.Reviewers.Options) 166 if err != nil { 167 return err 168 } 169 } 170 if editable.Assignees.Edited { 171 editable.Assignees.Value, err = multiSelectSurvey("Assignees", editable.Assignees.Default, editable.Assignees.Options) 172 if err != nil { 173 return err 174 } 175 } 176 if editable.Labels.Edited { 177 editable.Labels.Add, err = multiSelectSurvey("Labels", editable.Labels.Default, editable.Labels.Options) 178 if err != nil { 179 return err 180 } 181 for _, prev := range editable.Labels.Default { 182 var found bool 183 for _, selected := range editable.Labels.Add { 184 if prev == selected { 185 found = true 186 break 187 } 188 } 189 if !found { 190 editable.Labels.Remove = append(editable.Labels.Remove, prev) 191 } 192 } 193 } 194 if editable.Projects.Edited { 195 editable.Projects.Value, err = multiSelectSurvey("Projects", editable.Projects.Default, editable.Projects.Options) 196 if err != nil { 197 return err 198 } 199 } 200 if editable.Milestone.Edited { 201 editable.Milestone.Value, err = milestoneSurvey(editable.Milestone.Default, editable.Milestone.Options) 202 if err != nil { 203 return err 204 } 205 } 206 confirm, err := confirmSurvey() 207 if err != nil { 208 return err 209 } 210 if !confirm { 211 return fmt.Errorf("Discarding...") 212 } 213 214 return nil 215} 216 217func FieldsToEditSurvey(editable *Editable) error { 218 contains := func(s []string, str string) bool { 219 for _, v := range s { 220 if v == str { 221 return true 222 } 223 } 224 return false 225 } 226 227 opts := []string{"Title", "Body"} 228 if editable.Reviewers.Allowed { 229 opts = append(opts, "Reviewers") 230 } 231 opts = append(opts, "Assignees", "Labels", "Projects", "Milestone") 232 results, err := multiSelectSurvey("What would you like to edit?", []string{}, opts) 233 if err != nil { 234 return err 235 } 236 237 if contains(results, "Title") { 238 editable.Title.Edited = true 239 } 240 if contains(results, "Body") { 241 editable.Body.Edited = true 242 } 243 if contains(results, "Reviewers") { 244 editable.Reviewers.Edited = true 245 } 246 if contains(results, "Assignees") { 247 editable.Assignees.Edited = true 248 } 249 if contains(results, "Labels") { 250 editable.Labels.Edited = true 251 } 252 if contains(results, "Projects") { 253 editable.Projects.Edited = true 254 } 255 if contains(results, "Milestone") { 256 editable.Milestone.Edited = true 257 } 258 259 return nil 260} 261 262func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) error { 263 input := api.RepoMetadataInput{ 264 Reviewers: editable.Reviewers.Edited, 265 Assignees: editable.Assignees.Edited, 266 Labels: editable.Labels.Edited, 267 Projects: editable.Projects.Edited, 268 Milestones: editable.Milestone.Edited, 269 } 270 metadata, err := api.RepoMetadata(client, repo, input) 271 if err != nil { 272 return err 273 } 274 275 var users []string 276 for _, u := range metadata.AssignableUsers { 277 users = append(users, u.Login) 278 } 279 var teams []string 280 for _, t := range metadata.Teams { 281 teams = append(teams, fmt.Sprintf("%s/%s", repo.RepoOwner(), t.Slug)) 282 } 283 var labels []string 284 for _, l := range metadata.Labels { 285 labels = append(labels, l.Name) 286 } 287 var projects []string 288 for _, l := range metadata.Projects { 289 projects = append(projects, l.Name) 290 } 291 milestones := []string{noMilestone} 292 for _, m := range metadata.Milestones { 293 milestones = append(milestones, m.Title) 294 } 295 296 editable.Metadata = *metadata 297 editable.Reviewers.Options = append(users, teams...) 298 editable.Assignees.Options = users 299 editable.Labels.Options = labels 300 editable.Projects.Options = projects 301 editable.Milestone.Options = milestones 302 303 return nil 304} 305 306func titleSurvey(title string) (string, error) { 307 var result string 308 q := &survey.Input{ 309 Message: "Title", 310 Default: title, 311 } 312 err := survey.AskOne(q, &result) 313 return result, err 314} 315 316func bodySurvey(body, editorCommand string) (string, error) { 317 var result string 318 q := &surveyext.GhEditor{ 319 EditorCommand: editorCommand, 320 Editor: &survey.Editor{ 321 Message: "Body", 322 FileName: "*.md", 323 Default: body, 324 HideDefault: true, 325 AppendDefault: true, 326 }, 327 } 328 err := survey.AskOne(q, &result) 329 return result, err 330} 331 332func multiSelectSurvey(message string, defaults, options []string) ([]string, error) { 333 if len(options) == 0 { 334 return nil, nil 335 } 336 var results []string 337 q := &survey.MultiSelect{ 338 Message: message, 339 Options: options, 340 Default: defaults, 341 } 342 err := survey.AskOne(q, &results) 343 return results, err 344} 345 346func milestoneSurvey(title string, opts []string) (string, error) { 347 if len(opts) == 0 { 348 return "", nil 349 } 350 var result string 351 q := &survey.Select{ 352 Message: "Milestone", 353 Options: opts, 354 Default: title, 355 } 356 err := survey.AskOne(q, &result) 357 return result, err 358} 359 360func confirmSurvey() (bool, error) { 361 var result bool 362 q := &survey.Confirm{ 363 Message: "Submit?", 364 Default: true, 365 } 366 err := survey.AskOne(q, &result) 367 return result, err 368} 369