github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/review/git-codereview/api.go (about)

     1  // Copyright 2014 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 main
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"net/http"
    14  	"os"
    15  	"sort"
    16  	"strings"
    17  )
    18  
    19  // auth holds cached data about authentication to Gerrit.
    20  var auth struct {
    21  	host    string // "go.googlesource.com"
    22  	url     string // "https://go-review.googlesource.com"
    23  	project string // "go", "tools", "crypto", etc
    24  
    25  	// Authentication information.
    26  	// Either cookie name + value from git cookie file
    27  	// or username and password from .netrc.
    28  	cookieName  string
    29  	cookieValue string
    30  	user        string
    31  	password    string
    32  }
    33  
    34  // loadGerritOrigin loads the Gerrit host name from the origin remote.
    35  // If the origin remote does not appear to be a Gerrit server
    36  // (is missing, is GitHub, is not https, has too many path elements),
    37  // loadGerritOrigin dies.
    38  func loadGerritOrigin() {
    39  	if auth.host != "" {
    40  		return
    41  	}
    42  
    43  	// Gerrit must be set, either explicitly via the code review config or
    44  	// implicitly as Git's origin remote.
    45  	origin := config()["gerrit"]
    46  	if origin == "" {
    47  		origin = trim(cmdOutput("git", "config", "remote.origin.url"))
    48  	}
    49  
    50  	if strings.Contains(origin, "github.com") {
    51  		dief("git origin must be a Gerrit host, not GitHub: %s", origin)
    52  	}
    53  
    54  	if !strings.HasPrefix(origin, "https://") {
    55  		dief("git origin must be an https:// URL: %s", origin)
    56  	}
    57  	// https:// prefix and then one slash between host and top-level name
    58  	if strings.Count(origin, "/") != 3 {
    59  		dief("git origin is malformed: %s", origin)
    60  	}
    61  	host := origin[len("https://"):strings.LastIndex(origin, "/")]
    62  
    63  	// In the case of Google's Gerrit, host is go.googlesource.com
    64  	// and apiURL uses go-review.googlesource.com, but the Gerrit
    65  	// setup instructions do not write down a cookie explicitly for
    66  	// go-review.googlesource.com, so we look for the non-review
    67  	// host name instead.
    68  	url := origin
    69  	if i := strings.Index(url, ".googlesource.com"); i >= 0 {
    70  		url = url[:i] + "-review" + url[i:]
    71  	}
    72  	i := strings.LastIndex(url, "/")
    73  	url, project := url[:i], url[i+1:]
    74  
    75  	auth.host = host
    76  	auth.url = url
    77  	auth.project = project
    78  }
    79  
    80  // loadAuth loads the authentication tokens for making API calls to
    81  // the Gerrit origin host.
    82  func loadAuth() {
    83  	if auth.user != "" || auth.cookieName != "" {
    84  		return
    85  	}
    86  
    87  	loadGerritOrigin()
    88  
    89  	// First look in Git's http.cookiefile, which is where Gerrit
    90  	// now tells users to store this information.
    91  	if cookieFile, _ := trimErr(cmdOutputErr("git", "config", "http.cookiefile")); cookieFile != "" {
    92  		data, _ := ioutil.ReadFile(cookieFile)
    93  		maxMatch := -1
    94  		for _, line := range lines(string(data)) {
    95  			f := strings.Split(line, "\t")
    96  			if len(f) >= 7 && (f[0] == auth.host || strings.HasPrefix(f[0], ".") && strings.HasSuffix(auth.host, f[0])) {
    97  				if len(f[0]) > maxMatch {
    98  					auth.cookieName = f[5]
    99  					auth.cookieValue = f[6]
   100  					maxMatch = len(f[0])
   101  				}
   102  			}
   103  		}
   104  		if maxMatch > 0 {
   105  			return
   106  		}
   107  	}
   108  
   109  	// If not there, then look in $HOME/.netrc, which is where Gerrit
   110  	// used to tell users to store the information, until the passwords
   111  	// got so long that old versions of curl couldn't handle them.
   112  	data, _ := ioutil.ReadFile(os.Getenv("HOME") + "/.netrc")
   113  	for _, line := range lines(string(data)) {
   114  		if i := strings.Index(line, "#"); i >= 0 {
   115  			line = line[:i]
   116  		}
   117  		f := strings.Fields(line)
   118  		if len(f) >= 6 && f[0] == "machine" && f[1] == auth.host && f[2] == "login" && f[4] == "password" {
   119  			auth.user = f[3]
   120  			auth.password = f[5]
   121  			return
   122  		}
   123  	}
   124  
   125  	dief("cannot find authentication info for %s", auth.host)
   126  }
   127  
   128  // gerritError is an HTTP error response served by Gerrit.
   129  type gerritError struct {
   130  	url        string
   131  	statusCode int
   132  	status     string
   133  	body       string
   134  }
   135  
   136  func (e *gerritError) Error() string {
   137  	if e.statusCode == http.StatusNotFound {
   138  		return "change not found on Gerrit server"
   139  	}
   140  
   141  	extra := strings.TrimSpace(e.body)
   142  	if extra != "" {
   143  		extra = ": " + extra
   144  	}
   145  	return fmt.Sprintf("%s%s", e.status, extra)
   146  }
   147  
   148  // gerritAPI executes a GET or POST request to a Gerrit API endpoint.
   149  // It uses GET when requestBody is nil, otherwise POST. If target != nil,
   150  // gerritAPI expects to get a 200 response with a body consisting of an
   151  // anti-xss line (]})' or some such) followed by JSON.
   152  // If requestBody != nil, gerritAPI sets the Content-Type to application/json.
   153  func gerritAPI(path string, requestBody []byte, target interface{}) error {
   154  	// Strictly speaking, we might be able to use unauthenticated
   155  	// access, by removing the /a/ from the URL, but that assumes
   156  	// that all the information we care about is publicly visible.
   157  	// Using authentication makes it possible for this to work with
   158  	// non-public CLs or Gerrit hosts too.
   159  	loadAuth()
   160  
   161  	if !strings.HasPrefix(path, "/") {
   162  		dief("internal error: gerritAPI called with malformed path")
   163  	}
   164  
   165  	url := auth.url + path
   166  	method := "GET"
   167  	var reader io.Reader
   168  	if requestBody != nil {
   169  		method = "POST"
   170  		reader = bytes.NewReader(requestBody)
   171  	}
   172  	req, err := http.NewRequest(method, url, reader)
   173  	if err != nil {
   174  		return err
   175  	}
   176  	if requestBody != nil {
   177  		req.Header.Set("Content-Type", "application/json")
   178  	}
   179  	if auth.cookieName != "" {
   180  		req.AddCookie(&http.Cookie{
   181  			Name:  auth.cookieName,
   182  			Value: auth.cookieValue,
   183  		})
   184  	} else {
   185  		req.SetBasicAuth(auth.user, auth.password)
   186  	}
   187  
   188  	resp, err := http.DefaultClient.Do(req)
   189  	if err != nil {
   190  		return err
   191  	}
   192  	body, err := ioutil.ReadAll(resp.Body)
   193  	resp.Body.Close()
   194  
   195  	if err != nil {
   196  		return fmt.Errorf("reading response body: %v", err)
   197  	}
   198  	if resp.StatusCode != http.StatusOK {
   199  		return &gerritError{url, resp.StatusCode, resp.Status, string(body)}
   200  	}
   201  
   202  	if target != nil {
   203  		i := bytes.IndexByte(body, '\n')
   204  		if i < 0 {
   205  			return fmt.Errorf("%s: malformed json response", url)
   206  		}
   207  		body = body[i:]
   208  		if err := json.Unmarshal(body, target); err != nil {
   209  			return fmt.Errorf("%s: malformed json response", url)
   210  		}
   211  	}
   212  	return nil
   213  }
   214  
   215  // fullChangeID returns the unambigous Gerrit change ID for the commit c on branch b.
   216  // The retruned ID has the form project~originbranch~Ihexhexhexhexhex.
   217  // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id for details.
   218  func fullChangeID(b *Branch, c *Commit) string {
   219  	loadGerritOrigin()
   220  	return auth.project + "~" + strings.TrimPrefix(b.OriginBranch(), "origin/") + "~" + c.ChangeID
   221  }
   222  
   223  // readGerritChange reads the metadata about a change from the Gerrit server.
   224  // The changeID should use the syntax project~originbranch~Ihexhexhexhexhex returned
   225  // by fullChangeID. Using only Ihexhexhexhexhex will work provided it uniquely identifies
   226  // a single change on the server.
   227  // The changeID can have additional query parameters appended to it, as in "normalid?o=LABELS".
   228  // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id for details.
   229  func readGerritChange(changeID string) (*GerritChange, error) {
   230  	var c GerritChange
   231  	err := gerritAPI("/a/changes/"+changeID, nil, &c)
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  	return &c, nil
   236  }
   237  
   238  // GerritChange is the JSON struct returned by a Gerrit CL query.
   239  type GerritChange struct {
   240  	ID              string
   241  	Project         string
   242  	Branch          string
   243  	ChangeId        string `json:"change_id"`
   244  	Subject         string
   245  	Status          string
   246  	Created         string
   247  	Updated         string
   248  	Mergeable       bool
   249  	Insertions      int
   250  	Deletions       int
   251  	Number          int `json:"_number"`
   252  	Owner           *GerritAccount
   253  	Labels          map[string]*GerritLabel
   254  	CurrentRevision string `json:"current_revision"`
   255  	Revisions       map[string]*GerritRevision
   256  	Messages        []*GerritMessage
   257  }
   258  
   259  // LabelNames returns the label names for the change, in lexicographic order.
   260  func (g *GerritChange) LabelNames() []string {
   261  	var names []string
   262  	for name := range g.Labels {
   263  		names = append(names, name)
   264  	}
   265  	sort.Strings(names)
   266  	return names
   267  }
   268  
   269  // GerritMessage is the JSON struct for a Gerrit MessageInfo.
   270  type GerritMessage struct {
   271  	Author struct {
   272  		Name string
   273  	}
   274  	Message string
   275  }
   276  
   277  // GerritLabel is the JSON struct for a Gerrit LabelInfo.
   278  type GerritLabel struct {
   279  	Optional bool
   280  	Blocking bool
   281  	Approved *GerritAccount
   282  	Rejected *GerritAccount
   283  	All      []*GerritApproval
   284  }
   285  
   286  // GerritAccount is the JSON struct for a Gerrit AccountInfo.
   287  type GerritAccount struct {
   288  	ID       int `json:"_account_id"`
   289  	Name     string
   290  	Email    string
   291  	Username string
   292  }
   293  
   294  // GerritApproval is the JSON struct for a Gerrit ApprovalInfo.
   295  type GerritApproval struct {
   296  	GerritAccount
   297  	Value int
   298  	Date  string
   299  }
   300  
   301  // GerritRevision is the JSON struct for a Gerrit RevisionInfo.
   302  type GerritRevision struct {
   303  	Number int `json:"_number"`
   304  	Ref    string
   305  	Fetch  map[string]*GerritFetch
   306  }
   307  
   308  // GerritFetch is the JSON struct for a Gerrit FetchInfo
   309  type GerritFetch struct {
   310  	URL string
   311  	Ref string
   312  }