go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/api/gerrit/gerrit.go (about) 1 // Copyright 2017 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 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/json" 21 "fmt" 22 "io" 23 "net/http" 24 "net/url" 25 "strconv" 26 "strings" 27 28 "go.chromium.org/luci/common/errors" 29 "go.chromium.org/luci/common/retry" 30 "go.chromium.org/luci/common/retry/transient" 31 32 "golang.org/x/net/context/ctxhttp" 33 ) 34 35 const contentType = "application/json; charset=UTF-8" 36 37 // Change represents a Gerrit CL. Information about these fields in: 38 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info 39 // 40 // Note that not all fields will be filled for all CLs and queries depending on 41 // query options, and not all fields exposed by Gerrit are captured by this 42 // struct. Adding more fields to this struct should be okay (but only 43 // Gerrit-supported keys will be populated). 44 // 45 // TODO(nodir): replace this type with 46 // https://godoc.org/go.chromium.org/luci/common/proto/gerrit#ChangeInfo. 47 type Change struct { 48 ChangeNumber int `json:"_number"` 49 ID string `json:"id"` 50 ChangeID string `json:"change_id"` 51 Project string `json:"project"` 52 Branch string `json:"branch"` 53 Topic string `json:"topic"` 54 Hashtags []string `json:"hashtags"` 55 Subject string `json:"subject"` 56 Status string `json:"status"` 57 Created string `json:"created"` 58 Updated string `json:"updated"` 59 Mergeable bool `json:"mergeable,omitempty"` 60 Messages []ChangeMessageInfo `json:"messages"` 61 Submittable bool `json:"submittable,omitempty"` 62 Submitted string `json:"submitted"` 63 SubmitType string `json:"submit_type"` 64 Insertions int `json:"insertions"` 65 Deletions int `json:"deletions"` 66 UnresolvedCommentCount int `json:"unresolved_comment_count"` 67 HasReviewStarted bool `json:"has_review_started"` 68 Owner AccountInfo `json:"owner"` 69 Labels map[string]LabelInfo `json:"labels"` 70 Submitter AccountInfo `json:"submitter"` 71 Reviewers Reviewers `json:"reviewers"` 72 RevertOf int `json:"revert_of"` 73 CurrentRevision string `json:"current_revision"` 74 WorkInProgress bool `json:"work_in_progress,omitempty"` 75 CherryPickOfChange int `json:"cherry_pick_of_change"` 76 CherryPickOfPatchset int `json:"cherry_pick_of_patch_set"` 77 Revisions map[string]RevisionInfo `json:"revisions"` 78 // MoreChanges is not part of a Change, but gerrit piggy-backs on the 79 // last Change in a page to set this flag if there are more changes 80 // in the results of a query. 81 MoreChanges bool `json:"_more_changes"` 82 } 83 84 // ChangeMessageInfo contains information about a message attached to a change: 85 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-message-info 86 type ChangeMessageInfo struct { 87 ID string `json:"id"` 88 Author AccountInfo `json:"author,omitempty"` 89 RealAuthor AccountInfo `json:"real_author,omitempty"` 90 Date Timestamp `json:"date"` 91 Message string `json:"message"` 92 Tag string `json:"tag,omitempty"` 93 } 94 95 // LabelInfo contains information about a label on a change, always 96 // corresponding to the current patch set. 97 // 98 // Only one of Approved, Rejected, Recommended, and Disliked will be set, with 99 // priority Rejected > Approved > Recommended > Disliked if multiple users have 100 // given the label different values. 101 // 102 // TODO(mknyszek): Support 'value' and 'default_value' fields, as well as the 103 // fields given with the query parameter DETAILED_LABELS. 104 type LabelInfo struct { 105 // Optional reflects whether the label is optional (neither necessary 106 // for nor blocking submission). 107 Optional bool `json:"optional,omitempty"` 108 109 // Approved refers to one user who approved this label (max value). 110 Approved *AccountInfo `json:"approved,omitempty"` 111 112 // Rejected refers to one user who rejected this label (min value). 113 Rejected *AccountInfo `json:"rejected,omitempty"` 114 115 // Recommended refers to one user who recommended this label (positive 116 // value, but not max). 117 Recommended *AccountInfo `json:"recommended,omitempty"` 118 119 // Disliked refers to one user who disliked this label (negative value 120 // but not min). 121 Disliked *AccountInfo `json:"disliked,omitempty"` 122 123 // Blocking reflects whether this label block the submit operation. 124 Blocking bool `json:"blocking,omitempty"` 125 126 All []VoteInfo `json:"all,omitempty"` 127 Values map[string]string `json:"values,omitempty"` 128 } 129 130 // RevisionKind represents the "kind" field for a patch set. 131 type RevisionKind string 132 133 // Known revision kinds. 134 var ( 135 RevisionRework RevisionKind = "REWORK" 136 RevisionTrivialRebase = "TRIVIAL_REBASE" 137 RevisionMergeFirstParentUpdate = "MERGE_FIRST_PARENT_UPDATE" 138 RevisionNoCodeChange = "NO_CODE_CHANGE" 139 RevisionNoChange = "NO_CHANGE" 140 ) 141 142 // RevisionInfo contains information about a specific patch set. 143 // 144 // Corresponds with 145 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-info. 146 // TODO(mknyszek): Support the rest of that structure. 147 type RevisionInfo struct { 148 // PatchSetNumber is the number associated with the given patch set. 149 PatchSetNumber int `json:"_number"` 150 151 // Kind is the kind of revision this is. 152 // 153 // Allowed values are specified in the RevisionKind type. 154 Kind RevisionKind `json:"kind"` 155 156 // Ref is the Git reference for the patchset. 157 Ref string `json:"ref"` 158 159 // Uploader represents the account which uploaded this patch set. 160 Uploader AccountInfo `json:"uploader"` 161 162 // The description of this patchset, as displayed in the patchset selector menu. 163 Description string `json:"description,omitempty"` 164 165 // The commit associated with this revision. 166 Commit CommitInfo `json:"commit,omitempty"` 167 168 // A map of modified file paths to FileInfos. 169 Files map[string]FileInfo `json:"files,omitempty"` 170 } 171 172 // CommitInfo contains information about a commit. 173 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#commit-info 174 // 175 // TODO(kjharland): Support remaining fields. 176 type CommitInfo struct { 177 Commit string `json:"commit,omitempty"` 178 Parents []CommitInfo `json:"parents,omitempty"` 179 Author AccountInfo `json:"author,omitempty"` 180 Committer AccountInfo `json:"committer,omitempty"` 181 Subject string `json:"subject,omitempty"` 182 Message string `json:"message,omitempty"` 183 } 184 185 // FileInfo contains information about a file in a patch set. 186 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#file-info 187 type FileInfo struct { 188 Status string `json:"status,omitempty"` 189 Binary bool `json:"binary,omitempty"` 190 OldPath string `json:"old_path,omitempty"` 191 LinesInserted int `json:"lines_inserted,omitempty"` 192 LinesDeleted int `json:"lines_deleted,omitempty"` 193 SizeDelta int64 `json:"size_delta"` 194 Size int64 `json:"size"` 195 } 196 197 // Reviewers is a map that maps a type of reviewer to its account info. 198 type Reviewers struct { 199 CC []AccountInfo `json:"CC"` 200 Reviewer []AccountInfo `json:"REVIEWER"` 201 Removed []AccountInfo `json:"REMOVED"` 202 } 203 204 // AccountInfo contains information about an account. 205 type AccountInfo struct { 206 // AccountID is the numeric ID of the account managed by Gerrit, and is guaranteed to 207 // be unique. 208 AccountID int `json:"_account_id,omitempty"` 209 210 // Name is the full name of the user. 211 Name string `json:"name,omitempty"` 212 213 // Email is the email address the user prefers to be contacted through. 214 Email string `json:"email,omitempty"` 215 216 // SecondaryEmails is a list of secondary email addresses of the user. 217 SecondaryEmails []string `json:"secondary_emails,omitempty"` 218 219 // Username is the username of the user. 220 Username string `json:"username,omitempty"` 221 222 // MoreAccounts represents whether the query would deliver more results if not limited. 223 // Only set on the last account that is returned. 224 MoreAccounts bool `json:"_more_accounts,omitempty"` 225 } 226 227 // VoteInfo is AccountInfo plus a value field, used to represent votes on a label. 228 type VoteInfo struct { 229 AccountInfo // Embedded type 230 Value int64 `json:"value"` 231 } 232 233 // SubmittedTogetherInfo contains information about a collection of changes that would be submitted together. 234 type SubmittedTogetherInfo struct { 235 236 // A list of ChangeInfo entities representing the changes to be submitted together. 237 Changes []Change `json:"changes"` 238 239 // The number of changes to be submitted together that the current user cannot see. 240 NonVisibleChanges int `json:"non_visible_changes,omitempty"` 241 } 242 243 // BranchInfo contains information about a branch. 244 type BranchInfo struct { 245 // Ref is the ref of the branch. 246 Ref string `json:"ref"` 247 248 // Revision is the revision to which the branch points. 249 Revision string `json:"revision"` 250 } 251 252 // ValidateGerritURL validates Gerrit URL for use in this package. 253 func ValidateGerritURL(gerritURL string) error { 254 _, err := NormalizeGerritURL(gerritURL) 255 return err 256 } 257 258 // NormalizeGerritURL returns canonical for Gerrit URL. 259 // 260 // error is returned if validation fails. 261 func NormalizeGerritURL(gerritURL string) (string, error) { 262 u, err := url.Parse(gerritURL) 263 if err != nil { 264 return "", err 265 } 266 if u.Scheme != "https" { 267 return "", fmt.Errorf("%s should start with https://", gerritURL) 268 } 269 if !strings.HasSuffix(u.Host, "-review.googlesource.com") { 270 return "", errors.New("only *-review.googlesource.com Gerrits supported") 271 } 272 if u.Fragment != "" { 273 return "", errors.New("no fragments allowed in gerritURL") 274 } 275 if u.Path != "" && u.Path != "/" { 276 return "", errors.New("Unexpected path in URL") 277 } 278 if u.Path != "/" { 279 u.Path = "/" 280 } 281 return u.String(), nil 282 } 283 284 // Client is a production Gerrit client, instantiated via NewClient(), 285 // and uses an http.Client along with a validated url to communicate with 286 // Gerrit. 287 type Client struct { 288 httpClient *http.Client 289 290 // Set by NewClient after validation. 291 gerritURL url.URL 292 293 // Strategy for retrying GET requests. Other requests are not retried as 294 // they may not be idempotent. 295 retryStrategy retry.Factory 296 } 297 298 // NewClient creates a new instance of Client and validates and stores 299 // the URL to reach Gerrit. The result is wrapped inside a Client so that 300 // the apis can be called directly on the value returned by this function. 301 func NewClient(c *http.Client, gerritURL string) (*Client, error) { 302 u, err := NormalizeGerritURL(gerritURL) 303 if err != nil { 304 return nil, err 305 } 306 pu, err := url.Parse(u) 307 if err != nil { 308 return nil, err 309 } 310 return &Client{c, *pu, retry.Default}, nil 311 } 312 313 // ChangeQueryParams contains the parameters necessary for querying changes from Gerrit. 314 type ChangeQueryParams struct { 315 // Actual query string, see 316 // https://gerrit-review.googlesource.com/Documentation/user-search.html#_search_operators 317 Query string `json:"q"` 318 // How many changes to include in the response. 319 N int `json:"n"` 320 // Skip this many from the list of results (unreliable for paging). 321 S int `json:"S"` 322 // Include these options in the queries. Certain options will make 323 // Gerrit fill in additional fields of the response. These require 324 // additional database searches and may delay the response. 325 // 326 // The supported strings for options are listed in Gerrit's api 327 // documentation at the link below: 328 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes 329 Options []string `json:"o"` 330 } 331 332 // queryString renders the ChangeQueryParams as a url.Values. 333 func (qr *ChangeQueryParams) queryString() url.Values { 334 qs := make(url.Values, len(qr.Options)+3) 335 qs.Add("q", qr.Query) 336 if qr.N > 0 { 337 qs.Add("n", strconv.Itoa(qr.N)) 338 } 339 if qr.S > 0 { 340 qs.Add("S", strconv.Itoa(qr.S)) 341 } 342 for _, o := range qr.Options { 343 qs.Add("o", o) 344 } 345 return qs 346 } 347 348 // ChangeQuery returns a list of Gerrit changes for a given ChangeQueryParams. 349 // 350 // One example use case for this is getting the CL for a given commit hash. 351 // Only the .Query property of the qr parameter is required. 352 // 353 // Returns a slice of Change, whether there are more changes to fetch 354 // and an error. 355 func (c *Client) ChangeQuery(ctx context.Context, qr ChangeQueryParams) ([]*Change, bool, error) { 356 var resp struct { 357 Collection []*Change 358 } 359 if _, err := c.get(ctx, "a/changes/", qr.queryString(), &resp.Collection); err != nil { 360 return nil, false, err 361 } 362 result := resp.Collection 363 moreChanges := false 364 if len(result) > 0 { 365 moreChanges = result[len(result)-1].MoreChanges 366 result[len(result)-1].MoreChanges = false 367 } 368 return result, moreChanges, nil 369 } 370 371 // ChangeDetailsParams contains optional parameters for getting change details from Gerrit. 372 type ChangeDetailsParams struct { 373 // Options is a set of optional details to add as additional fieldsof the response. 374 // These require additional database searches and may delay the response. 375 // 376 // The supported strings for options are listed in Gerrit's api 377 // documentation at the link below: 378 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes 379 Options []string `json:"o"` 380 } 381 382 // queryString renders the ChangeDetailsParams as a url.Values. 383 func (qr *ChangeDetailsParams) queryString() url.Values { 384 qs := make(url.Values, len(qr.Options)) 385 for _, o := range qr.Options { 386 qs.Add("o", o) 387 } 388 return qs 389 } 390 391 // ChangeDetails gets details about a single change with optional fields. 392 // 393 // This method returns a single *Change and an error. 394 // 395 // The changeID parameter may be in any of the forms supported by Gerrit: 396 // - "4247" 397 // - "I8473b95934b5732ac55d26311a706c9c2bde9940" 398 // - etc. See the link below. 399 // 400 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id 401 // 402 // options is a list of strings like {"CURRENT_REVISION"} which tells Gerrit 403 // to return non-default properties for Change. The supported strings for 404 // options are listed in Gerrit's api documentation at the link below: 405 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes 406 func (c *Client) ChangeDetails(ctx context.Context, changeID string, options ChangeDetailsParams) (*Change, error) { 407 var resp Change 408 path := fmt.Sprintf("a/changes/%s/detail", url.PathEscape(changeID)) 409 if _, err := c.get(ctx, path, options.queryString(), &resp); err != nil { 410 return nil, err 411 } 412 return &resp, nil 413 } 414 415 // The CommentInput entity contains information for creating an inline comment. 416 type CommentInput struct { 417 // The URL encoded UUID of the comment if an existing draft comment should be updated. 418 // Optional 419 ID string `json:"id,omitempty"` 420 421 // The file path for which the inline comment should be added. 422 // Doesn’t need to be set if contained in a map where the key is the file path. 423 // Optional 424 Path string `json:"path,omitempty"` 425 426 // The side on which the comment should be added. 427 // Allowed values are REVISION and PARENT. 428 // If not set, the default is REVISION. 429 // Optional 430 Side string `json:"side,omitempty"` 431 432 // The number of the line for which the comment should be added. 433 // 0 if it is a file comment. 434 // If neither line nor range is set, a file comment is added. 435 // If range is set, this value is ignored in favor of the end_line of the range. 436 // Optional 437 Line int `json:"line,omitempty"` 438 439 // The range of the comment as a CommentRange entity. 440 // Optional 441 Range CommentRange `json:"range,omitempty"` 442 443 // The URL encoded UUID of the comment to which this comment is a reply. 444 // Optional 445 InReplyTo string `json:"in_reply_to,omitempty"` 446 447 // The comment message. 448 // If not set and an existing draft comment is updated, the existing draft comment is deleted. 449 // Optional 450 Message string `json:"message,omitempty"` 451 452 // Value of the tag field. Only allowed on draft comment inputs; 453 // for published comments, use the tag field in ReviewInput. 454 // Votes/comments that contain tag with 'autogenerated:' prefix can be filtered out in the web UI. 455 // Optional 456 Tag string `json:"tag,omitempty"` 457 458 // Whether or not the comment must be addressed by the user. 459 // This value will default to false if the comment is an orphan, or the 460 // value of the in_reply_to comment if it is supplied. 461 // Optional 462 Unresolved bool `json:"unresolved,omitempty"` 463 } 464 465 // The RobotCommentInput entity contains information for creating an inline 466 // robot comment. 467 type RobotCommentInput struct { 468 // The name of the robot that generated this comment. 469 // Required 470 RobotID string `json:"robot_id"` 471 472 // A unique ID of the run of the robot. 473 // Required 474 RobotRunID string `json:"robot_run_id"` 475 476 // The file path for which the inline comment should be added. 477 Path string `json:"path,omitempty"` 478 479 // The side on which the comment should be added. 480 // Allowed values are REVISION and PARENT. 481 // If not set, the default is REVISION. 482 // Optional 483 Side string `json:"side,omitempty"` 484 485 // The number of the line for which the comment should be added. 486 // 0 if it is a file comment. 487 // If neither line nor range is set, a file comment is added. 488 // If range is set, this value is ignored in favor of the end_line of the range. 489 // Optional 490 Line int `json:"line,omitempty"` 491 492 // The range of the comment as a CommentRange entity. 493 // Optional 494 Range *CommentRange `json:"range,omitempty"` 495 496 // The URL encoded UUID of the comment to which this comment is a reply. 497 // Optional 498 InReplyTo string `json:"in_reply_to,omitempty"` 499 500 // The comment message. 501 // If not set and an existing draft comment is updated, the existing draft comment is deleted. 502 // Optional 503 Message string `json:"message,omitempty"` 504 505 // Suggested fixes. 506 FixSuggestions []FixSuggestion `json:"fix_suggestions,omitempty"` 507 } 508 509 // FixSuggestion represents a suggested fix for a robot comment. 510 type FixSuggestion struct { 511 Description string `json:"description"` 512 Replacements []Replacement `json:"replacements"` 513 } 514 515 // Replacement represents a potential source replacement for applying a 516 // suggested fix. 517 type Replacement struct { 518 Path string `json:"path"` 519 Replacement string `json:"replacement"` 520 Range *CommentRange `json:"range,omitempty"` 521 } 522 523 // CommentRange is included within Comment. See Comment for more details. 524 type CommentRange struct { 525 StartLine int `json:"start_line,omitempty"` 526 StartCharacter int `json:"start_character,omitempty"` 527 EndLine int `json:"end_line,omitempty"` 528 EndCharacter int `json:"end_character,omitempty"` 529 } 530 531 // Comment represents a comment on a Gerrit CL. Information about these fields 532 // is in: 533 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-info 534 // 535 // Note that not all fields will be filled for all comments depending on the 536 // way the comment was added to Gerrit, and not all fields exposed by Gerrit 537 // are captured by this struct. Adding more fields to this struct should be 538 // okay (but only Gerrit-supported keys will be populated). 539 type Comment struct { 540 ID string `json:"id"` 541 Owner AccountInfo `json:"author"` 542 ChangeMessageID string `json:"change_message_id"` 543 PatchSet int `json:"patch_set"` 544 Line int `json:"line"` 545 Range CommentRange `json:"range"` 546 Updated string `json:"updated"` 547 Message string `json:"message"` 548 Unresolved bool `json:"unresolved"` 549 InReplyTo string `json:"in_reply_to"` 550 CommitID string `json:"commit_id"` 551 } 552 553 // RobotComment represents a robot comment on a Gerrit CL. Information about 554 // these fields is in: 555 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#robot-comment-info 556 // 557 // RobotComment shares most of the same fields with Comment. Note that robot 558 // comments do not have the `unresolved` field so it will always be false. 559 type RobotComment struct { 560 Comment 561 562 RobotID string `json:"robot_id"` 563 RobotRunID string `json:"robot_run_id"` 564 URL string `json:"url"` 565 Properties map[string]string `json:"properties"` 566 } 567 568 // ListChangeComments gets all comments on a single change. 569 // 570 // This method returns a list of comments for each file path (including 571 // pseudo-files like '/PATCHSET_LEVEL' and '/COMMIT_MSG') and an error. 572 // 573 // The changeID parameter may be in any of the forms supported by Gerrit: 574 // - "4247" 575 // - "I8473b95934b5732ac55d26311a706c9c2bde9940" 576 // - etc. See the link below. 577 // 578 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id 579 func (c *Client) ListChangeComments(ctx context.Context, changeID string, revisionID string) (map[string][]Comment, error) { 580 var resp map[string][]Comment 581 var path string 582 if revisionID != "" { 583 path = fmt.Sprintf("a/changes/%s/revisions/%s/comments", url.PathEscape(changeID), url.PathEscape(revisionID)) 584 } else { 585 path = fmt.Sprintf("a/changes/%s/comments", url.PathEscape(changeID)) 586 } 587 if _, err := c.get(ctx, path, url.Values{}, &resp); err != nil { 588 return nil, err 589 } 590 return resp, nil 591 } 592 593 // ListRobotComments gets all robot comments on a single change. 594 // 595 // This method returns a list of robot comments for each file path (including 596 // pseudo-files like '/PATCHSET_LEVEL' and '/COMMIT_MSG') and an error. 597 // 598 // The changeID parameter may be in any of the forms supported by Gerrit: 599 // - "4247" 600 // - "I8473b95934b5732ac55d26311a706c9c2bde9940" 601 // - etc. See the link below. 602 // 603 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id 604 func (c *Client) ListRobotComments(ctx context.Context, changeID string, revisionID string) (map[string][]RobotComment, error) { 605 var resp map[string][]RobotComment 606 var path string 607 if revisionID != "" { 608 path = fmt.Sprintf("a/changes/%s/revisions/%s/robotcomments", url.PathEscape(changeID), url.PathEscape(revisionID)) 609 } else { 610 path = fmt.Sprintf("a/changes/%s/robotcomments", url.PathEscape(changeID)) 611 } 612 if _, err := c.get(ctx, path, url.Values{}, &resp); err != nil { 613 return nil, err 614 } 615 return resp, nil 616 } 617 618 // ChangesSubmittedTogether returns a list of Gerrit changes which are submitted 619 // when Submit is called for the given change, including the current change itself. 620 // As a special case, the list is empty if this change would be submitted by itself 621 // (without other changes). 622 // 623 // Returns a slice of Change and an error. 624 // 625 // The changeID parameter may be in any of the forms supported by Gerrit: 626 // - "4247" 627 // - "I8473b95934b5732ac55d26311a706c9c2bde9940" 628 // - etc. See the link below. 629 // 630 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submitted-together 631 // 632 // options is a list of strings like {"CURRENT_REVISION"} which tells Gerrit 633 // to return non-default properties for each Change. The supported strings for 634 // options are listed in Gerrit's api documentation at the link below: 635 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes 636 func (c *Client) ChangesSubmittedTogether(ctx context.Context, changeID string, options ChangeDetailsParams) (*SubmittedTogetherInfo, error) { 637 var resp SubmittedTogetherInfo 638 path := fmt.Sprintf("a/changes/%s/submitted_together", url.PathEscape(changeID)) 639 if _, err := c.get(ctx, path, options.queryString(), &resp); err != nil { 640 return nil, err 641 } 642 return &resp, nil 643 } 644 645 // ChangeInput contains the parameters necessary for creating a change in 646 // Gerrit. 647 // 648 // This struct is intended to be one-to-one with the ChangeInput structure 649 // described in Gerrit's documentation: 650 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-input 651 // 652 // The exception is only status, which is always "NEW" and is therefore unavailable in 653 // this struct. 654 // 655 // TODO(mknyszek): Support merge and notify_details. 656 type ChangeInput struct { 657 // Project is the name of the project for this change. 658 Project string `json:"project"` 659 660 // Branch is the name of the target branch for this change. 661 // refs/heads/ prefix is omitted. 662 Branch string `json:"branch"` 663 664 // Subject is the header line of the commit message. 665 Subject string `json:"subject"` 666 667 // Topic is the topic to which this change belongs. Optional. 668 Topic string `json:"topic,omitempty"` 669 670 // IsPrivate sets whether the change is marked private. 671 IsPrivate bool `json:"is_private,omitempty"` 672 673 // WorkInProgress sets the work-in-progress bit for the change. 674 WorkInProgress bool `json:"work_in_progress,omitempty"` 675 676 // BaseChange is the change this new change is based on. Optional. 677 // 678 // This can be anything Gerrit expects as a ChangeID. We recommend <numericId>, 679 // but the full list of supported formats can be found here: 680 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id 681 BaseChange string `json:"base_change,omitempty"` 682 683 // NewBranch allows for creating a new branch when set to true. 684 NewBranch bool `json:"new_branch,omitempty"` 685 686 // Notify is an enum specifying whom to send notifications to. 687 // 688 // Valid values are NONE, OWNER, OWNER_REVIEWERS, and ALL. 689 // 690 // Optional. The default is ALL. 691 Notify string `json:"notify,omitempty"` 692 } 693 694 // CreateChange creates a new change in Gerrit. 695 // 696 // Returns a Change describing the newly created change or an error. 697 func (c *Client) CreateChange(ctx context.Context, ci *ChangeInput) (*Change, error) { 698 var resp Change 699 if _, err := c.post(ctx, "a/changes/", ci, &resp); err != nil { 700 return nil, err 701 } 702 return &resp, nil 703 } 704 705 // AbandonInput contains information for abandoning a change. 706 // 707 // This struct is intended to be one-to-one with the AbandonInput structure 708 // described in Gerrit's documentation: 709 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#abandon-input 710 // 711 // TODO(mknyszek): Support notify_details. 712 type AbandonInput struct { 713 // Message to be added as review comment to the change when abandoning it. 714 Message string `json:"message,omitempty"` 715 716 // Notify is an enum specifying whom to send notifications to. 717 // 718 // Valid values are NONE, OWNER, OWNER_REVIEWERS, and ALL. 719 // 720 // Optional. The default is ALL. 721 Notify string `json:"notify,omitempty"` 722 } 723 724 // AbandonChange abandons an existing change in Gerrit. 725 // 726 // Returns a Change referenced by changeID, with status ABANDONED, if 727 // abandoned. 728 // 729 // AbandonInput is optional (that is, may be nil). 730 // 731 // The changeID parameter may be in any of the forms supported by Gerrit: 732 // - "4247" 733 // - "I8473b95934b5732ac55d26311a706c9c2bde9940" 734 // - etc. See the link below. 735 // 736 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id 737 func (c *Client) AbandonChange(ctx context.Context, changeID string, ai *AbandonInput) (*Change, error) { 738 var resp Change 739 path := fmt.Sprintf("a/changes/%s/abandon", url.PathEscape(changeID)) 740 if _, err := c.post(ctx, path, ai, &resp); err != nil { 741 return nil, err 742 } 743 return &resp, nil 744 } 745 746 // IsChangePureRevert determines if a change is a pure revert of another commit. 747 // 748 // This method returns a bool and an error. 749 // 750 // To determine which commit the change is purportedly reverting, use the 751 // revert_of property of the change. 752 // 753 // Gerrit's doc for the api: 754 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-pure-revert 755 func (c *Client) IsChangePureRevert(ctx context.Context, changeID string) (bool, error) { 756 resp := &pureRevertResponse{} 757 path := fmt.Sprintf("changes/%s/pure_revert", url.PathEscape(changeID)) 758 sc, err := c.get(ctx, path, url.Values{}, resp) 759 if err != nil { 760 switch sc { 761 case 400: 762 return false, nil 763 default: 764 return false, err 765 } 766 } 767 return resp.IsPureRevert, nil 768 } 769 770 // ReviewerInput contains information for adding a reviewer to a change. 771 // 772 // TODO(mknyszek): Add support for notify_details. 773 type ReviewerInput struct { 774 // Reviewer is an account-id that should be added as a reviewer, or a group-id for which 775 // all members should be added as reviewers. If Reviewer identifies both an account and a 776 // group, only the account is added as a reviewer to the change. 777 // 778 // More information on account-id may be found here: 779 // https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-id 780 // 781 // More information on group-id may be found here: 782 // https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#group-id 783 Reviewer string `json:"reviewer"` 784 785 // State is the state in which to add the reviewer. Possible reviewer states are REVIEWER 786 // and CC. If not given, defaults to REVIEWER. 787 State string `json:"state,omitempty"` 788 789 // Confirmed represents whether adding the reviewer is confirmed. 790 // 791 // The Gerrit server may be configured to require a confirmation when adding a group as 792 // a reviewer that has many members. 793 Confirmed bool `json:"confirmed"` 794 795 // Notify is an enum specifying whom to send notifications to. 796 // 797 // Valid values are NONE, OWNER, OWNER_REVIEWERS, and ALL. 798 // 799 // Optional. The default is ALL. 800 Notify string `json:"notify,omitempty"` 801 } 802 803 // ReviewInput contains information for adding a review to a revision. 804 // 805 // TODO(mknyszek): Add support for drafts, notify, and omit_duplicate_comments. 806 type ReviewInput struct { 807 // Inline comments to be added to the revision. 808 Comments map[string][]CommentInput `json:"comments,omitempty"` 809 810 // Robot comments to be added to the revision. 811 RobotComments map[string][]RobotCommentInput `json:"robot_comments,omitempty"` 812 813 // Message is the message to be added as a review comment. 814 Message string `json:"message,omitempty"` 815 816 // Tag represents a tag to apply to review comment messages, votes, and inline comments. 817 // 818 // Tags may be used by CI or other automated systems to distinguish them from human 819 // reviews. 820 Tag string `json:"tag,omitempty"` 821 822 // Labels represents the votes that should be added to the revision as a map of label names 823 // to voting values. 824 Labels map[string]int `json:"labels,omitempty"` 825 826 // Notify is an enum specifying whom to send notifications to. 827 // 828 // Valid values are NONE, OWNER, OWNER_REVIEWERS, and ALL. 829 // 830 // Optional. The default is ALL. 831 Notify string `json:"notify,omitempty"` 832 833 // OnBehalfOf is an account-id which the review should be posted on behalf of. 834 // 835 // To use this option the caller must have granted labelAs-NAME permission for all keys 836 // of labels. 837 // 838 // More information on account-id may be found here: 839 // https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-id 840 OnBehalfOf string `json:"on_behalf_of,omitempty"` 841 842 // Reviewers is a list of ReviewerInput representing reviewers that should be added to the change. Optional. 843 Reviewers []ReviewerInput `json:"reviewers,omitempty"` 844 845 // Ready, if set to true, will move the change from WIP to ready to review if possible. 846 // 847 // It is an error for both Ready and WorkInProgress to be true. 848 Ready bool `json:"ready,omitempty"` 849 850 // WorkInProgress sets the work-in-progress bit for the change. 851 // 852 // It is an error for both Ready and WorkInProgress to be true. 853 WorkInProgress bool `json:"work_in_progress,omitempty"` 854 } 855 856 // SubmitInput contains information for submitting a change. 857 type SubmitInput struct { 858 // Notify is an enum specifying whom to send notifications to. 859 // 860 // Valid values are NONE, OWNER, OWNER_REVIEWERS, and ALL. 861 // 862 // Optional. The default is ALL. 863 Notify string `json:"notify,omitempty"` 864 865 // OnBehalfOf is an account-id which the review should be posted on behalf of. 866 // 867 // To use this option the caller must have granted labelAs-NAME permission for all keys 868 // of labels. 869 // 870 // More information on account-id may be found here: 871 // https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-id 872 OnBehalfOf string `json:"on_behalf_of,omitempty"` 873 } 874 875 // ReviewerInfo contains information about a reviewer and their votes on a change. 876 // 877 // It has the same fields as AccountInfo, except the AccountID is optional if an unregistered reviewer 878 // was added. 879 type ReviewerInfo struct { 880 AccountInfo 881 882 // Approvals maps label names to the approval values given by this reviewer. 883 Approvals map[string]string `json:"approvals,omitempty"` 884 } 885 886 // AddReviewerResult describes the result of adding a reviewer to a change. 887 type AddReviewerResult struct { 888 // Input is the value of the `reviewer` field from ReviewerInput set while adding the reviewer. 889 Input string `json:"input"` 890 891 // Reviewers is a list of newly added reviewers. 892 Reviewers []ReviewerInfo `json:"reviewers,omitempty"` 893 894 // CCs is a list of newly CCed accounts. This field will only appear if the requested `state` 895 // for the reviewer was CC and NoteDb is enabled on the server. 896 CCs []ReviewerInfo `json:"ccs,omitempty"` 897 898 // Error is a message explaining why the reviewer could not be added. 899 // 900 // If a group was specified in the input and an error is returned, that means none of the 901 // members were added as reviewer. 902 Error string `json:"error,omitempty"` 903 904 // Confirm represents whether adding the reviewer requires confirmation. 905 Confirm bool `json:"confirm,omitempty"` 906 } 907 908 // ReviewResult contains information regarding the updates that were made to a review. 909 type ReviewResult struct { 910 // Labels is a map of labels to values after the review was posted. 911 // 912 // nil if any reviewer additions were rejected. 913 Labels map[string]int `json:"labels,omitempty"` 914 915 // Reviewers is a map of accounts or group identifiers to an AddReviewerResult representing 916 // the outcome of adding the reviewer. 917 // 918 // Absent if no reviewer additions were requested. 919 Reviewers map[string]AddReviewerResult `json:"reviewers,omitempty"` 920 921 // Ready marks if the change was moved from WIP to ready for review as a result of this action. 922 Ready bool `json:"ready,omitempty"` 923 } 924 925 // SetReview sets a review on a revision, optionally also publishing 926 // draft comments, setting labels, adding reviewers or CCs, and modifying 927 // the work in progress property. 928 // 929 // Returns a ReviewResult which describes the applied labels and any added reviewers. 930 // 931 // The changeID parameter may be in any of the forms supported by Gerrit: 932 // - "4247" 933 // - "I8473b95934b5732ac55d26311a706c9c2bde9940" 934 // - etc. See the link below. 935 // 936 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id 937 // 938 // The revisionID parameter may be in any of the forms supported by Gerrit: 939 // - "current" 940 // - a commit ID 941 // - "0" or the literal "edit" for a change edit 942 // - etc. See the link below. 943 // 944 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-id 945 func (c *Client) SetReview(ctx context.Context, changeID string, revisionID string, ri *ReviewInput) (*ReviewResult, error) { 946 var resp ReviewResult 947 path := fmt.Sprintf("a/changes/%s/revisions/%s/review", url.PathEscape(changeID), url.PathEscape(revisionID)) 948 if _, err := c.post(ctx, path, ri, &resp); err != nil { 949 return nil, err 950 } 951 return &resp, nil 952 } 953 954 // MergeableResult contains information if the change for the revision can be merged or not. 955 type MergeableResult struct { 956 957 // Mergeable marks if the change is ready to be submitted cleanly, false otherwise 958 Mergeable bool `json:"mergeable"` 959 } 960 961 // GetMergeable API checks if a change is ready to be submitted cleanly. 962 // 963 // Returns a MergeableResult that has details if the change be merged or not. 964 func (c *Client) GetMergeable(ctx context.Context, changeID string, revisionID string) (*MergeableResult, error) { 965 var resp MergeableResult 966 path := fmt.Sprintf("a/changes/%s/revisions/%s/mergeable", url.PathEscape(changeID), url.PathEscape(revisionID)) 967 if _, err := c.get(ctx, path, url.Values{}, &resp); err != nil { 968 return nil, err 969 } 970 return &resp, nil 971 } 972 973 // Submit submits a change to the repository. It bypasses the Commit Queue. 974 // 975 // Returns a Change. 976 // 977 // The changeID parameter may be in any of the forms supported by Gerrit: 978 // - "4247" 979 // - "I8473b95934b5732ac55d26311a706c9c2bde9940" 980 // - etc. See the link below. 981 // 982 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id 983 func (c *Client) Submit(ctx context.Context, changeID string, si *SubmitInput) (*Change, error) { 984 var resp Change 985 path := fmt.Sprintf("a/changes/%s/submit", url.PathEscape(changeID)) 986 if _, err := c.post(ctx, path, si, &resp); err != nil { 987 return nil, err 988 } 989 return &resp, nil 990 } 991 992 // RebaseInput contains information for rebasing a change. 993 type RebaseInput struct { 994 // Base is the revision to rebase on top of. 995 Base string `json:"base,omitempty"` 996 997 // AllowConflicts allows the rebase to succeed even if there are conflicts. 998 AllowConflicts bool `json:"allow_conflicts,omitempty"` 999 1000 // OnBehalfOfUploader causes the rebase to be done on behalf of the 1001 // uploader. This means the uploader of the current patch set will also be 1002 // the uploader of the rebased patch set. 1003 // 1004 // Rebasing on behalf of the uploader is only supported for trivial rebases. 1005 // This means this option cannot be combined with the `allow_conflicts` 1006 // option. 1007 OnBehalfOfUploader bool `json:"on_behalf_of_uploader,omitempty"` 1008 } 1009 1010 // RebaseChange rebases an open change on top of a specified revision (or its 1011 // parent change, if no revision is specified). 1012 // 1013 // Returns a Change referenced by changeID, if rebased. 1014 // 1015 // RebaseInput is optional (that is, may be nil). 1016 // 1017 // The changeID parameter may be in any of the forms supported by Gerrit: 1018 // - "4247" 1019 // - "I8473b95934b5732ac55d26311a706c9c2bde9940" 1020 // - etc. See the link below. 1021 // 1022 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id 1023 func (c *Client) RebaseChange(ctx context.Context, changeID string, ri *RebaseInput) (*Change, error) { 1024 var resp Change 1025 path := fmt.Sprintf("a/changes/%s/rebase", url.PathEscape(changeID)) 1026 if _, err := c.post(ctx, path, ri, &resp); err != nil { 1027 return nil, err 1028 } 1029 return &resp, nil 1030 } 1031 1032 // RestoreInput contains information for restoring a change. 1033 type RestoreInput struct { 1034 // Message is the message to be added as review comment to the change when restoring the change. 1035 Message string `json:"message,omitempty"` 1036 } 1037 1038 // RestoreChange restores an existing abandoned change in Gerrit. 1039 // 1040 // Returns a Change referenced by changeID, if restored. 1041 // 1042 // RestoreInput is optional (that is, may be nil). 1043 // 1044 // The changeID parameter may be in any of the forms supported by Gerrit: 1045 // - "4247" 1046 // - "I8473b95934b5732ac55d26311a706c9c2bde9940" 1047 // - etc. See the link below. 1048 // 1049 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id 1050 func (c *Client) RestoreChange(ctx context.Context, changeID string, ri *RestoreInput) (*Change, error) { 1051 var resp Change 1052 path := fmt.Sprintf("a/changes/%s/restore", url.PathEscape(changeID)) 1053 if _, err := c.post(ctx, path, ri, &resp); err != nil { 1054 return nil, err 1055 } 1056 return &resp, nil 1057 } 1058 1059 // BranchInput contains information for creating a branch. 1060 type BranchInput struct { 1061 // Ref is the name of the branch. 1062 Ref string `json:"ref,omitempty"` 1063 1064 // Revision is the base revision of the new branch. 1065 Revision string `json:"revision,omitempty"` 1066 } 1067 1068 // CreateBranch creates a branch. 1069 // 1070 // Returns BranchInfo, or an error if the branch could not be created. 1071 // 1072 // See the link below for API documentation. 1073 // https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#create-branch 1074 func (c *Client) CreateBranch(ctx context.Context, projectID string, bi *BranchInput) (*BranchInfo, error) { 1075 var resp BranchInfo 1076 path := fmt.Sprintf("a/projects/%s/branches/%s", url.PathEscape(projectID), url.PathEscape(bi.Ref)) 1077 if _, err := c.post(ctx, path, bi, &resp); err != nil { 1078 return nil, err 1079 } 1080 return &resp, nil 1081 } 1082 1083 // AccountQueryParams contains the parameters necessary for querying accounts from Gerrit. 1084 type AccountQueryParams struct { 1085 // Actual query string, see 1086 // https://gerrit-review.googlesource.com/Documentation/user-search-accounts.html#_search_operators 1087 Query string `json:"q"` 1088 // How many changes to include in the response. 1089 N int `json:"n"` 1090 // Skip this many from the list of results (unreliable for paging). 1091 S int `json:"S"` 1092 // Include these options in the queries. Certain options will make 1093 // Gerrit fill in additional fields of the response. These require 1094 // additional database searches and may delay the response. 1095 // 1096 // The supported strings for options are listed in Gerrit's api 1097 // documentation at the link below: 1098 // https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html 1099 Options []string `json:"o"` 1100 } 1101 1102 // queryString renders the ChangeQueryParams as a url.Values. 1103 func (qr *AccountQueryParams) queryString() url.Values { 1104 qs := make(url.Values, len(qr.Options)+3) 1105 qs.Add("q", qr.Query) 1106 if qr.N > 0 { 1107 qs.Add("n", strconv.Itoa(qr.N)) 1108 } 1109 if qr.S > 0 { 1110 qs.Add("S", strconv.Itoa(qr.S)) 1111 } 1112 for _, o := range qr.Options { 1113 qs.Add("o", o) 1114 } 1115 return qs 1116 } 1117 1118 // AccountQuery gets all matching accounts. 1119 // 1120 // Only the .Query property of the qr parameter is required. 1121 // 1122 // Returns a slice of AccountInfo, whether there are more accounts to fetch, 1123 // and an error. 1124 // 1125 // https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html 1126 func (c *Client) AccountQuery(ctx context.Context, qr AccountQueryParams) ([]*AccountInfo, bool, error) { 1127 var resp struct{ Collection []*AccountInfo } 1128 if _, err := c.get(ctx, "a/accounts/", qr.queryString(), &resp.Collection); err != nil { 1129 return nil, false, err 1130 } 1131 result := resp.Collection 1132 moreAccounts := false 1133 if len(result) > 0 { 1134 moreAccounts = result[len(result)-1].MoreAccounts 1135 result[len(result)-1].MoreAccounts = false 1136 } 1137 return result, moreAccounts, nil 1138 } 1139 1140 func (c *Client) get(ctx context.Context, path string, query url.Values, result any) (int, error) { 1141 u := c.gerritURL 1142 u.Opaque = "//" + u.Host + "/" + path 1143 u.RawQuery = query.Encode() 1144 1145 var statusCode int 1146 err := retry.Retry(ctx, transient.Only(c.retryStrategy), func() error { 1147 var err error 1148 statusCode, err = c.getOnce(ctx, u, result) 1149 return err 1150 }, nil) 1151 return statusCode, err 1152 } 1153 1154 func (c *Client) getOnce(ctx context.Context, u url.URL, result any) (int, error) { 1155 r, err := ctxhttp.Get(ctx, c.httpClient, u.String()) 1156 if err != nil { 1157 return 0, transient.Tag.Apply(err) 1158 } 1159 defer r.Body.Close() 1160 if r.StatusCode < 200 || r.StatusCode >= 300 { 1161 err = errors.Reason("failed to fetch %q, status code %d", u.String(), r.StatusCode).Err() 1162 if r.StatusCode >= 500 { 1163 err = transient.Tag.Apply(err) 1164 } 1165 return r.StatusCode, err 1166 } 1167 return r.StatusCode, parseResponse(r.Body, result) 1168 } 1169 1170 func (c *Client) post(ctx context.Context, path string, data any, result any) (int, error) { 1171 var buffer bytes.Buffer 1172 if err := json.NewEncoder(&buffer).Encode(data); err != nil { 1173 return 200, err 1174 } 1175 u := c.gerritURL 1176 u.Opaque = "//" + u.Host + "/" + path 1177 r, err := ctxhttp.Post(ctx, c.httpClient, u.String(), contentType, &buffer) 1178 if err != nil { 1179 return 0, transient.Tag.Apply(err) 1180 } 1181 defer r.Body.Close() 1182 if r.StatusCode < 200 || r.StatusCode >= 300 { 1183 b, err := io.ReadAll(r.Body) 1184 if err != nil { 1185 return 0, transient.Tag.Apply(err) 1186 } 1187 err = errors.Reason("failed to post to %q, status code %d: %s", u.String(), r.StatusCode, strings.TrimSpace(string(b))).Err() 1188 if r.StatusCode >= 500 { 1189 err = transient.Tag.Apply(err) 1190 } 1191 return r.StatusCode, err 1192 } 1193 return r.StatusCode, parseResponse(r.Body, result) 1194 } 1195 1196 func parseResponse(resp io.Reader, result any) error { 1197 // Strip out the jsonp header, which is ")]}'" 1198 const gerritPrefix = ")]}'" 1199 trash := make([]byte, len(gerritPrefix)) 1200 cnt, err := resp.Read(trash) 1201 if err != nil { 1202 return errors.Annotate(err, "unexpected response from Gerrit").Err() 1203 } 1204 if cnt != len(gerritPrefix) || gerritPrefix != string(trash) { 1205 return errors.New("unexpected response from Gerrit") 1206 } 1207 if err = json.NewDecoder(resp).Decode(result); err != nil { 1208 return errors.Annotate(err, "failed to decode Gerrit response into %T", result).Err() 1209 } 1210 return nil 1211 } 1212 1213 // pureRevertResponse contains the response fields for calls to get-pure-revert 1214 // api. 1215 type pureRevertResponse struct { 1216 IsPureRevert bool `json:"is_pure_revert"` 1217 }