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