go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/bisection/internal/gerrit/gerrit.go (about) 1 // Copyright 2022 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package gerrit contains logic for interacting with Gerrit 16 package gerrit 17 18 import ( 19 "context" 20 "fmt" 21 "net/http" 22 "time" 23 24 "go.chromium.org/luci/common/api/gerrit" 25 "go.chromium.org/luci/common/errors" 26 "go.chromium.org/luci/common/logging" 27 gerritpb "go.chromium.org/luci/common/proto/gerrit" 28 "go.chromium.org/luci/server/auth" 29 ) 30 31 // The options to use when querying Gerrit for changes 32 var queryOptions = []gerritpb.QueryOption{ 33 gerritpb.QueryOption_LABELS, 34 gerritpb.QueryOption_CURRENT_REVISION, 35 gerritpb.QueryOption_CURRENT_COMMIT, 36 gerritpb.QueryOption_DETAILED_ACCOUNTS, 37 gerritpb.QueryOption_MESSAGES, 38 gerritpb.QueryOption_CHANGE_ACTIONS, 39 gerritpb.QueryOption_SKIP_MERGEABLE, 40 gerritpb.QueryOption_CHECK, 41 } 42 43 // mockedGerritClientKey is the context key to indicate using mocked 44 // Gerrit client in tests 45 var mockedGerritClientKey = "mock Gerrit client" 46 47 // Client is the client to communicate with Gerrit 48 // It wraps a gerritpb.GerritClient 49 type Client struct { 50 gerritClient gerritpb.GerritClient 51 host string 52 } 53 54 func newGerritClient(ctx context.Context, host string) (gerritpb.GerritClient, error) { 55 if mockClient, ok := ctx.Value(&mockedGerritClientKey).(*gerritpb.MockGerritClient); ok { 56 // return a mock Gerrit client for tests 57 return mockClient, nil 58 } 59 60 t, err := auth.GetRPCTransport(ctx, auth.AsSelf, auth.WithScopes(gerrit.OAuthScope)) 61 if err != nil { 62 return nil, err 63 } 64 65 return gerrit.NewRESTClient(&http.Client{Transport: t}, host, true) 66 } 67 68 // NewClient creates a client to communicate with Gerrit 69 func NewClient(ctx context.Context, host string) (*Client, error) { 70 client, err := newGerritClient(ctx, host) 71 if err != nil { 72 return nil, errors.Annotate(err, "error making Gerrit client for host %s", host).Err() 73 } 74 75 return &Client{ 76 gerritClient: client, 77 host: host, 78 }, nil 79 } 80 81 // Host returns the Gerrit host string 82 func (c *Client) Host(ctx context.Context) string { 83 return c.host 84 } 85 86 // queryChanges gets the info for corresponding change(s) given the query string. 87 func (c *Client) queryChanges(ctx context.Context, query string) ([]*gerritpb.ChangeInfo, error) { 88 req := &gerritpb.ListChangesRequest{ 89 Query: query, 90 Options: queryOptions, 91 } 92 93 res, err := c.gerritClient.ListChanges(ctx, req) 94 if err != nil { 95 return nil, err 96 } 97 98 return res.Changes, nil 99 } 100 101 // GetChange gets the corresponding change info given the commit ID. 102 // This function returns an error if none or more than 1 changes are returned 103 // by Gerrit. 104 func (c *Client) GetChange(ctx context.Context, project string, commitID string) (*gerritpb.ChangeInfo, error) { 105 query := fmt.Sprintf("project:\"%s\" commit:\"%s\"", project, commitID) 106 changes, err := c.queryChanges(ctx, query) 107 if err != nil { 108 return nil, errors.Annotate(err, "error getting change from Gerrit host %s using query %s", 109 c.host, query).Err() 110 } 111 112 if len(changes) == 0 { 113 return nil, fmt.Errorf("no change found from Gerrit host %s using query %s", 114 c.host, query, 115 ) 116 } 117 118 if len(changes) > 1 { 119 return nil, fmt.Errorf("multiple changes found from Gerrit host %s using query %s", 120 c.host, query, 121 ) 122 } 123 124 return changes[0], nil 125 } 126 127 // RefetchChange queries Gerrit for the given change, and returns the latest 128 // state of the change 129 func (c *Client) RefetchChange(ctx context.Context, change *gerritpb.ChangeInfo) (*gerritpb.ChangeInfo, error) { 130 req := &gerritpb.GetChangeRequest{ 131 Project: change.Project, 132 Number: change.Number, 133 Options: queryOptions, 134 } 135 136 res, err := c.gerritClient.GetChange(ctx, req) 137 if err != nil { 138 return nil, err 139 } 140 141 return res, nil 142 } 143 144 // GetReverts gets the corresponding revert(s) for the given change. 145 func (c *Client) GetReverts(ctx context.Context, change *gerritpb.ChangeInfo) ([]*gerritpb.ChangeInfo, error) { 146 query := fmt.Sprintf("project:\"%s\" revertof:%d", change.Project, change.Number) 147 changes, err := c.queryChanges(ctx, query) 148 if err != nil { 149 return nil, errors.Annotate(err, "error getting reverts of a change from Gerrit host %s using query %s", 150 c.host, query, 151 ).Err() 152 } 153 154 return changes, nil 155 } 156 157 // HasDependency returns whether the change has another merged change depending on it 158 func (c *Client) HasDependency(ctx context.Context, change *gerritpb.ChangeInfo) (bool, error) { 159 relatedChanges, err := c.getRelatedChanges(ctx, change) 160 if err != nil { 161 return false, errors.Annotate(err, "failed checking dependency").Err() 162 } 163 164 for _, relatedChange := range relatedChanges { 165 if relatedChange.Status == gerritpb.ChangeStatus_MERGED { 166 // relatedChange here is the newest merged. If relatedChange != change, 167 // then there is a merged dependency 168 return relatedChange.Project != change.Project || 169 relatedChange.Number != change.Number, nil 170 } 171 } 172 173 // none of the related changes are merged, so no merged dependencies 174 return false, nil 175 } 176 177 // CreateRevert creates a revert change in Gerrit for the specified change. 178 func (c *Client) CreateRevert(ctx context.Context, change *gerritpb.ChangeInfo, message string) (*gerritpb.ChangeInfo, error) { 179 logging.Debugf(ctx, "gerrit Client.CreateRevert message: '%s'", message) 180 req := &gerritpb.RevertChangeRequest{ 181 Project: change.Project, 182 Number: change.Number, 183 Message: message, 184 } 185 186 // Set timeout for creating a revert 187 waitCtx, cancel := context.WithTimeout(ctx, time.Minute*1) 188 defer cancel() 189 190 res, err := c.gerritClient.RevertChange(waitCtx, req) 191 if err != nil { 192 return nil, errors.Annotate(err, "error creating revert change on Gerrit host %s for change %s~%d", 193 c.host, req.Project, req.Number).Err() 194 } 195 196 return res, nil 197 } 198 199 // AddComment adds the given message as a review comment on a change 200 func (c *Client) AddComment(ctx context.Context, change *gerritpb.ChangeInfo, message string) (*gerritpb.ReviewResult, error) { 201 req := c.createSetReviewRequest(ctx, change, message) 202 res, err := c.setReview(ctx, req) 203 if err != nil { 204 return nil, errors.Annotate(err, "error adding comment").Err() 205 } 206 207 return res, nil 208 } 209 210 // SendForReview adds the emails as reviewers for the 211 // change, and sets the change to be ready for review 212 func (c *Client) SendForReview(ctx context.Context, change *gerritpb.ChangeInfo, message string, 213 reviewerEmails []string, ccEmails []string) (*gerritpb.ReviewResult, error) { 214 req := c.createSetReviewRequest(ctx, change, message) 215 216 // Add reviewer and CC emails to the change 217 reviewerCount := len(reviewerEmails) 218 reviewerInputs := make([]*gerritpb.ReviewerInput, reviewerCount+len(ccEmails)) 219 for i, email := range reviewerEmails { 220 reviewerInputs[i] = &gerritpb.ReviewerInput{ 221 Reviewer: email, 222 State: gerritpb.ReviewerInput_REVIEWER_INPUT_STATE_REVIEWER, 223 } 224 } 225 for i, email := range ccEmails { 226 reviewerInputs[reviewerCount+i] = &gerritpb.ReviewerInput{ 227 Reviewer: email, 228 State: gerritpb.ReviewerInput_REVIEWER_INPUT_STATE_CC, 229 } 230 } 231 req.Reviewers = reviewerInputs 232 233 res, err := c.setReview(ctx, req) 234 if err != nil { 235 return nil, errors.Annotate(err, "error sending for review").Err() 236 } 237 return res, nil 238 } 239 240 // CommitRevert bot-commits the revert change. The change must be a pure revert; 241 // if not, this function does not attempt to commit the change and returns an error. 242 func (c *Client) CommitRevert(ctx context.Context, change *gerritpb.ChangeInfo, 243 message string, ccEmails []string) (*gerritpb.ReviewResult, error) { 244 // Check the change is a pure revert 245 isRevert, err := c.isPureRevert(ctx, change) 246 if err != nil { 247 return nil, err 248 } 249 250 if !isRevert { 251 return nil, fmt.Errorf( 252 "failed to commit change on Gerrit host %s - change %s~%d is not a pure revert", 253 c.host, change.Project, change.Number) 254 } 255 256 req := c.createSetReviewRequest(ctx, change, message) 257 258 // Add CC emails to the change 259 reviewerInputs := make([]*gerritpb.ReviewerInput, len(ccEmails)) 260 for i, email := range ccEmails { 261 reviewerInputs[i] = &gerritpb.ReviewerInput{ 262 Reviewer: email, 263 State: gerritpb.ReviewerInput_REVIEWER_INPUT_STATE_CC, 264 } 265 } 266 req.Reviewers = reviewerInputs 267 268 // Specify the labels required to submit the change to CQ 269 req.Labels = map[string]int32{ 270 "Owners-Override": 1, 271 "Bot-Commit": 1, 272 "Commit-Queue": 2, 273 } 274 275 res, err := c.setReview(ctx, req) 276 if err != nil { 277 return nil, errors.Annotate(err, "error committing").Err() 278 } 279 280 return res, nil 281 } 282 283 // createSetReviewRequest is a helper to create a basic SetReviewRequest 284 func (c *Client) createSetReviewRequest(ctx context.Context, change *gerritpb.ChangeInfo, 285 message string) *gerritpb.SetReviewRequest { 286 return &gerritpb.SetReviewRequest{ 287 Project: change.Project, 288 Number: change.Number, 289 RevisionId: "current", 290 Message: message, 291 } 292 } 293 294 // getRelatedChanges is a helper to call the Gerrit client GetRelatedChanges function 295 func (c *Client) getRelatedChanges(ctx context.Context, change *gerritpb.ChangeInfo) ([]*gerritpb.GetRelatedChangesResponse_ChangeAndCommit, error) { 296 req := &gerritpb.GetRelatedChangesRequest{ 297 Project: change.Project, 298 Number: change.Number, 299 RevisionId: "current", 300 } 301 302 res, err := c.gerritClient.GetRelatedChanges(ctx, req) 303 if err != nil { 304 return nil, errors.Annotate(err, "failed getting related changes from Gerrit host %s for change %s~%d", 305 c.host, req.Project, req.Number, 306 ).Err() 307 } 308 309 // Changes are sorted by git commit order, newest to oldest. 310 // Empty if there are no related changes. See: 311 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#related-changes-info 312 return res.Changes, nil 313 } 314 315 // isPureRevert is a helper to call the Gerrit client GetPureRevert function, 316 // and returns whether the change is a pure revert of the change 317 // referenced in its "revertOf" field. See: 318 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-pure-revert 319 func (c *Client) isPureRevert(ctx context.Context, change *gerritpb.ChangeInfo) (bool, error) { 320 req := &gerritpb.GetPureRevertRequest{ 321 Project: change.Project, 322 Number: change.Number, 323 } 324 325 res, err := c.gerritClient.GetPureRevert(ctx, req) 326 if err != nil { 327 return false, errors.Annotate(err, 328 "error querying Gerrit host %s on whether the change %s~%d is a pure revert", 329 c.host, req.Project, req.Number).Err() 330 } 331 332 return res.IsPureRevert, nil 333 } 334 335 // setReview is a helper to call the Gerrit client SetReview function 336 func (c *Client) setReview(ctx context.Context, req *gerritpb.SetReviewRequest) (*gerritpb.ReviewResult, error) { 337 res, err := c.gerritClient.SetReview(ctx, req) 338 if err != nil { 339 return nil, errors.Annotate(err, "failed to set review on Gerrit host %s for change %s~%d", 340 c.host, req.Project, req.Number).Err() 341 } 342 343 return res, nil 344 }