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