1package shared
2
3import (
4	"context"
5	"net/http"
6
7	"github.com/cli/cli/v2/api"
8	"github.com/cli/cli/v2/internal/ghinstance"
9	"github.com/cli/cli/v2/internal/ghrepo"
10	graphql "github.com/cli/shurcooL-graphql"
11	"github.com/shurcooL/githubv4"
12	"golang.org/x/sync/errgroup"
13)
14
15func UpdateIssue(httpClient *http.Client, repo ghrepo.Interface, id string, isPR bool, options Editable) error {
16	var wg errgroup.Group
17
18	// Labels are updated through discrete mutations to avoid having to replace the entire list of labels
19	// and risking race conditions.
20	if options.Labels.Edited {
21		if len(options.Labels.Add) > 0 {
22			wg.Go(func() error {
23				addedLabelIds, err := options.Metadata.LabelsToIDs(options.Labels.Add)
24				if err != nil {
25					return err
26				}
27				return addLabels(httpClient, id, repo, addedLabelIds)
28			})
29		}
30		if len(options.Labels.Remove) > 0 {
31			wg.Go(func() error {
32				removeLabelIds, err := options.Metadata.LabelsToIDs(options.Labels.Remove)
33				if err != nil {
34					return err
35				}
36				return removeLabels(httpClient, id, repo, removeLabelIds)
37			})
38		}
39	}
40
41	if dirtyExcludingLabels(options) {
42		wg.Go(func() error {
43			return replaceIssueFields(httpClient, repo, id, isPR, options)
44		})
45	}
46
47	return wg.Wait()
48}
49
50func replaceIssueFields(httpClient *http.Client, repo ghrepo.Interface, id string, isPR bool, options Editable) error {
51	apiClient := api.NewClientFromHTTP(httpClient)
52	assigneeIds, err := options.AssigneeIds(apiClient, repo)
53	if err != nil {
54		return err
55	}
56
57	projectIds, err := options.ProjectIds()
58	if err != nil {
59		return err
60	}
61
62	milestoneId, err := options.MilestoneId()
63	if err != nil {
64		return err
65	}
66
67	if isPR {
68		params := githubv4.UpdatePullRequestInput{
69			PullRequestID: id,
70			Title:         ghString(options.TitleValue()),
71			Body:          ghString(options.BodyValue()),
72			AssigneeIDs:   ghIds(assigneeIds),
73			ProjectIDs:    ghIds(projectIds),
74			MilestoneID:   ghId(milestoneId),
75		}
76		if options.Base.Edited {
77			params.BaseRefName = ghString(&options.Base.Value)
78		}
79		return updatePullRequest(httpClient, repo, params)
80	}
81
82	params := githubv4.UpdateIssueInput{
83		ID:          id,
84		Title:       ghString(options.TitleValue()),
85		Body:        ghString(options.BodyValue()),
86		AssigneeIDs: ghIds(assigneeIds),
87		ProjectIDs:  ghIds(projectIds),
88		MilestoneID: ghId(milestoneId),
89	}
90	return updateIssue(httpClient, repo, params)
91}
92
93func dirtyExcludingLabels(e Editable) bool {
94	return e.Title.Edited ||
95		e.Body.Edited ||
96		e.Base.Edited ||
97		e.Reviewers.Edited ||
98		e.Assignees.Edited ||
99		e.Projects.Edited ||
100		e.Milestone.Edited
101}
102
103func addLabels(httpClient *http.Client, id string, repo ghrepo.Interface, labels []string) error {
104	params := githubv4.AddLabelsToLabelableInput{
105		LabelableID: id,
106		LabelIDs:    *ghIds(&labels),
107	}
108
109	var mutation struct {
110		AddLabelsToLabelable struct {
111			Typename string `graphql:"__typename"`
112		} `graphql:"addLabelsToLabelable(input: $input)"`
113	}
114
115	variables := map[string]interface{}{"input": params}
116	gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), httpClient)
117	return gql.MutateNamed(context.Background(), "LabelAdd", &mutation, variables)
118}
119
120func removeLabels(httpClient *http.Client, id string, repo ghrepo.Interface, labels []string) error {
121	params := githubv4.RemoveLabelsFromLabelableInput{
122		LabelableID: id,
123		LabelIDs:    *ghIds(&labels),
124	}
125
126	var mutation struct {
127		RemoveLabelsFromLabelable struct {
128			Typename string `graphql:"__typename"`
129		} `graphql:"removeLabelsFromLabelable(input: $input)"`
130	}
131
132	variables := map[string]interface{}{"input": params}
133	gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), httpClient)
134	return gql.MutateNamed(context.Background(), "LabelRemove", &mutation, variables)
135}
136
137func updateIssue(httpClient *http.Client, repo ghrepo.Interface, params githubv4.UpdateIssueInput) error {
138	var mutation struct {
139		UpdateIssue struct {
140			Typename string `graphql:"__typename"`
141		} `graphql:"updateIssue(input: $input)"`
142	}
143	variables := map[string]interface{}{"input": params}
144	gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), httpClient)
145	return gql.MutateNamed(context.Background(), "IssueUpdate", &mutation, variables)
146}
147
148func updatePullRequest(httpClient *http.Client, repo ghrepo.Interface, params githubv4.UpdatePullRequestInput) error {
149	var mutation struct {
150		UpdatePullRequest struct {
151			Typename string `graphql:"__typename"`
152		} `graphql:"updatePullRequest(input: $input)"`
153	}
154	variables := map[string]interface{}{"input": params}
155	gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), httpClient)
156	err := gql.MutateNamed(context.Background(), "PullRequestUpdate", &mutation, variables)
157	return err
158}
159
160func ghIds(s *[]string) *[]githubv4.ID {
161	if s == nil {
162		return nil
163	}
164	ids := make([]githubv4.ID, len(*s))
165	for i, v := range *s {
166		ids[i] = v
167	}
168	return &ids
169}
170
171func ghId(s *string) *githubv4.ID {
172	if s == nil {
173		return nil
174	}
175	if *s == "" {
176		r := githubv4.ID(nil)
177		return &r
178	}
179	r := githubv4.ID(*s)
180	return &r
181}
182
183func ghString(s *string) *githubv4.String {
184	if s == nil {
185		return nil
186	}
187	r := githubv4.String(*s)
188	return &r
189}
190