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 }