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  }