@@ -9,19 +9,47 @@ import (
99 "github.com/cli/cli/v2/internal/ghrepo"
1010 graphql "github.com/cli/shurcooL-graphql"
1111 "github.com/shurcooL/githubv4"
12+ "golang.org/x/sync/errgroup"
1213)
1314
1415func UpdateIssue (httpClient * http.Client , repo ghrepo.Interface , id string , isPR bool , options Editable ) error {
15- title := ghString (options .TitleValue ())
16- body := ghString (options .BodyValue ())
16+ var wg errgroup.Group
1717
18- apiClient := api .NewClientFromHTTP (httpClient )
19- assigneeIds , err := options .AssigneeIds (apiClient , repo )
20- if err != nil {
21- return err
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+ })
2245 }
2346
24- labelIds , err := options .LabelIds ()
47+ return wg .Wait ()
48+ }
49+
50+ func 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 )
2553 if err != nil {
2654 return err
2755 }
@@ -39,10 +67,9 @@ func UpdateIssue(httpClient *http.Client, repo ghrepo.Interface, id string, isPR
3967 if isPR {
4068 params := githubv4.UpdatePullRequestInput {
4169 PullRequestID : id ,
42- Title : title ,
43- Body : body ,
70+ Title : ghString ( options . TitleValue ()) ,
71+ Body : ghString ( options . BodyValue ()) ,
4472 AssigneeIDs : ghIds (assigneeIds ),
45- LabelIDs : ghIds (labelIds ),
4673 ProjectIDs : ghIds (projectIds ),
4774 MilestoneID : ghId (milestoneId ),
4875 }
@@ -52,23 +79,65 @@ func UpdateIssue(httpClient *http.Client, repo ghrepo.Interface, id string, isPR
5279 return updatePullRequest (httpClient , repo , params )
5380 }
5481
55- return updateIssue ( httpClient , repo , githubv4.UpdateIssueInput {
82+ params := githubv4.UpdateIssueInput {
5683 ID : id ,
57- Title : title ,
58- Body : body ,
84+ Title : ghString ( options . TitleValue ()) ,
85+ Body : ghString ( options . BodyValue ()) ,
5986 AssigneeIDs : ghIds (assigneeIds ),
60- LabelIDs : ghIds (labelIds ),
6187 ProjectIDs : ghIds (projectIds ),
6288 MilestoneID : ghId (milestoneId ),
63- })
89+ }
90+ return updateIssue (httpClient , repo , params )
91+ }
92+
93+ func 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+
103+ func 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+
120+ func 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 )
64135}
65136
66137func updateIssue (httpClient * http.Client , repo ghrepo.Interface , params githubv4.UpdateIssueInput ) error {
67138 var mutation struct {
68139 UpdateIssue struct {
69- Issue struct {
70- ID string
71- }
140+ Typename string `graphql:"__typename"`
72141 } `graphql:"updateIssue(input: $input)"`
73142 }
74143 variables := map [string ]interface {}{"input" : params }
@@ -79,9 +148,7 @@ func updateIssue(httpClient *http.Client, repo ghrepo.Interface, params githubv4
79148func updatePullRequest (httpClient * http.Client , repo ghrepo.Interface , params githubv4.UpdatePullRequestInput ) error {
80149 var mutation struct {
81150 UpdatePullRequest struct {
82- PullRequest struct {
83- ID string
84- }
151+ Typename string `graphql:"__typename"`
85152 } `graphql:"updatePullRequest(input: $input)"`
86153 }
87154 variables := map [string ]interface {}{"input" : params }
0 commit comments