v.io/jiri@v0.0.0-20160715023856-abfb8b131290/gerrit/gerrit.go (about)

     1  // Copyright 2015 The Vanadium 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 provides library functions for interacting with the
     6  // gerrit code review system.
     7  package gerrit
     8  
     9  import (
    10  	"bufio"
    11  	"bytes"
    12  	"encoding/json"
    13  	"fmt"
    14  	"io"
    15  	"io/ioutil"
    16  	"net/http"
    17  	"net/url"
    18  	"regexp"
    19  	"strconv"
    20  	"strings"
    21  
    22  	"v.io/jiri/collect"
    23  	"v.io/jiri/gitutil"
    24  	"v.io/jiri/runutil"
    25  )
    26  
    27  var (
    28  	autosubmitRE    = regexp.MustCompile("AutoSubmit")
    29  	remoteRE        = regexp.MustCompile("remote:[^\n]*")
    30  	multiPartRE     = regexp.MustCompile(`MultiPart:\s*(\d+)\s*/\s*(\d+)`)
    31  	presubmitTestRE = regexp.MustCompile(`PresubmitTest:\s*(.*)`)
    32  
    33  	queryParameters = []string{"CURRENT_REVISION", "CURRENT_COMMIT", "CURRENT_FILES", "LABELS", "DETAILED_ACCOUNTS"}
    34  )
    35  
    36  // Comment represents a single inline file comment.
    37  type Comment struct {
    38  	Line    int    `json:"line,omitempty"`
    39  	Message string `json:"message,omitempty"`
    40  }
    41  
    42  // Review represents a Gerrit review. For more details, see:
    43  // http://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#review-input
    44  type Review struct {
    45  	Message  string               `json:"message,omitempty"`
    46  	Labels   map[string]string    `json:"labels,omitempty"`
    47  	Comments map[string][]Comment `json:"comments,omitempty"`
    48  }
    49  
    50  // CLOpts records the review options.
    51  type CLOpts struct {
    52  	// Autosubmit determines if the CL should be auto-submitted when it
    53  	// meets the submission rules.
    54  	Autosubmit bool
    55  	// Branch identifies the local branch that contains the CL.
    56  	Branch string
    57  	// Ccs records a list of email addresses to cc on the CL.
    58  	Ccs []string
    59  	// Draft determines if this CL is a draft.
    60  	Draft bool
    61  	// Edit determines if the user should be prompted to edit the commit
    62  	// message when the CL is exported to Gerrit.
    63  	Edit bool
    64  	// Remote identifies the Gerrit remote that this CL will be pushed to
    65  	Remote string
    66  	// Host identifies the Gerrit host.
    67  	Host *url.URL
    68  	// Presubmit determines what presubmit tests to run.
    69  	Presubmit PresubmitTestType
    70  	// RemoteBranch identifies the remote branch the CL pertains to.
    71  	RemoteBranch string
    72  	// Reviewers records a list of email addresses of CL reviewers.
    73  	Reviewers []string
    74  	// Topic records the CL topic.
    75  	Topic string
    76  	// Verify controls whether git pre-push hooks should be run before uploading.
    77  	Verify bool
    78  }
    79  
    80  // Gerrit records a hostname of a Gerrit instance.
    81  type Gerrit struct {
    82  	host *url.URL
    83  	s    runutil.Sequence
    84  }
    85  
    86  // New is the Gerrit factory.
    87  func New(s runutil.Sequence, host *url.URL) *Gerrit {
    88  	return &Gerrit{
    89  		host: host,
    90  		s:    s,
    91  	}
    92  }
    93  
    94  // PostReview posts a review to the given Gerrit reference.
    95  func (g *Gerrit) PostReview(ref string, message string, labels map[string]string) (e error) {
    96  	cred, err := hostCredentials(g.s, g.host)
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	review := Review{
   102  		Message: message,
   103  		Labels:  labels,
   104  	}
   105  
   106  	// Encode "review" as JSON.
   107  	encodedBytes, err := json.Marshal(review)
   108  	if err != nil {
   109  		return fmt.Errorf("Marshal(%#v) failed: %v", review, err)
   110  	}
   111  
   112  	// Construct API URL.
   113  	// ref is in the form of "refs/changes/<last two digits of change number>/<change number>/<patch set number>".
   114  	parts := strings.Split(ref, "/")
   115  	if expected, got := 5, len(parts); expected != got {
   116  		return fmt.Errorf("unexpected number of %q parts: expected %v, got %v", ref, expected, got)
   117  	}
   118  	cl, revision := parts[3], parts[4]
   119  	url := fmt.Sprintf("%s/a/changes/%s/revisions/%s/review", g.host, cl, revision)
   120  
   121  	// Post the review.
   122  	method, body := "POST", bytes.NewReader(encodedBytes)
   123  	req, err := http.NewRequest(method, url, body)
   124  	if err != nil {
   125  		return fmt.Errorf("NewRequest(%q, %q, %v) failed: %v", method, url, body, err)
   126  	}
   127  	req.Header.Add("Content-Type", "application/json;charset=UTF-8")
   128  	req.SetBasicAuth(cred.username, cred.password)
   129  	res, err := http.DefaultClient.Do(req)
   130  	if err != nil {
   131  		return fmt.Errorf("Do(%v) failed: %v", req, err)
   132  	}
   133  	if res.StatusCode != http.StatusOK {
   134  		return fmt.Errorf("PostReview:Do(%v) failed: %v", req, res.StatusCode)
   135  	}
   136  	defer collect.Error(func() error { return res.Body.Close() }, &e)
   137  
   138  	return nil
   139  }
   140  
   141  type Topic struct {
   142  	Topic string `json:"topic"`
   143  }
   144  
   145  // SetTopic sets the topic of the given Gerrit reference.
   146  func (g *Gerrit) SetTopic(cl string, opts CLOpts) (e error) {
   147  	cred, err := hostCredentials(g.s, g.host)
   148  	if err != nil {
   149  		return err
   150  	}
   151  	topic := Topic{opts.Topic}
   152  	data, err := json.Marshal(topic)
   153  	if err != nil {
   154  		return fmt.Errorf("Marshal(%#v) failed: %v", topic, err)
   155  	}
   156  
   157  	url := fmt.Sprintf("%s/a/changes/%s/topic", g.host, cl)
   158  	method, body := "PUT", bytes.NewReader(data)
   159  	req, err := http.NewRequest(method, url, body)
   160  	if err != nil {
   161  		return fmt.Errorf("NewRequest(%q, %q, %v) failed: %v", method, url, body, err)
   162  	}
   163  	req.Header.Add("Content-Type", "application/json;charset=UTF-8")
   164  	req.SetBasicAuth(cred.username, cred.password)
   165  	res, err := http.DefaultClient.Do(req)
   166  	if err != nil {
   167  		return fmt.Errorf("Do(%v) failed: %v", req, err)
   168  	}
   169  	if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent {
   170  		return fmt.Errorf("SetTopic:Do(%v) failed: %v", req, res.StatusCode)
   171  	}
   172  	defer collect.Error(func() error { return res.Body.Close() }, &e)
   173  
   174  	return nil
   175  }
   176  
   177  // The following types reflect the schema Gerrit uses to represent
   178  // CLs.
   179  type CLList []Change
   180  type CLRefMap map[string]Change
   181  type Change struct {
   182  	// CL data.
   183  	Change_id        string
   184  	Current_revision string
   185  	Project          string
   186  	Topic            string
   187  	Revisions        Revisions
   188  	Owner            Owner
   189  	Labels           map[string]map[string]interface{}
   190  
   191  	// Custom labels.
   192  	AutoSubmit    bool
   193  	MultiPart     *MultiPartCLInfo
   194  	PresubmitTest PresubmitTestType
   195  }
   196  type Revisions map[string]Revision
   197  type Revision struct {
   198  	Fetch  `json:"fetch"`
   199  	Commit `json:"commit"`
   200  	Files  `json:"files"`
   201  }
   202  type Fetch struct {
   203  	Http `json:"http"`
   204  }
   205  type Http struct {
   206  	Ref string
   207  }
   208  type Commit struct {
   209  	Message string
   210  }
   211  type Owner struct {
   212  	Email string
   213  }
   214  type Files map[string]struct{}
   215  type ChangeError struct {
   216  	Err error
   217  	CL  Change
   218  }
   219  
   220  func (ce *ChangeError) Error() string {
   221  	return ce.Err.Error()
   222  }
   223  
   224  func NewChangeError(cl Change, err error) *ChangeError {
   225  	return &ChangeError{err, cl}
   226  }
   227  
   228  func (c Change) Reference() string {
   229  	return c.Revisions[c.Current_revision].Fetch.Http.Ref
   230  }
   231  
   232  func (c Change) OwnerEmail() string {
   233  	return c.Owner.Email
   234  }
   235  
   236  type PresubmitTestType string
   237  
   238  const (
   239  	PresubmitTestTypeNone PresubmitTestType = "none"
   240  	PresubmitTestTypeAll  PresubmitTestType = "all"
   241  )
   242  
   243  func PresubmitTestTypes() []string {
   244  	return []string{string(PresubmitTestTypeNone), string(PresubmitTestTypeAll)}
   245  }
   246  
   247  // parseQueryResults parses a list of Gerrit ChangeInfo entries (json
   248  // result of a query) and returns a list of Change entries.
   249  func parseQueryResults(reader io.Reader) (CLList, error) {
   250  	r := bufio.NewReader(reader)
   251  
   252  	// The first line of the input is the XSSI guard
   253  	// ")]}'". Getting rid of that.
   254  	if _, err := r.ReadSlice('\n'); err != nil {
   255  		return nil, err
   256  	}
   257  
   258  	// Parse the remaining input to construct a slice of Change objects
   259  	// to return.
   260  	var changes CLList
   261  	if err := json.NewDecoder(r).Decode(&changes); err != nil {
   262  		return nil, fmt.Errorf("Decode() failed: %v", err)
   263  	}
   264  
   265  	newChanges := CLList{}
   266  	for _, change := range changes {
   267  		clMessage := change.Revisions[change.Current_revision].Commit.Message
   268  		multiPartCLInfo, err := parseMultiPartMatch(clMessage)
   269  		if err != nil {
   270  			return nil, err
   271  		}
   272  		if multiPartCLInfo != nil {
   273  			multiPartCLInfo.Topic = change.Topic
   274  		}
   275  		change.MultiPart = multiPartCLInfo
   276  		change.PresubmitTest = parsePresubmitTestType(clMessage)
   277  		change.AutoSubmit = autosubmitRE.FindStringSubmatch(clMessage) != nil
   278  		newChanges = append(newChanges, change)
   279  	}
   280  	return newChanges, nil
   281  }
   282  
   283  // parseMultiPartMatch uses multiPartRE (a pattern like: MultiPart: 1/3) to match the given string.
   284  func parseMultiPartMatch(match string) (*MultiPartCLInfo, error) {
   285  	matches := multiPartRE.FindStringSubmatch(match)
   286  	if matches != nil {
   287  		index, err := strconv.Atoi(matches[1])
   288  		if err != nil {
   289  			return nil, fmt.Errorf("Atoi(%q) failed: %v", matches[1], err)
   290  		}
   291  		total, err := strconv.Atoi(matches[2])
   292  		if err != nil {
   293  			return nil, fmt.Errorf("Atoi(%q) failed: %v", matches[2], err)
   294  		}
   295  		return &MultiPartCLInfo{
   296  			Index: index,
   297  			Total: total,
   298  		}, nil
   299  	}
   300  	return nil, nil
   301  }
   302  
   303  // parsePresubmitTestType uses presubmitTestRE to match the given string and
   304  // returns the presubmit test type.
   305  func parsePresubmitTestType(match string) PresubmitTestType {
   306  	ret := PresubmitTestTypeAll
   307  	matches := presubmitTestRE.FindStringSubmatch(match)
   308  	if matches != nil {
   309  		switch matches[1] {
   310  		case string(PresubmitTestTypeNone):
   311  			ret = PresubmitTestTypeNone
   312  		case string(PresubmitTestTypeAll):
   313  			ret = PresubmitTestTypeAll
   314  		}
   315  	}
   316  	return ret
   317  }
   318  
   319  // Query returns a list of QueryResult entries matched by the given
   320  // Gerrit query string from the given Gerrit instance. The result is
   321  // sorted by the last update time, most recently updated to oldest
   322  // updated.
   323  //
   324  // See the following links for more details about Gerrit search syntax:
   325  // - https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
   326  // - https://gerrit-review.googlesource.com/Documentation/user-search.html
   327  func (g *Gerrit) Query(query string) (_ CLList, e error) {
   328  	cred, err := hostCredentials(g.s, g.host)
   329  	if err != nil {
   330  		return nil, err
   331  	}
   332  
   333  	u, err := url.Parse(g.host.String())
   334  	if err != nil {
   335  		return nil, err
   336  	}
   337  	u.Path = "/a/changes/"
   338  	v := url.Values{}
   339  	v.Set("q", query)
   340  	for _, o := range queryParameters {
   341  		v.Add("o", o)
   342  	}
   343  	u.RawQuery = v.Encode()
   344  	url := u.String()
   345  
   346  	var body io.Reader
   347  	method, body := "GET", nil
   348  	req, err := http.NewRequest(method, url, body)
   349  	if err != nil {
   350  		return nil, fmt.Errorf("NewRequest(%q, %q, %v) failed: %v", method, url, body, err)
   351  	}
   352  	req.Header.Add("Accept", "application/json")
   353  	req.SetBasicAuth(cred.username, cred.password)
   354  
   355  	res, err := http.DefaultClient.Do(req)
   356  	if err != nil {
   357  		return nil, fmt.Errorf("Do(%v) failed: %v", req, err)
   358  	}
   359  	if res.StatusCode != http.StatusOK {
   360  		return nil, fmt.Errorf("Query:Do(%v) failed: %v", req, res.StatusCode)
   361  	}
   362  	defer collect.Error(func() error { return res.Body.Close() }, &e)
   363  	return parseQueryResults(res.Body)
   364  }
   365  
   366  // GetChange returns a Change object for the given changeId number.
   367  func (g *Gerrit) GetChange(changeNumber int) (*Change, error) {
   368  	clList, err := g.Query(fmt.Sprintf("%d", changeNumber))
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  	if len(clList) == 0 {
   373  		return nil, fmt.Errorf("Query for change '%d' returned no results", changeNumber)
   374  	}
   375  	if len(clList) > 1 {
   376  		// Based on cursory testing with Gerrit, I don't expect this to ever happen, but in
   377  		// case it does, I'm raising an error to inspire investigation. -- lanechr
   378  		return nil, fmt.Errorf("Too many changes returned for query '%d'", changeNumber)
   379  	}
   380  	return &clList[0], nil
   381  }
   382  
   383  // Submit submits the given changelist through Gerrit.
   384  func (g *Gerrit) Submit(changeID string) (e error) {
   385  	cred, err := hostCredentials(g.s, g.host)
   386  	if err != nil {
   387  		return err
   388  	}
   389  
   390  	// Encode data needed for Submit.
   391  	data := struct {
   392  		WaitForMerge bool `json:"wait_for_merge"`
   393  	}{
   394  		WaitForMerge: true,
   395  	}
   396  	encodedBytes, err := json.Marshal(data)
   397  	if err != nil {
   398  		return fmt.Errorf("Marshal(%#v) failed: %v", data, err)
   399  	}
   400  
   401  	// Call Submit API.
   402  	// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submit-change
   403  	url := fmt.Sprintf("%s/a/changes/%s/submit", g.host, changeID)
   404  	var body io.Reader
   405  	method, body := "POST", bytes.NewReader(encodedBytes)
   406  	req, err := http.NewRequest(method, url, body)
   407  	if err != nil {
   408  		return fmt.Errorf("NewRequest(%q, %q, %v) failed: %v", method, url, body, err)
   409  	}
   410  	req.Header.Add("Content-Type", "application/json;charset=UTF-8")
   411  	req.SetBasicAuth(cred.username, cred.password)
   412  
   413  	res, err := http.DefaultClient.Do(req)
   414  	if err != nil {
   415  		return fmt.Errorf("Do(%v) failed: %v", req, err)
   416  	}
   417  	if res.StatusCode != http.StatusOK {
   418  		return fmt.Errorf("Submit:Do(%v) failed: %v", req, res.StatusCode)
   419  	}
   420  	defer collect.Error(func() error { return res.Body.Close() }, &e)
   421  
   422  	// Check response.
   423  	bytes, err := ioutil.ReadAll(res.Body)
   424  	if err != nil {
   425  		return err
   426  	}
   427  	resContent := string(bytes)
   428  	// For a "TBR" CL, the response code is not 200 but the submit will still succeed.
   429  	// In those cases, the "error" message will be "change is new".
   430  	// We don't treat this case as error.
   431  	if res.StatusCode != http.StatusOK && strings.TrimSpace(resContent) != "change is new" {
   432  		return fmt.Errorf("Failed to submit CL %q:\n%s", changeID, resContent)
   433  	}
   434  
   435  	return nil
   436  }
   437  
   438  // formatParams formats parameters of a change list.
   439  func formatParams(params []string, key string) []string {
   440  	var keyedParams []string
   441  	for _, param := range params {
   442  		keyedParams = append(keyedParams, key+"="+param)
   443  	}
   444  	return keyedParams
   445  }
   446  
   447  // Reference inputs CL options and returns a matching string
   448  // representation of a Gerrit reference.
   449  func Reference(opts CLOpts) string {
   450  	var ref string
   451  	if opts.Draft {
   452  		ref = "refs/drafts/" + opts.RemoteBranch
   453  	} else {
   454  		ref = "refs/for/" + opts.RemoteBranch
   455  	}
   456  	var params []string
   457  	params = append(params, formatParams(opts.Reviewers, "r")...)
   458  	params = append(params, formatParams(opts.Ccs, "cc")...)
   459  	if len(params) > 0 {
   460  		ref = ref + "%" + strings.Join(params, ",")
   461  	}
   462  	return ref
   463  }
   464  
   465  // Push pushes the current branch to Gerrit.
   466  func Push(seq runutil.Sequence, clOpts CLOpts) error {
   467  	refspec := "HEAD:" + Reference(clOpts)
   468  	args := []string{"push", clOpts.Remote, refspec}
   469  	// TODO(jamesr): This should really reuse gitutil/git.go's Push which knows
   470  	// how to set this option but doesn't yet know how to pipe stdout/stderr the way
   471  	// this function wants.
   472  	if clOpts.Verify {
   473  		args = append(args, "--verify")
   474  	} else {
   475  		args = append(args, "--no-verify")
   476  	}
   477  	var stdout, stderr bytes.Buffer
   478  	if err := seq.Capture(&stdout, &stderr).Last("git", args...); err != nil {
   479  		return gitutil.Error(stdout.String(), stderr.String(), args...)
   480  	}
   481  	for _, line := range strings.Split(stderr.String(), "\n") {
   482  		if remoteRE.MatchString(line) {
   483  			fmt.Println(line)
   484  		}
   485  	}
   486  	return nil
   487  }
   488  
   489  // ParseRefString parses the cl and patchset number from the given ref string.
   490  func ParseRefString(ref string) (int, int, error) {
   491  	parts := strings.Split(ref, "/")
   492  	if expected, got := 5, len(parts); expected != got {
   493  		return -1, -1, fmt.Errorf("unexpected number of %q parts: expected %v, got %v", ref, expected, got)
   494  	}
   495  	cl, err := strconv.Atoi(parts[3])
   496  	if err != nil {
   497  		return -1, -1, fmt.Errorf("Atoi(%q) failed: %v", parts[3], err)
   498  	}
   499  	patchset, err := strconv.Atoi(parts[4])
   500  	if err != nil {
   501  		return -1, -1, fmt.Errorf("Atoi(%q) failed: %v", parts[4], err)
   502  	}
   503  	return cl, patchset, nil
   504  }