golang.org/x/build@v0.0.0-20240506185731-218518f32b70/gerrit/gerrit.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package gerrit contains code to interact with Gerrit servers. 6 // 7 // This package doesn't intend to provide complete coverage of the Gerrit API, 8 // but only a small subset for the current needs of the Go project infrastructure. 9 // Its API is not subject to the Go 1 compatibility promise and may change at any time. 10 // For general-purpose Gerrit API clients, see https://pkg.go.dev/search?q=gerrit. 11 package gerrit 12 13 import ( 14 "bufio" 15 "bytes" 16 "context" 17 "encoding/base64" 18 "encoding/json" 19 "errors" 20 "fmt" 21 "io" 22 "net/http" 23 "net/url" 24 "sort" 25 "strconv" 26 "strings" 27 "time" 28 ) 29 30 // Client is a Gerrit client. 31 type Client struct { 32 url string // URL prefix, e.g. "https://go-review.googlesource.com" (without trailing slash) 33 auth Auth 34 35 // HTTPClient optionally specifies an HTTP client to use 36 // instead of http.DefaultClient. 37 HTTPClient *http.Client 38 } 39 40 // NewClient returns a new Gerrit client with the given URL prefix 41 // and authentication mode. 42 // The url should be just the scheme and hostname. For example, "https://go-review.googlesource.com". 43 // If auth is nil, a default is used, or requests are made unauthenticated. 44 func NewClient(url string, auth Auth) *Client { 45 if auth == nil { 46 // TODO(bradfitz): use GitCookies auth, once that exists 47 auth = NoAuth 48 } 49 return &Client{ 50 url: strings.TrimSuffix(url, "/"), 51 auth: auth, 52 } 53 } 54 55 func (c *Client) httpClient() *http.Client { 56 if c.HTTPClient != nil { 57 return c.HTTPClient 58 } 59 return http.DefaultClient 60 } 61 62 // ErrResourceNotExist is returned when the requested resource doesn't exist. 63 // It is only for use with errors.Is. 64 var ErrResourceNotExist = errors.New("gerrit: requested resource does not exist") 65 66 // ErrNotModified is returned when a modification didn't result in any change. 67 // It is only for use with errors.Is. Not all APIs return this error; check the documentation. 68 var ErrNotModified = errors.New("gerrit: requested modification resulted in no change") 69 70 // HTTPError is the error type returned when a Gerrit API call does not return 71 // the expected status. 72 type HTTPError struct { 73 Res *http.Response // non-nil 74 Body []byte // 4KB prefix 75 BodyErr error // any error reading Body 76 } 77 78 func (e *HTTPError) Error() string { 79 return fmt.Sprintf("HTTP status %s on request to %s; %s", e.Res.Status, e.Res.Request.URL, e.Body) 80 } 81 82 func (e *HTTPError) Is(target error) bool { 83 switch target { 84 case ErrResourceNotExist: 85 return e.Res.StatusCode == http.StatusNotFound 86 case ErrNotModified: 87 // As of writing, this error text is the only way to distinguish different Conflict errors. See 88 // https://cs.opensource.google/gerrit/gerrit/gerrit/+/master:java/com/google/gerrit/server/restapi/change/ChangeEdits.java;l=346;drc=d338da307a518f7f28b94310c1c083c997ca3c6a 89 // https://cs.opensource.google/gerrit/gerrit/gerrit/+/master:java/com/google/gerrit/server/edit/ChangeEditModifier.java;l=453;drc=3bc970bb3e689d1d340382c3f5e5285d44f91dbf 90 return e.Res.StatusCode == http.StatusConflict && bytes.Contains(e.Body, []byte("no changes were made")) 91 default: 92 return false 93 } 94 } 95 96 // doArg is an optional argument for the Client.do method. 97 type doArg interface { 98 isDoArg() 99 } 100 101 type wantResStatus int 102 103 func (wantResStatus) isDoArg() {} 104 105 // reqBodyJSON sets the request body to a JSON encoding of v, 106 // and the request's Content-Type header to "application/json". 107 type reqBodyJSON struct{ v interface{} } 108 109 func (reqBodyJSON) isDoArg() {} 110 111 // reqBodyRaw sets the request body to r, 112 // and the request's Content-Type header to "application/octet-stream". 113 type reqBodyRaw struct{ r io.Reader } 114 115 func (reqBodyRaw) isDoArg() {} 116 117 type urlValues url.Values 118 119 func (urlValues) isDoArg() {} 120 121 // respBodyRaw returns the body of the response. If set, dst is ignored. 122 type respBodyRaw struct{ rc *io.ReadCloser } 123 124 func (respBodyRaw) isDoArg() {} 125 126 func (c *Client) do(ctx context.Context, dst interface{}, method, path string, opts ...doArg) error { 127 var arg url.Values 128 var requestBody io.Reader 129 var contentType string 130 var wantStatus = http.StatusOK 131 var responseBody *io.ReadCloser 132 for _, opt := range opts { 133 switch opt := opt.(type) { 134 case wantResStatus: 135 wantStatus = int(opt) 136 case reqBodyJSON: 137 b, err := json.MarshalIndent(opt.v, "", " ") 138 if err != nil { 139 return err 140 } 141 requestBody = bytes.NewReader(b) 142 contentType = "application/json" 143 case reqBodyRaw: 144 requestBody = opt.r 145 contentType = "application/octet-stream" 146 case urlValues: 147 arg = url.Values(opt) 148 case respBodyRaw: 149 responseBody = opt.rc 150 default: 151 panic(fmt.Sprintf("internal error; unsupported type %T", opt)) 152 } 153 } 154 155 // slashA is either "/a" (for authenticated requests) or "" for unauthenticated. 156 // See https://gerrit-review.googlesource.com/Documentation/rest-api.html#authentication 157 slashA := "/a" 158 if _, ok := c.auth.(noAuth); ok { 159 slashA = "" 160 } 161 u := c.url + slashA + path 162 if arg != nil { 163 u += "?" + arg.Encode() 164 } 165 req, err := http.NewRequestWithContext(ctx, method, u, requestBody) 166 if err != nil { 167 return err 168 } 169 if contentType != "" { 170 req.Header.Set("Content-Type", contentType) 171 } 172 if err := c.auth.setAuth(c, req); err != nil { 173 return fmt.Errorf("setting Gerrit auth: %v", err) 174 } 175 res, err := c.httpClient().Do(req) 176 if err != nil { 177 return err 178 } 179 defer func() { 180 if responseBody != nil && *responseBody != nil { 181 // We've handed off the body to the user. 182 return 183 } 184 res.Body.Close() 185 }() 186 187 if res.StatusCode != wantStatus { 188 body, err := io.ReadAll(io.LimitReader(res.Body, 4<<10)) 189 return &HTTPError{res, body, err} 190 } 191 192 if responseBody != nil { 193 *responseBody = res.Body 194 return nil 195 } 196 197 if dst == nil { 198 // Drain the response body, return an error if it's anything but empty. 199 body, err := io.ReadAll(io.LimitReader(res.Body, 4<<10)) 200 if err != nil || len(body) != 0 { 201 return &HTTPError{res, body, err} 202 } 203 return nil 204 } 205 // The JSON response begins with an XSRF-defeating header 206 // like ")]}\n". Read that and skip it. 207 br := bufio.NewReader(res.Body) 208 if _, err := br.ReadSlice('\n'); err != nil { 209 return err 210 } 211 return json.NewDecoder(br).Decode(dst) 212 } 213 214 // Possible values for the ChangeInfo Status field. 215 const ( 216 ChangeStatusNew = "NEW" 217 ChangeStatusAbandoned = "ABANDONED" 218 ChangeStatusMerged = "MERGED" 219 ) 220 221 // ChangeInfo is a Gerrit data structure. 222 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info 223 type ChangeInfo struct { 224 // The ID of the change. Subject to a 'GerritBackendFeature__return_new_change_info_id' experiment, 225 // the format is either "'<project>~<_number>'" (new format), 226 // or "'<project>~<branch>~<Change-Id>'" (old format). 227 // 'project', '_number', and 'branch' are URL encoded. 228 // For 'branch' the refs/heads/ prefix is omitted. 229 // The callers must not rely on the format. 230 ID string `json:"id"` 231 232 // ChangeNumber is a change number like "4247". 233 ChangeNumber int `json:"_number"` 234 235 // ChangeID is the Change-Id footer value like "I8473b95934b5732ac55d26311a706c9c2bde9940". 236 // Note that some of the functions in this package take a changeID parameter that is a {change-id}, 237 // which is a distinct concept from a Change-Id footer. (See the documentation links for details, 238 // including https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id). 239 ChangeID string `json:"change_id"` 240 241 Project string `json:"project"` 242 243 // Branch is the name of the target branch. 244 // The refs/heads/ prefix is omitted. 245 Branch string `json:"branch"` 246 247 Topic string `json:"topic"` 248 Assignee *AccountInfo `json:"assignee"` 249 Hashtags []string `json:"hashtags"` 250 251 // Subject is the subject of the change 252 // (the header line of the commit message). 253 Subject string `json:"subject"` 254 255 // Status is the status of the change (NEW, SUBMITTED, MERGED, 256 // ABANDONED, DRAFT). 257 Status string `json:"status"` 258 259 Created TimeStamp `json:"created"` 260 Updated TimeStamp `json:"updated"` 261 Submitted TimeStamp `json:"submitted"` 262 Submitter *AccountInfo `json:"submitter"` 263 SubmitType string `json:"submit_type"` 264 265 // Mergeable indicates whether the change can be merged. 266 // It is not set for already-merged changes, 267 // nor if the change is untested, nor if the 268 // SKIP_MERGEABLE option has been set. 269 Mergeable bool `json:"mergeable"` 270 271 // Submittable indicates whether the change can be submitted. 272 // It is only set if requested, using the "SUBMITTABLE" option. 273 Submittable bool `json:"submittable"` 274 275 // Insertions and Deletions count inserted and deleted lines. 276 Insertions int `json:"insertions"` 277 Deletions int `json:"deletions"` 278 279 // CurrentRevision is the commit ID of the current patch set 280 // of this change. This is only set if the current revision 281 // is requested or if all revisions are requested (fields 282 // "CURRENT_REVISION" or "ALL_REVISIONS"). 283 CurrentRevision string `json:"current_revision"` 284 285 // Revisions maps a commit ID of the patch set to a 286 // RevisionInfo entity. 287 // 288 // Only set if the current revision is requested (in which 289 // case it will only contain a key for the current revision) 290 // or if all revisions are requested. 291 Revisions map[string]RevisionInfo `json:"revisions"` 292 293 // Owner is the author of the change. 294 // The details are only filled in if field "DETAILED_ACCOUNTS" is requested. 295 Owner *AccountInfo `json:"owner"` 296 297 // Messages are included if field "MESSAGES" is requested. 298 Messages []ChangeMessageInfo `json:"messages"` 299 300 // Labels maps label names to LabelInfo entries. 301 Labels map[string]LabelInfo `json:"labels"` 302 303 // ReviewerUpdates are included if field "REVIEWER_UPDATES" is requested. 304 ReviewerUpdates []ReviewerUpdateInfo `json:"reviewer_updates"` 305 306 // Reviewers maps reviewer state ("REVIEWER", "CC", "REMOVED") 307 // to a list of accounts. 308 // REVIEWER lists users with at least one non-zero vote on the change. 309 // CC lists users added to the change who has not voted. 310 // REMOVED lists users who were previously reviewers on the change 311 // but who have been removed. 312 // Reviewers is only included if "DETAILED_LABELS" is requested. 313 Reviewers map[string][]*AccountInfo `json:"reviewers"` 314 315 // WorkInProgress indicates that the change is marked as a work in progress. 316 // (This means it is not yet ready for review, but it is still publicly visible.) 317 WorkInProgress bool `json:"work_in_progress"` 318 319 // HasReviewStarted indicates whether the change has ever been marked 320 // ready for review in the past (not as a work in progress). 321 HasReviewStarted bool `json:"has_review_started"` 322 323 // RevertOf lists the numeric Change-Id of the change that this change reverts. 324 RevertOf int `json:"revert_of"` 325 326 // MoreChanges is set on the last change from QueryChanges if 327 // the result set is truncated by an 'n' parameter. 328 MoreChanges bool `json:"_more_changes"` 329 } 330 331 // ReviewerUpdateInfo is a Gerrit data structure. 332 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#review-update-info 333 type ReviewerUpdateInfo struct { 334 Updated TimeStamp `json:"updated"` 335 UpdatedBy *AccountInfo `json:"updated_by"` 336 Reviewer *AccountInfo `json:"reviewer"` 337 State string // "REVIEWER", "CC", or "REMOVED" 338 } 339 340 // AccountInfo is a Gerrit data structure. It's used both for getting the details 341 // for a single account, as well as for querying multiple accounts. 342 // See https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-info. 343 type AccountInfo struct { 344 NumericID int64 `json:"_account_id"` 345 Name string `json:"name,omitempty"` 346 Email string `json:"email,omitempty"` 347 Username string `json:"username,omitempty"` 348 Tags []string `json:"tags,omitempty"` 349 350 // MoreAccounts is set on the last account from QueryAccounts if 351 // the result set is truncated by an 'n' parameter (or has more). 352 MoreAccounts bool `json:"_more_accounts"` 353 } 354 355 func (ai *AccountInfo) Equal(v *AccountInfo) bool { 356 if ai == nil || v == nil { 357 return false 358 } 359 return ai.NumericID == v.NumericID 360 } 361 362 type ChangeMessageInfo struct { 363 ID string `json:"id"` 364 Author *AccountInfo `json:"author"` 365 Time TimeStamp `json:"date"` 366 Message string `json:"message"` 367 Tag string `json:"tag,omitempty"` 368 RevisionNumber int `json:"_revision_number"` 369 } 370 371 // The LabelInfo entity contains information about a label on a 372 // change, always corresponding to the current patch set. 373 // 374 // There are two options that control the contents of LabelInfo: 375 // LABELS and DETAILED_LABELS. 376 // 377 // For a quick summary of the state of labels, use LABELS. 378 // 379 // For detailed information about labels, including exact numeric 380 // votes for all users and the allowed range of votes for the current 381 // user, use DETAILED_LABELS. 382 type LabelInfo struct { 383 // Optional means the label may be set, but it’s neither 384 // necessary for submission nor does it block submission if 385 // set. 386 Optional bool `json:"optional"` 387 388 // Fields set by LABELS field option: 389 Approved *AccountInfo `json:"approved"` 390 391 // Fields set by DETAILED_LABELS option: 392 All []ApprovalInfo `json:"all"` 393 } 394 395 type ApprovalInfo struct { 396 AccountInfo 397 Value int `json:"value"` 398 Date TimeStamp `json:"date"` 399 } 400 401 // The RevisionInfo entity contains information about a patch set. Not 402 // all fields are returned by default. Additional fields can be 403 // obtained by adding o parameters as described at: 404 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes 405 type RevisionInfo struct { 406 Draft bool `json:"draft"` 407 PatchSetNumber int `json:"_number"` 408 Created TimeStamp `json:"created"` 409 Uploader *AccountInfo `json:"uploader"` 410 Ref string `json:"ref"` 411 Fetch map[string]*FetchInfo `json:"fetch"` 412 Commit *CommitInfo `json:"commit"` 413 Files map[string]*FileInfo `json:"files"` 414 CommitWithFooters string `json:"commit_with_footers"` 415 Kind string `json:"kind"` 416 // TODO: more 417 } 418 419 type CommitInfo struct { 420 Author GitPersonInfo `json:"author"` 421 Committer GitPersonInfo `json:"committer"` 422 CommitID string `json:"commit"` 423 Subject string `json:"subject"` 424 Message string `json:"message"` 425 Parents []CommitInfo `json:"parents"` 426 } 427 428 type GitPersonInfo struct { 429 Name string `json:"name"` 430 Email string `json:"Email"` 431 Date TimeStamp `json:"date"` 432 TZOffset int `json:"tz"` 433 } 434 435 func (gpi *GitPersonInfo) Equal(v *GitPersonInfo) bool { 436 if gpi == nil { 437 if gpi != v { 438 return false 439 } 440 return true 441 } 442 return gpi.Name == v.Name && gpi.Email == v.Email && gpi.Date.Equal(v.Date) && 443 gpi.TZOffset == v.TZOffset 444 } 445 446 // Possible values for the FileInfo Status field. 447 const ( 448 FileInfoAdded = "A" 449 FileInfoDeleted = "D" 450 FileInfoRenamed = "R" 451 FileInfoCopied = "C" 452 FileInfoRewritten = "W" 453 ) 454 455 type FileInfo struct { 456 Status string `json:"status"` 457 Binary bool `json:"binary"` 458 OldPath string `json:"old_path"` 459 LinesInserted int `json:"lines_inserted"` 460 LinesDeleted int `json:"lines_deleted"` 461 } 462 463 type FetchInfo struct { 464 URL string `json:"url"` 465 Ref string `json:"ref"` 466 Commands map[string]string `json:"commands"` 467 } 468 469 // QueryChangesOpt are options for QueryChanges. 470 type QueryChangesOpt struct { 471 // N is the number of results to return. 472 // If 0, the 'n' parameter is not sent to Gerrit. 473 N int 474 475 // Start is the number of results to skip (useful in pagination). 476 // To figure out if there are more results, the last ChangeInfo struct 477 // in the last call to QueryChanges will have the field MoreAccounts=true. 478 // If 0, the 'S' parameter is not sent to Gerrit. 479 Start int 480 481 // Fields are optional fields to also return. 482 // Example strings include "ALL_REVISIONS", "LABELS", "MESSAGES". 483 // For a complete list, see: 484 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info 485 Fields []string 486 } 487 488 func condInt(n int) []string { 489 if n != 0 { 490 return []string{strconv.Itoa(n)} 491 } 492 return nil 493 } 494 495 // QueryChanges queries changes. The q parameter is a Gerrit search query. 496 // For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes 497 // For the query syntax, see https://gerrit-review.googlesource.com/Documentation/user-search.html#_search_operators 498 func (c *Client) QueryChanges(ctx context.Context, q string, opts ...QueryChangesOpt) ([]*ChangeInfo, error) { 499 var opt QueryChangesOpt 500 switch len(opts) { 501 case 0: 502 case 1: 503 opt = opts[0] 504 default: 505 return nil, errors.New("only 1 option struct supported") 506 } 507 var changes []*ChangeInfo 508 err := c.do(ctx, &changes, "GET", "/changes/", urlValues{ 509 "q": {q}, 510 "n": condInt(opt.N), 511 "o": opt.Fields, 512 "S": condInt(opt.Start), 513 }) 514 return changes, err 515 } 516 517 // GetChange returns information about a single change. 518 // For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-change 519 func (c *Client) GetChange(ctx context.Context, changeID string, opts ...QueryChangesOpt) (*ChangeInfo, error) { 520 var opt QueryChangesOpt 521 switch len(opts) { 522 case 0: 523 case 1: 524 opt = opts[0] 525 default: 526 return nil, errors.New("only 1 option struct supported") 527 } 528 var change ChangeInfo 529 err := c.do(ctx, &change, "GET", "/changes/"+changeID, urlValues{ 530 "n": condInt(opt.N), 531 "o": opt.Fields, 532 }) 533 return &change, err 534 } 535 536 // GetChangeDetail retrieves a change with labels, detailed labels, detailed 537 // accounts, and messages. 538 // For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-change-detail 539 func (c *Client) GetChangeDetail(ctx context.Context, changeID string, opts ...QueryChangesOpt) (*ChangeInfo, error) { 540 var opt QueryChangesOpt 541 switch len(opts) { 542 case 0: 543 case 1: 544 opt = opts[0] 545 default: 546 return nil, errors.New("only 1 option struct supported") 547 } 548 var change ChangeInfo 549 err := c.do(ctx, &change, "GET", "/changes/"+changeID+"/detail", urlValues{ 550 "o": opt.Fields, 551 }) 552 if err != nil { 553 return nil, err 554 } 555 return &change, nil 556 } 557 558 // ListChangeComments retrieves a map of published comments for the given change ID. 559 // The map key is the file path (such as "maintner/git_test.go" or "/PATCHSET_LEVEL"). 560 // For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-change-comments. 561 func (c *Client) ListChangeComments(ctx context.Context, changeID string) (map[string][]CommentInfo, error) { 562 var m map[string][]CommentInfo 563 if err := c.do(ctx, &m, "GET", "/changes/"+changeID+"/comments"); err != nil { 564 return nil, err 565 } 566 return m, nil 567 } 568 569 // CommentInfo contains information about an inline comment. 570 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-info. 571 type CommentInfo struct { 572 PatchSet int `json:"patch_set,omitempty"` 573 ID string `json:"id"` 574 Path string `json:"path,omitempty"` 575 Message string `json:"message,omitempty"` 576 Updated TimeStamp `json:"updated"` 577 Author *AccountInfo `json:"author,omitempty"` 578 InReplyTo string `json:"in_reply_to,omitempty"` 579 Unresolved *bool `json:"unresolved,omitempty"` 580 Tag string `json:"tag,omitempty"` 581 } 582 583 // ListFiles retrieves a map of filenames to FileInfo's for the given change ID and revision. 584 // For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-files 585 func (c *Client) ListFiles(ctx context.Context, changeID, revision string) (map[string]*FileInfo, error) { 586 var m map[string]*FileInfo 587 if err := c.do(ctx, &m, "GET", "/changes/"+changeID+"/revisions/"+revision+"/files"); err != nil { 588 return nil, err 589 } 590 return m, nil 591 } 592 593 // ReviewInput contains information for adding a review to a revision. 594 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#review-input 595 type ReviewInput struct { 596 Message string `json:"message,omitempty"` 597 Labels map[string]int `json:"labels,omitempty"` 598 Tag string `json:"tag,omitempty"` 599 600 // Comments contains optional per-line comments to post. 601 // The map key is a file path (such as "src/foo/bar.go"). 602 Comments map[string][]CommentInput `json:"comments,omitempty"` 603 604 // Reviewers optionally specifies new reviewers to add to the change. 605 Reviewers []ReviewerInput `json:"reviewers,omitempty"` 606 } 607 608 // ReviewerInput contains information for adding a reviewer to a change. 609 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#reviewer-input 610 type ReviewerInput struct { 611 // Reviewer is the ID of the account to be added as reviewer. 612 // See https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-id 613 Reviewer string `json:"reviewer"` 614 State string `json:"state,omitempty"` // REVIEWER or CC (default: REVIEWER) 615 } 616 617 // CommentInput contains information for creating an inline comment. 618 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-input 619 type CommentInput struct { 620 Line int `json:"line,omitempty"` 621 Message string `json:"message"` 622 InReplyTo string `json:"in_reply_to,omitempty"` 623 Unresolved *bool `json:"unresolved,omitempty"` 624 625 // TODO(haya14busa): more, as needed. 626 } 627 628 type reviewInfo struct { 629 Labels map[string]int `json:"labels,omitempty"` 630 } 631 632 // SetReview leaves a message on a change and/or modifies labels. 633 // For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#set-review 634 // The changeID is https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id 635 // The revision is https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-id 636 func (c *Client) SetReview(ctx context.Context, changeID, revision string, review ReviewInput) error { 637 var res reviewInfo 638 return c.do(ctx, &res, "POST", fmt.Sprintf("/changes/%s/revisions/%s/review", changeID, revision), 639 reqBodyJSON{&review}) 640 } 641 642 // ReviewerInfo contains information about reviewers of a change. 643 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#reviewer-info 644 type ReviewerInfo struct { 645 AccountInfo 646 Approvals map[string]string `json:"approvals"` 647 } 648 649 // ListReviewers returns all reviewers on a change. 650 // For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-reviewers 651 // The changeID is https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id 652 func (c *Client) ListReviewers(ctx context.Context, changeID string) ([]ReviewerInfo, error) { 653 var res []ReviewerInfo 654 if err := c.do(ctx, &res, "GET", fmt.Sprintf("/changes/%s/reviewers", changeID)); err != nil { 655 return nil, err 656 } 657 return res, nil 658 } 659 660 // HashtagsInput is the request body used when modifying a CL's hashtags. 661 // 662 // See https://gerrit-documentation.storage.googleapis.com/Documentation/2.15.1/rest-api-changes.html#hashtags-input 663 type HashtagsInput struct { 664 Add []string `json:"add"` 665 Remove []string `json:"remove"` 666 } 667 668 // SetHashtags modifies the hashtags for a CL, supporting both adding 669 // and removing hashtags in one request. On success it returns the new 670 // set of hashtags. 671 // 672 // See https://gerrit-documentation.storage.googleapis.com/Documentation/2.15.1/rest-api-changes.html#set-hashtags 673 func (c *Client) SetHashtags(ctx context.Context, changeID string, hashtags HashtagsInput) ([]string, error) { 674 var res []string 675 err := c.do(ctx, &res, "POST", fmt.Sprintf("/changes/%s/hashtags", changeID), reqBodyJSON{&hashtags}) 676 return res, err 677 } 678 679 // AddHashtags is a wrapper around SetHashtags that only supports adding tags. 680 func (c *Client) AddHashtags(ctx context.Context, changeID string, tags ...string) ([]string, error) { 681 return c.SetHashtags(ctx, changeID, HashtagsInput{Add: tags}) 682 } 683 684 // RemoveHashtags is a wrapper around SetHashtags that only supports removing tags. 685 func (c *Client) RemoveHashtags(ctx context.Context, changeID string, tags ...string) ([]string, error) { 686 return c.SetHashtags(ctx, changeID, HashtagsInput{Remove: tags}) 687 } 688 689 // GetHashtags returns a CL's current hashtags. 690 // 691 // See https://gerrit-documentation.storage.googleapis.com/Documentation/2.15.1/rest-api-changes.html#get-hashtags 692 func (c *Client) GetHashtags(ctx context.Context, changeID string) ([]string, error) { 693 var res []string 694 err := c.do(ctx, &res, "GET", fmt.Sprintf("/changes/%s/hashtags", changeID)) 695 return res, err 696 } 697 698 // AbandonChange abandons the given change. 699 // For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#abandon-change 700 // The changeID is https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id 701 // The input for the call is https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#abandon-input 702 func (c *Client) AbandonChange(ctx context.Context, changeID string, message ...string) error { 703 var msg string 704 if len(message) > 1 { 705 panic("invalid use of multiple message inputs") 706 } 707 if len(message) == 1 { 708 msg = message[0] 709 } 710 b := struct { 711 Message string `json:"message,omitempty"` 712 }{msg} 713 var change ChangeInfo 714 return c.do(ctx, &change, "POST", "/changes/"+changeID+"/abandon", reqBodyJSON{&b}) 715 } 716 717 // ProjectInput contains the options for creating a new project. 718 // See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-input 719 type ProjectInput struct { 720 Parent string `json:"parent,omitempty"` 721 Description string `json:"description,omitempty"` 722 SubmitType string `json:"submit_type,omitempty"` 723 724 CreateNewChangeForAllNotInTarget string `json:"create_new_change_for_all_not_in_target,omitempty"` 725 726 // TODO(bradfitz): more, as needed. 727 } 728 729 // ProjectInfo is information about a Gerrit project. 730 // See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-info 731 type ProjectInfo struct { 732 ID string `json:"id"` 733 Name string `json:"name"` 734 Parent string `json:"parent"` 735 CloneURL string `json:"clone_url"` 736 Description string `json:"description"` 737 State string `json:"state"` 738 Branches map[string]string `json:"branches"` 739 WebLinks []WebLinkInfo `json:"web_links,omitempty"` 740 } 741 742 // ListProjects returns the server's active projects. 743 // 744 // The returned slice is sorted by project ID and excludes the "All-Projects" and "All-Users" projects. 745 // 746 // See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#list-projects. 747 func (c *Client) ListProjects(ctx context.Context) ([]ProjectInfo, error) { 748 var res map[string]ProjectInfo 749 err := c.do(ctx, &res, "GET", "/projects/") 750 if err != nil { 751 return nil, err 752 } 753 var ret []ProjectInfo 754 for name, pi := range res { 755 if name == "All-Projects" || name == "All-Users" { 756 continue 757 } 758 if pi.State != "ACTIVE" { 759 continue 760 } 761 // https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-info: 762 // "name not set if returned in a map where the project name is used as map key" 763 pi.Name = name 764 ret = append(ret, pi) 765 } 766 sort.Slice(ret, func(i, j int) bool { return ret[i].ID < ret[j].ID }) 767 return ret, nil 768 } 769 770 // CreateProject creates a new project. 771 func (c *Client) CreateProject(ctx context.Context, name string, p ...ProjectInput) (ProjectInfo, error) { 772 var pi ProjectInput 773 if len(p) > 1 { 774 panic("invalid use of multiple project inputs") 775 } 776 if len(p) == 1 { 777 pi = p[0] 778 } 779 var res ProjectInfo 780 err := c.do(ctx, &res, "PUT", fmt.Sprintf("/projects/%s", url.PathEscape(name)), reqBodyJSON{&pi}, wantResStatus(http.StatusCreated)) 781 return res, err 782 } 783 784 // CreateChange creates a new change. 785 // 786 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#create-change. 787 func (c *Client) CreateChange(ctx context.Context, ci ChangeInput) (ChangeInfo, error) { 788 var res ChangeInfo 789 err := c.do(ctx, &res, "POST", "/changes/", reqBodyJSON{&ci}, wantResStatus(http.StatusCreated)) 790 return res, err 791 } 792 793 // ChangeInput contains the options for creating a new change. 794 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-input. 795 type ChangeInput struct { 796 Project string `json:"project"` 797 Branch string `json:"branch"` 798 Subject string `json:"subject"` 799 } 800 801 // ChangeFileContentInChangeEdit puts content of a file to a change edit. 802 // If no change is made, an error that matches ErrNotModified is returned. 803 // 804 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#put-edit-file. 805 func (c *Client) ChangeFileContentInChangeEdit(ctx context.Context, changeID string, path string, content string) error { 806 return c.do(ctx, nil, "PUT", "/changes/"+changeID+"/edit/"+url.PathEscape(path), 807 reqBodyRaw{strings.NewReader(content)}, wantResStatus(http.StatusNoContent)) 808 } 809 810 // DeleteFileInChangeEdit deletes a file from a change edit. 811 // If no change is made, an error that matches ErrNotModified is returned. 812 // 813 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#delete-edit-file. 814 func (c *Client) DeleteFileInChangeEdit(ctx context.Context, changeID string, path string) error { 815 return c.do(ctx, nil, "DELETE", "/changes/"+changeID+"/edit/"+url.PathEscape(path), wantResStatus(http.StatusNoContent)) 816 } 817 818 // PublishChangeEdit promotes the change edit to a regular patch set. 819 // 820 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#publish-edit. 821 func (c *Client) PublishChangeEdit(ctx context.Context, changeID string) error { 822 return c.do(ctx, nil, "POST", "/changes/"+changeID+"/edit:publish", wantResStatus(http.StatusNoContent)) 823 } 824 825 // GetProjectInfo returns info about a project. 826 func (c *Client) GetProjectInfo(ctx context.Context, name string) (ProjectInfo, error) { 827 var res ProjectInfo 828 err := c.do(ctx, &res, "GET", fmt.Sprintf("/projects/%s", url.PathEscape(name))) 829 return res, err 830 } 831 832 // BranchInfo is information about a branch. 833 // See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#branch-info 834 type BranchInfo struct { 835 Ref string `json:"ref"` 836 Revision string `json:"revision"` 837 CanDelete bool `json:"can_delete"` 838 } 839 840 // GetProjectBranches returns the branches for the project name. The branches are stored in a map 841 // keyed by reference. 842 func (c *Client) GetProjectBranches(ctx context.Context, name string) (map[string]BranchInfo, error) { 843 var res []BranchInfo 844 err := c.do(ctx, &res, "GET", fmt.Sprintf("/projects/%s/branches/", url.PathEscape(name))) 845 if err != nil { 846 return nil, err 847 } 848 m := map[string]BranchInfo{} 849 for _, bi := range res { 850 m[bi.Ref] = bi 851 } 852 return m, nil 853 } 854 855 // GetBranch gets a particular branch in project. 856 // 857 // See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-branch. 858 func (c *Client) GetBranch(ctx context.Context, project, branch string) (BranchInfo, error) { 859 var res BranchInfo 860 err := c.do(ctx, &res, "GET", fmt.Sprintf("/projects/%s/branches/%s", url.PathEscape(project), branch)) 861 return res, err 862 } 863 864 // GetFileContent gets a file's contents at a particular commit. 865 // 866 // See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-content-from-commit. 867 func (c *Client) GetFileContent(ctx context.Context, project, commit, path string) (io.ReadCloser, error) { 868 var body io.ReadCloser 869 err := c.do(ctx, nil, "GET", fmt.Sprintf("/projects/%s/commits/%s/files/%s/content", url.PathEscape(project), commit, url.PathEscape(path)), respBodyRaw{&body}) 870 if err != nil { 871 return nil, err 872 } 873 return readCloser{ 874 Reader: base64.NewDecoder(base64.StdEncoding, body), 875 Closer: body, 876 }, nil 877 } 878 879 type readCloser struct { 880 io.Reader 881 io.Closer 882 } 883 884 // WebLinkInfo is information about a web link. 885 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#web-link-info 886 type WebLinkInfo struct { 887 Name string `json:"name"` 888 URL string `json:"url"` 889 ImageURL string `json:"image_url"` 890 } 891 892 func (wli *WebLinkInfo) Equal(v *WebLinkInfo) bool { 893 if wli == nil || v == nil { 894 return false 895 } 896 return wli.Name == v.Name && wli.URL == v.URL && wli.ImageURL == v.ImageURL 897 } 898 899 // TagInfo is information about a tag. 900 // See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#tag-info 901 type TagInfo struct { 902 Ref string `json:"ref"` 903 Revision string `json:"revision"` 904 Object string `json:"object,omitempty"` 905 Message string `json:"message,omitempty"` 906 Tagger *GitPersonInfo `json:"tagger,omitempty"` 907 Created TimeStamp `json:"created,omitempty"` 908 CanDelete bool `json:"can_delete"` 909 WebLinks []WebLinkInfo `json:"web_links,omitempty"` 910 } 911 912 func (ti *TagInfo) Equal(v *TagInfo) bool { 913 if ti == nil || v == nil { 914 return false 915 } 916 if ti.Ref != v.Ref || ti.Revision != v.Revision || ti.Object != v.Object || 917 ti.Message != v.Message || !ti.Created.Equal(v.Created) || ti.CanDelete != v.CanDelete { 918 return false 919 } 920 if !ti.Tagger.Equal(v.Tagger) { 921 return false 922 } 923 if len(ti.WebLinks) != len(v.WebLinks) { 924 return false 925 } 926 for i := range ti.WebLinks { 927 if !ti.WebLinks[i].Equal(&v.WebLinks[i]) { 928 return false 929 } 930 } 931 return true 932 } 933 934 // GetProjectTags returns the tags for the project name. The tags are stored in a map keyed by 935 // reference. 936 func (c *Client) GetProjectTags(ctx context.Context, name string) (map[string]TagInfo, error) { 937 var res []TagInfo 938 err := c.do(ctx, &res, "GET", fmt.Sprintf("/projects/%s/tags/", url.PathEscape(name))) 939 if err != nil { 940 return nil, err 941 } 942 m := map[string]TagInfo{} 943 for _, ti := range res { 944 m[ti.Ref] = ti 945 } 946 return m, nil 947 } 948 949 // GetTag returns a particular tag on project. 950 // 951 // See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-tag. 952 func (c *Client) GetTag(ctx context.Context, project, tag string) (TagInfo, error) { 953 var res TagInfo 954 err := c.do(ctx, &res, "GET", fmt.Sprintf("/projects/%s/tags/%s", url.PathEscape(project), url.PathEscape(tag))) 955 return res, err 956 } 957 958 // TagInput contains information for creating a tag. 959 // See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#tag-input 960 type TagInput struct { 961 // Ref is optional, and when present must be equal to the URL parameter. Removed. 962 Revision string `json:"revision,omitempty"` 963 Message string `json:"message,omitempty"` 964 } 965 966 // CreateTag creates a tag on project. 967 // See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#create-tag. 968 func (c *Client) CreateTag(ctx context.Context, project, tag string, input TagInput) (TagInfo, error) { 969 var res TagInfo 970 err := c.do(ctx, &res, "PUT", fmt.Sprintf("/projects/%s/tags/%s", project, url.PathEscape(tag)), reqBodyJSON{&input}, wantResStatus(http.StatusCreated)) 971 return res, err 972 } 973 974 // GetAccountInfo gets the specified account's information from Gerrit. 975 // For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#get-account 976 // The accountID is https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-id 977 // 978 // Note that getting "self" is a good way to validate host access, since it only requires peeker 979 // access to the host, not to any particular repository. 980 func (c *Client) GetAccountInfo(ctx context.Context, accountID string) (AccountInfo, error) { 981 var res AccountInfo 982 err := c.do(ctx, &res, "GET", fmt.Sprintf("/accounts/%s", accountID)) 983 return res, err 984 } 985 986 // QueryAccountsOpt are options for QueryAccounts. 987 type QueryAccountsOpt struct { 988 // N is the number of results to return. 989 // If 0, the 'n' parameter is not sent to Gerrit. 990 N int 991 992 // Start is the number of results to skip (useful in pagination). 993 // To figure out if there are more results, the last AccountInfo struct 994 // in the last call to QueryAccounts will have the field MoreAccounts=true. 995 // If 0, the 'S' parameter is not sent to Gerrit. 996 Start int 997 998 // Fields are optional fields to also return. 999 // Example strings include "DETAILS", "ALL_EMAILS". 1000 // By default, only the account IDs are returned. 1001 // For a complete list, see: 1002 // https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#query-account 1003 Fields []string 1004 } 1005 1006 // QueryAccounts queries accounts. The q parameter is a Gerrit search query. 1007 // For the API call and query syntax, see https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#query-account 1008 func (c *Client) QueryAccounts(ctx context.Context, q string, opts ...QueryAccountsOpt) ([]*AccountInfo, error) { 1009 var opt QueryAccountsOpt 1010 switch len(opts) { 1011 case 0: 1012 case 1: 1013 opt = opts[0] 1014 default: 1015 return nil, errors.New("only 1 option struct supported") 1016 } 1017 var changes []*AccountInfo 1018 err := c.do(ctx, &changes, "GET", "/accounts/", urlValues{ 1019 "q": {q}, 1020 "n": condInt(opt.N), 1021 "o": opt.Fields, 1022 "S": condInt(opt.Start), 1023 }) 1024 return changes, err 1025 } 1026 1027 type TimeStamp time.Time 1028 1029 func (ts TimeStamp) Equal(v TimeStamp) bool { 1030 return ts.Time().Equal(v.Time()) 1031 } 1032 1033 // Gerrit's timestamp layout is like time.RFC3339Nano, but with a space instead of the "T", 1034 // and without a timezone (it's always in UTC). 1035 const timeStampLayout = "2006-01-02 15:04:05.999999999" 1036 1037 func (ts TimeStamp) MarshalJSON() ([]byte, error) { 1038 return []byte(fmt.Sprintf(`"%s"`, ts.Time().UTC().Format(timeStampLayout))), nil 1039 } 1040 1041 func (ts *TimeStamp) UnmarshalJSON(p []byte) error { 1042 if len(p) < 2 { 1043 return errors.New("timestamp too short") 1044 } 1045 if p[0] != '"' || p[len(p)-1] != '"' { 1046 return errors.New("not double-quoted") 1047 } 1048 s := strings.Trim(string(p), "\"") 1049 t, err := time.Parse(timeStampLayout, s) 1050 if err != nil { 1051 return err 1052 } 1053 *ts = TimeStamp(t) 1054 return nil 1055 } 1056 1057 func (ts TimeStamp) Time() time.Time { return time.Time(ts) } 1058 1059 // GroupInfo contains information about a group. 1060 // 1061 // See https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#group-info. 1062 type GroupInfo struct { 1063 ID string `json:"id"` 1064 URL string `json:"url"` 1065 Name string `json:"name"` 1066 GroupID int64 `json:"group_id"` 1067 Options GroupOptionsInfo `json:"options"` 1068 Owner string `json:"owner"` 1069 OwnerID string `json:"owner_id"` 1070 } 1071 1072 type GroupOptionsInfo struct { 1073 VisibleToAll bool `json:"visible_to_all"` 1074 } 1075 1076 func (c *Client) GetGroups(ctx context.Context) (map[string]*GroupInfo, error) { 1077 res := make(map[string]*GroupInfo) 1078 err := c.do(ctx, &res, "GET", "/groups/") 1079 for k, gi := range res { 1080 if gi != nil && gi.Name == "" { 1081 gi.Name = k 1082 } 1083 } 1084 return res, err 1085 } 1086 1087 func (c *Client) GetGroupMembers(ctx context.Context, groupID string) ([]AccountInfo, error) { 1088 var ais []AccountInfo 1089 err := c.do(ctx, &ais, "GET", "/groups/"+groupID+"/members") 1090 return ais, err 1091 } 1092 1093 // SubmitChange submits the given change. 1094 // For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submit-change 1095 // The changeID is https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id 1096 func (c *Client) SubmitChange(ctx context.Context, changeID string) (ChangeInfo, error) { 1097 var change ChangeInfo 1098 err := c.do(ctx, &change, "POST", "/changes/"+changeID+"/submit") 1099 return change, err 1100 } 1101 1102 // MergeableInfo contains information about the mergeability of a change. 1103 // 1104 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#mergeable-info. 1105 type MergeableInfo struct { 1106 SubmitType string `json:"submit_type"` 1107 Strategy string `json:"strategy"` 1108 Mergeable bool `json:"mergeable"` 1109 CommitMerged bool `json:"commit_merged"` 1110 } 1111 1112 // GetMergeable retrieves mergeability information for a change at a specific revision. 1113 func (c *Client) GetMergeable(ctx context.Context, changeID, revision string) (MergeableInfo, error) { 1114 var mergeable MergeableInfo 1115 err := c.do(ctx, &mergeable, "GET", "/changes/"+changeID+"/revisions/"+revision+"/mergeable") 1116 return mergeable, err 1117 } 1118 1119 // ActionInfo contains information about actions a client can make to 1120 // manipulate a resource. 1121 // 1122 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#action-info. 1123 type ActionInfo struct { 1124 Method string `json:"method"` 1125 Label string `json:"label"` 1126 Title string `json:"title"` 1127 Enabled bool `json:"enabled"` 1128 } 1129 1130 // GetRevisionActions retrieves revision actions. 1131 func (c *Client) GetRevisionActions(ctx context.Context, changeID, revision string) (map[string]*ActionInfo, error) { 1132 var actions map[string]*ActionInfo 1133 err := c.do(ctx, &actions, "GET", "/changes/"+changeID+"/revisions/"+revision+"/actions") 1134 return actions, err 1135 } 1136 1137 // RelatedChangeAndCommitInfo contains information about a particular 1138 // change at a particular commit. 1139 // 1140 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#related-change-and-commit-info. 1141 type RelatedChangeAndCommitInfo struct { 1142 Project string `json:"project"` 1143 ChangeID string `json:"change_id"` 1144 ChangeNumber int32 `json:"_change_number"` 1145 Commit CommitInfo `json:"commit"` 1146 Status string `json:"status"` 1147 } 1148 1149 // RelatedChangesInfo contains information about a set of related changes. 1150 // 1151 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#related-changes-info. 1152 type RelatedChangesInfo struct { 1153 Changes []RelatedChangeAndCommitInfo `json:"changes"` 1154 } 1155 1156 // GetRelatedChanges retrieves information about a set of related changes. 1157 func (c *Client) GetRelatedChanges(ctx context.Context, changeID, revision string) (*RelatedChangesInfo, error) { 1158 var changes *RelatedChangesInfo 1159 err := c.do(ctx, &changes, "GET", "/changes/"+changeID+"/revisions/"+revision+"/related") 1160 return changes, err 1161 } 1162 1163 // GetCommitsInRefs gets refs in which the specified commits were merged into. 1164 // 1165 // See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#commits-included-in. 1166 func (c *Client) GetCommitsInRefs(ctx context.Context, project string, commits, refs []string) (map[string][]string, error) { 1167 result := map[string][]string{} 1168 vals := url.Values{} 1169 vals["commit"] = commits 1170 vals["ref"] = refs 1171 err := c.do(ctx, &result, "GET", "/projects/"+url.PathEscape(project)+"/commits:in", urlValues(vals)) 1172 return result, err 1173 }