gerrit: add Auth mode to use a specific cookie file, add more methods

This is stuff needed for my Github->Gerrit bot.

Change-Id: Ie072d7b66be2b219ba48245305a8187446238c9e
Reviewed-on: https://go-review.googlesource.com/19380
Reviewed-by: Andrew Gerrand <adg@golang.org>
diff --git a/gerrit/auth.go b/gerrit/auth.go
index 64371f6..527249e 100644
--- a/gerrit/auth.go
+++ b/gerrit/auth.go
@@ -5,6 +5,7 @@
 package gerrit
 
 import (
+	"fmt"
 	"io/ioutil"
 	"log"
 	"net/http"
@@ -15,6 +16,7 @@
 	"path/filepath"
 	"strconv"
 	"strings"
+	"sync"
 	"time"
 )
 
@@ -39,20 +41,24 @@
 
 // GitCookiesAuth derives the Gerrit authentication token from
 // gitcookies based on the URL of the Gerrit request.
+// The cookie file used is determined by running "git config
+// http.cookiefile" in the current directory.
+// To use a specific file, see GitCookieFileAuth.
 func GitCookiesAuth() Auth {
 	return gitCookiesAuth{}
 }
 
+// GitCookieFileAuth derives the Gerrit authentication token from the
+// provided gitcookies file. It is equivalent to GitCookiesAuth,
+// except that "git config http.cookiefile" is not used to find which
+// cookie file to use.
+func GitCookieFileAuth(file string) Auth {
+	return &gitCookieFileAuth{file: file}
+}
+
 type gitCookiesAuth struct{}
 
 func (gitCookiesAuth) setAuth(c *Client, r *http.Request) {
-	url, err := url.Parse(c.url)
-	if err != nil {
-		// Something else will complain about this.
-		return
-	}
-	host := url.Host
-
 	// First look in Git's http.cookiefile, which is where Gerrit
 	// now tells users to store this information.
 	git := exec.Command("git", "config", "http.cookiefile")
@@ -64,21 +70,23 @@
 	}
 	cookieFile := strings.TrimSpace(string(gitOut))
 	if len(cookieFile) != 0 {
-		// Load the cookie file.
-		data, _ := ioutil.ReadFile(cookieFile)
-		jar := parseGitCookies(string(data))
-		cookies := jar.Cookies(url)
-		if len(cookies) > 0 {
-			for _, cookie := range cookies {
-				r.AddCookie(cookie)
-			}
+		auth := &gitCookieFileAuth{file: cookieFile}
+		auth.setAuth(c, r)
+		if len(r.Header["Cookie"]) > 0 {
 			return
 		}
 	}
 
+	url, err := url.Parse(c.url)
+	if err != nil {
+		// Something else will complain about this.
+		return
+	}
+
 	// If not there, then look in $HOME/.netrc, which is where Gerrit
 	// used to tell users to store the information, until the passwords
 	// got so long that old versions of curl couldn't handle them.
+	host := url.Host
 	data, _ := ioutil.ReadFile(filepath.Join(os.Getenv("HOME"), ".netrc"))
 	for _, line := range strings.Split(string(data), "\n") {
 		if i := strings.Index(line, "#"); i >= 0 {
@@ -92,6 +100,41 @@
 	}
 }
 
+type gitCookieFileAuth struct {
+	file string
+
+	once sync.Once
+	jar  *cookiejar.Jar
+	err  error
+}
+
+func (a *gitCookieFileAuth) loadCookieFileOnce() {
+	data, err := ioutil.ReadFile(a.file)
+	if err != nil {
+		a.err = fmt.Errorf("Error loading cookie file: %v", err)
+		return
+	}
+	a.jar = parseGitCookies(string(data))
+}
+
+func (a *gitCookieFileAuth) setAuth(c *Client, r *http.Request) {
+	a.once.Do(a.loadCookieFileOnce)
+	if a.err != nil {
+		log.Print(a.err)
+		return
+	}
+
+	url, err := url.Parse(c.url)
+	if err != nil {
+		// Something else will complain about this.
+		return
+	}
+
+	for _, cookie := range a.jar.Cookies(url) {
+		r.AddCookie(cookie)
+	}
+}
+
 func parseGitCookies(data string) *cookiejar.Jar {
 	jar, _ := cookiejar.New(nil)
 	for _, line := range strings.Split(data, "\n") {
diff --git a/gerrit/gerrit.go b/gerrit/gerrit.go
index be1aab7..fbdc7a8 100644
--- a/gerrit/gerrit.go
+++ b/gerrit/gerrit.go
@@ -52,7 +52,52 @@
 	return http.DefaultClient
 }
 
-func (c *Client) do(dst interface{}, method, path string, arg url.Values, body interface{}) error {
+// HTTPError is the error type returned when a Gerrit API call does not return
+// the expected status.
+type HTTPError struct {
+	Res     *http.Response
+	Body    []byte // 4KB prefix
+	BodyErr error  // any error reading Body
+}
+
+func (e *HTTPError) Error() string {
+	return fmt.Sprintf("HTTP status %s; %s", e.Res.Status, e.Body)
+}
+
+// doArg is one of urlValues, reqBody, or wantResStatus
+type doArg interface {
+	isDoArg()
+}
+
+type wantResStatus int
+
+func (wantResStatus) isDoArg() {}
+
+type reqBody struct{ body interface{} }
+
+func (reqBody) isDoArg() {}
+
+type urlValues url.Values
+
+func (urlValues) isDoArg() {}
+
+func (c *Client) do(dst interface{}, method, path string, opts ...doArg) error {
+	var arg url.Values
+	var body interface{}
+	var wantStatus = http.StatusOK
+	for _, opt := range opts {
+		switch opt := opt.(type) {
+		case wantResStatus:
+			wantStatus = int(opt)
+		case reqBody:
+			body = opt.body
+		case urlValues:
+			arg = url.Values(opt)
+		default:
+			panic(fmt.Sprintf("internal error; unsupported type %T", opt))
+		}
+	}
+
 	var bodyr io.Reader
 	var contentType string
 	if body != nil {
@@ -88,9 +133,9 @@
 	}
 	defer res.Body.Close()
 
-	if res.StatusCode != http.StatusOK {
-		body, _ := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
-		return fmt.Errorf("HTTP status %s; %s", res.Status, body)
+	if res.StatusCode != wantStatus {
+		body, err := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
+		return &HTTPError{res, body, err}
 	}
 
 	// The JSON response begins with an XSRF-defeating header
@@ -289,11 +334,11 @@
 		return nil, errors.New("only 1 option struct supported")
 	}
 	var changes []*ChangeInfo
-	err := c.do(&changes, "GET", "/changes/", url.Values{
+	err := c.do(&changes, "GET", "/changes/", urlValues{
 		"q": {q},
 		"n": condInt(opt.N),
 		"o": opt.Fields,
-	}, nil)
+	})
 	return changes, err
 }
 
@@ -302,7 +347,7 @@
 // For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-change-detail
 func (c *Client) GetChangeDetail(changeID string) (*ChangeInfo, error) {
 	var change ChangeInfo
-	err := c.do(&change, "GET", "/changes/"+changeID+"/detail", nil, nil)
+	err := c.do(&change, "GET", "/changes/"+changeID+"/detail")
 	if err != nil {
 		return nil, err
 	}
@@ -325,13 +370,88 @@
 func (c *Client) SetReview(changeID, revision string, review ReviewInput) error {
 	var res reviewInfo
 	return c.do(&res, "POST", fmt.Sprintf("/changes/%s/revisions/%s/review", changeID, revision),
-		nil, review)
+		reqBody{review})
 }
 
 // AbandonChange abandons the given change.
 func (c *Client) AbandonChange(changeID string) error {
 	var change ChangeInfo
-	return c.do(&change, "POST", "/changes/"+changeID+"/abandon", nil, nil)
+	return c.do(&change, "POST", "/changes/"+changeID+"/abandon")
+}
+
+// ProjectInput contains the options for creating a new project.
+// See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-input
+type ProjectInput struct {
+	Parent      string `json:"parent,omitempty"`
+	Description string `json:"description,omitempty"`
+	SubmitType  string `json:"submit_type,omitempty"`
+
+	CreateNewChangeForAllNotInTarget string `json:"create_new_change_for_all_not_in_target,omitempty"`
+
+	// TODO(bradfitz): more, as needed.
+}
+
+// ProjectInfo is information about a Gerrit project.
+// See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-info
+type ProjectInfo struct {
+	ID          string            `json:"id"`
+	Name        string            `json:"name"`
+	Parent      string            `json:"parent"`
+	Description string            `json:"description"`
+	State       string            `json:"state"`
+	Branches    map[string]string `json:"branches"`
+}
+
+// CreateProject creates a new project.
+func (c *Client) CreateProject(name string, p ...ProjectInput) (ProjectInfo, error) {
+	var pi ProjectInput
+	if len(p) > 1 {
+		panic("invalid use of multiple project inputs")
+	}
+	if len(p) == 1 {
+		pi = p[0]
+	}
+	var res ProjectInfo
+	err := c.do(&res, "PUT", fmt.Sprintf("/projects/%s", name), reqBody{&pi}, wantResStatus(http.StatusCreated))
+	return res, err
+}
+
+// ErrProjectNotExist is returned when a project doesn't exist.
+// It is not necessarily returned unless a method is documented as
+// returning it.
+var ErrProjectNotExist = errors.New("gerrit: requested project does not exist")
+
+// GetProjectInfo returns info about a project.
+// If the project doesn't exist, the error will be ErrProjectNotExist.
+func (c *Client) GetProjectInfo(name string) (ProjectInfo, error) {
+	var res ProjectInfo
+	err := c.do(&res, "GET", fmt.Sprintf("/projects/%s", name))
+	if he, ok := err.(*HTTPError); ok && he.Res.StatusCode == 404 {
+		return res, ErrProjectNotExist
+	}
+	return res, err
+}
+
+// BranchInfo is information about a branch.
+// See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#branch-info
+type BranchInfo struct {
+	Ref       string `json:"ref"`
+	Revision  string `json:"revision"`
+	CanDelete bool   `json:"can_delete"`
+}
+
+// GetProjectBranches returns a project's branches.
+func (c *Client) GetProjectBranches(name string) (map[string]BranchInfo, error) {
+	var res []BranchInfo
+	err := c.do(&res, "GET", fmt.Sprintf("/projects/%s/branches/", name))
+	if err != nil {
+		return nil, err
+	}
+	m := map[string]BranchInfo{}
+	for _, bi := range res {
+		m[bi.Ref] = bi
+	}
+	return m, nil
 }
 
 // GetAccountInfo gets the specified account's information from Gerrit.
@@ -342,7 +462,7 @@
 // access to the host, not to any particular repository.
 func (c *Client) GetAccountInfo(accountID string) (AccountInfo, error) {
 	var res AccountInfo
-	err := c.do(&res, "GET", fmt.Sprintf("/accounts/%s", accountID), nil, nil)
+	err := c.do(&res, "GET", fmt.Sprintf("/accounts/%s", accountID))
 	return res, err
 }