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  }