gerrit: new package for server interactions
diff --git a/cmd/checker/main.go b/cmd/checker/main.go
index 6a6a67d..f16d6bf 100644
--- a/cmd/checker/main.go
+++ b/cmd/checker/main.go
@@ -27,198 +27,13 @@
 	"net/rpc"
 	"net/url"
 	"os"
-	"path"
 	"strings"
-	"time"
 
 	"github.com/google/fmtserver"
+	"github.com/google/fmtserver/gerrit"
 	"github.com/google/slothfs/cookie"
 )
 
-var jsonPrefix = []byte(")]}'")
-
-type File struct {
-	Status        string
-	LinesInserted int `json:"lines_inserted"`
-	SizeDelta     int `json:"size_delta"`
-	Size          int
-	Content       []byte
-}
-
-type Change struct {
-	Files map[string]*File
-}
-
-type gerrit struct {
-	UserAgent string
-	URL       url.URL
-	client    http.Client
-	basicAuth string
-}
-
-func newGerrit(u url.URL) *gerrit {
-	g := &gerrit{
-		URL: u,
-	}
-
-	g.client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
-		g.setRequest(req)
-		return nil
-	}
-
-	return g
-}
-
-func (g *gerrit) setAuth(auth []byte) {
-	encoded := make([]byte, base64.StdEncoding.EncodedLen(len(auth)))
-	base64.StdEncoding.Encode(encoded, auth)
-	g.basicAuth = string(encoded)
-}
-
-func (g *gerrit) setRequest(req *http.Request) {
-	req.Header.Set("User-Agent", g.UserAgent)
-	req.Header.Set("Authorization", "Basic "+string(g.basicAuth))
-}
-
-func (g *gerrit) getPath(p string) ([]byte, error) {
-	u := g.URL
-	u.Path = path.Join(u.Path, p)
-	if strings.HasSuffix(p, "/") && !strings.HasSuffix(u.Path, "/") {
-		// Ugh.
-		u.Path += "/"
-	}
-
-	req, err := http.NewRequest("GET", u.String(), nil)
-	if err != nil {
-		return nil, err
-	}
-	g.setRequest(req)
-	rep, err := g.client.Do(req)
-	if err != nil {
-		return nil, err
-	}
-	if rep.StatusCode != 200 {
-		return nil, fmt.Errorf("Get %s: status %d", u.String(), rep.StatusCode)
-	}
-
-	defer rep.Body.Close()
-	return ioutil.ReadAll(rep.Body)
-}
-
-func (g *gerrit) postPath(p string, contentType string, content []byte) ([]byte, error) {
-	u := g.URL
-	u.Path = path.Join(u.Path, p)
-	if strings.HasSuffix(p, "/") && !strings.HasSuffix(u.Path, "/") {
-		// Ugh.
-		u.Path += "/"
-	}
-	req, err := http.NewRequest("POST", u.String(), bytes.NewBuffer(content))
-	if err != nil {
-		return nil, err
-	}
-	g.setRequest(req)
-	req.Header.Set("Content-Type", contentType)
-	rep, err := g.client.Do(req)
-	if err != nil {
-		return nil, err
-	}
-	if rep.StatusCode/100 != 2 {
-		return nil, fmt.Errorf("Post %s: status %d", u.String(), rep.StatusCode)
-	}
-
-	defer rep.Body.Close()
-	return ioutil.ReadAll(rep.Body)
-}
-
-// GetContent returns the file content from a file in a change.
-func (g *gerrit) GetContent(changeID string, revID string, fileID string) ([]byte, error) {
-	c, err := g.getPath(fmt.Sprintf("changes/%s/revisions/%s/files/%s/content",
-		url.PathEscape(changeID), revID, url.PathEscape(fileID)))
-	if err != nil {
-		return nil, err
-	}
-
-	dest := make([]byte, base64.StdEncoding.DecodedLen(len(c)))
-	n, err := base64.StdEncoding.Decode(dest, c)
-	if err != nil {
-		return nil, err
-	}
-	return dest[:n], nil
-}
-
-// GetChange returns the Change (including file contents) for a given change.
-func (g *gerrit) GetChange(changeID string, revID string) (*Change, error) {
-	content, err := g.getPath(fmt.Sprintf("changes/%s/revisions/%s/files/",
-		url.PathEscape(changeID), revID))
-	if err != nil {
-		return nil, err
-	}
-	content = bytes.TrimPrefix(content, jsonPrefix)
-
-	files := map[string]*File{}
-	if err := json.Unmarshal(content, &files); err != nil {
-		return nil, err
-	}
-
-	for name := range files {
-		c, err := g.GetContent(changeID, revID, name)
-		if err != nil {
-			return nil, err
-		}
-
-		files[name].Content = c
-	}
-	return &Change{files}, nil
-}
-
-type CheckerInput struct {
-	UUID        string   `json:"uuid"`
-	Name        string   `json:"name"`
-	Description string   `json:"description"`
-	URL         string   `json:"url"`
-	Repository  string   `json:"repository"`
-	Status      string   `json:"status"`
-	Blocking    []string `json:"blocking"`
-	Query       string   `json:"query"`
-}
-
-const timeLayout = "2006-01-02 15:04:05.000000000"
-
-type Timestamp time.Time
-
-func (ts *Timestamp) String() string {
-	return ((time.Time)(*ts)).String()
-}
-
-func (ts *Timestamp) MarshalJSON() ([]byte, error) {
-	t := (*time.Time)(ts)
-	return []byte("\"" + t.Format(timeLayout) + "\""), nil
-}
-
-func (ts *Timestamp) UnmarshalJSON(b []byte) error {
-	b = bytes.TrimPrefix(b, []byte{'"'})
-	b = bytes.TrimSuffix(b, []byte{'"'})
-	t, err := time.Parse(timeLayout, string(b))
-	if err != nil {
-		return err
-	}
-	*ts = Timestamp(t)
-	return nil
-}
-
-type CheckerInfo struct {
-	UUID        string `json:"uuid"`
-	Name        string
-	Description string
-	URL         string `json:"url"`
-	Repository  string `json:"repository"`
-	Status      string
-	Blocking    []string  `json:"blocking"`
-	Query       string    `json:"query"`
-	Created     Timestamp `json:"created"`
-	Updated     Timestamp `json:"updated"`
-}
-
 type wrapJar struct {
 	http.CookieJar
 }
@@ -235,12 +50,12 @@
 
 const checkerScheme = "fmt:"
 
-func (g *gerrit) CreateChecker(repo string) (*CheckerInfo, error) {
+func CreateChecker(s *gerrit.Server, repo string) (*gerrit.CheckerInfo, error) {
 	var uuidRandom [20]byte
 	rand.Reader.Read(uuidRandom[:])
 
 	uuid := fmt.Sprintf("%s%x", checkerScheme, uuidRandom)
-	in := CheckerInput{
+	in := gerrit.CheckerInput{
 		UUID:        uuid,
 		Name:        "fmtserver",
 		Repository:  repo,
@@ -254,13 +69,13 @@
 		return nil, err
 	}
 
-	content, err := g.postPath("a/plugins/checks/checkers/", "application/json", body)
+	content, err := s.PostPath("a/plugins/checks/checkers/", "application/json", body)
 	if err != nil {
 		return nil, err
 	}
 
-	out := CheckerInfo{}
-	if err := unmarshal(content, &out); err != nil {
+	out := gerrit.CheckerInfo{}
+	if err := gerrit.Unmarshal(content, &out); err != nil {
 		return nil, err
 	}
 
@@ -273,29 +88,14 @@
 	return &out, nil
 }
 
-func unmarshal(content []byte, dest interface{}) error {
-	if !bytes.HasPrefix(content, jsonPrefix) {
-		if len(content) > 100 {
-			content = content[:100]
-		}
-		bodyStr := string(content)
-
-		return fmt.Errorf("prefix %q not found, got %s", jsonPrefix, bodyStr)
-	}
-
-	content = bytes.TrimPrefix(content, []byte(jsonPrefix))
-
-	return json.Unmarshal(content, dest)
-}
-
-func (g *gerrit) ListCheckers() ([]*CheckerInfo, error) {
-	c, err := g.getPath("plugins/checks/checkers/")
+func ListCheckers(g *gerrit.Server) ([]*gerrit.CheckerInfo, error) {
+	c, err := g.GetPath("plugins/checks/checkers/")
 	if err != nil {
 		log.Fatalf("ListCheckers: %v", err)
 	}
 
-	var out []*CheckerInfo
-	if err := unmarshal(c, &out); err != nil {
+	var out []*gerrit.CheckerInfo
+	if err := gerrit.Unmarshal(c, &out); err != nil {
 		return nil, err
 	}
 
@@ -311,12 +111,12 @@
 }
 
 type gerritChecker struct {
-	gerrit    *gerrit
+	server    *gerrit.Server
 	fmtClient *rpc.Client
 }
 
 func (c *gerritChecker) checkChange(changeID string) error {
-	ch, err := c.gerrit.GetChange(changeID, "current")
+	ch, err := c.server.GetChange(changeID, "current")
 	if err != nil {
 		return err
 	}
@@ -361,7 +161,7 @@
 	list := flag.Bool("list", false, "List pending checks")
 	agent := flag.String("agent", "fmtserver", "user-agent for the fmtserver.")
 	cookieJar := flag.String("cookies", "", "comma separated paths to cURL-style cookie jar file.")
-	auth := flag.String("auth_file", "", "file containing user:password")
+	authFile := flag.String("auth_file", "", "file containing user:password")
 	repo := flag.String("repo", "", "the repository (project) name to apply the checker to.")
 	flag.Parse()
 	if *gerritURL == "" {
@@ -373,7 +173,7 @@
 		log.Fatalf("url.Parse: %v", err)
 	}
 
-	g := newGerrit(*u)
+	g := gerrit.New(*u)
 
 	if nm := *cookieJar; nm != "" {
 		jar, err := cookie.NewJar(nm)
@@ -384,26 +184,29 @@
 			log.Printf("WatchJar: %v", err)
 			log.Println("continuing despite WatchJar failure", err)
 		}
-		g.client.Jar = &wrapJar{jar}
+		g.Client.Jar = &wrapJar{jar}
 	}
 	g.UserAgent = *agent
 
-	if *auth == "" {
+	if *authFile == "" {
 		log.Fatal("must set --auth_file")
 	}
-	if content, err := ioutil.ReadFile(*auth); err != nil {
+	if content, err := ioutil.ReadFile(*authFile); err != nil {
 		log.Fatal(err)
 	} else {
-		g.setAuth(bytes.TrimSpace(content))
+		auth := bytes.TrimSpace(content)
+		encoded := make([]byte, base64.StdEncoding.EncodedLen(len(auth)))
+		base64.StdEncoding.Encode(encoded, auth)
+		g.BasicAuth = string(encoded)
 	}
 
 	// Do a GET first to complete any cookie dance, because POST aren't redirected properly.
-	if _, err := g.getPath("a/accounts/self"); err != nil {
+	if _, err := g.GetPath("a/accounts/self"); err != nil {
 		log.Fatalf("accounts/self: %v", err)
 	}
 
 	if *list {
-		if out, err := g.ListCheckers(); err != nil {
+		if out, err := ListCheckers(g); err != nil {
 			log.Fatalf("Lits: %v", err)
 		} else {
 			log.Println(out)
@@ -416,7 +219,7 @@
 		if *repo == "" {
 			log.Fatalf("need to set --repo")
 		}
-		ch, err := g.CreateChecker(*repo)
+		ch, err := CreateChecker(g, *repo)
 		if err != nil {
 			log.Fatalf("CreateChecker: %v", err)
 		}
@@ -434,7 +237,7 @@
 	}
 
 	gc := gerritChecker{
-		gerrit:    g,
+		server:    g,
 		fmtClient: client,
 	}
 
diff --git a/gerrit/server.go b/gerrit/server.go
new file mode 100644
index 0000000..d394337
--- /dev/null
+++ b/gerrit/server.go
@@ -0,0 +1,148 @@
+// Copyright 2019 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gerrit
+
+import (
+	"bytes"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"path"
+	"strings"
+)
+
+type Server struct {
+	UserAgent string
+	URL       url.URL
+	Client    http.Client
+
+	// Base64 encoded user:secret string.
+	BasicAuth string
+}
+
+func New(u url.URL) *Server {
+	g := &Server{
+		URL: u,
+	}
+
+	g.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+		g.setRequest(req)
+		return nil
+	}
+
+	return g
+}
+
+func (g *Server) setAuth(auth []byte) {
+}
+
+func (g *Server) setRequest(req *http.Request) {
+	req.Header.Set("User-Agent", g.UserAgent)
+	req.Header.Set("Authorization", "Basic "+string(g.BasicAuth))
+}
+
+func (g *Server) GetPath(p string) ([]byte, error) {
+	u := g.URL
+	u.Path = path.Join(u.Path, p)
+	if strings.HasSuffix(p, "/") && !strings.HasSuffix(u.Path, "/") {
+		// Ugh.
+		u.Path += "/"
+	}
+
+	req, err := http.NewRequest("GET", u.String(), nil)
+	if err != nil {
+		return nil, err
+	}
+	g.setRequest(req)
+	rep, err := g.Client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	if rep.StatusCode != 200 {
+		return nil, fmt.Errorf("Get %s: status %d", u.String(), rep.StatusCode)
+	}
+
+	defer rep.Body.Close()
+	return ioutil.ReadAll(rep.Body)
+}
+
+func (g *Server) PostPath(p string, contentType string, content []byte) ([]byte, error) {
+	u := g.URL
+	u.Path = path.Join(u.Path, p)
+	if strings.HasSuffix(p, "/") && !strings.HasSuffix(u.Path, "/") {
+		// Ugh.
+		u.Path += "/"
+	}
+	req, err := http.NewRequest("POST", u.String(), bytes.NewBuffer(content))
+	if err != nil {
+		return nil, err
+	}
+	g.setRequest(req)
+	req.Header.Set("Content-Type", contentType)
+	rep, err := g.Client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	if rep.StatusCode/100 != 2 {
+		return nil, fmt.Errorf("Post %s: status %d", u.String(), rep.StatusCode)
+	}
+
+	defer rep.Body.Close()
+	return ioutil.ReadAll(rep.Body)
+}
+
+// GetContent returns the file content from a file in a change.
+func (g *Server) GetContent(changeID string, revID string, fileID string) ([]byte, error) {
+	c, err := g.GetPath(fmt.Sprintf("changes/%s/revisions/%s/files/%s/content",
+		url.PathEscape(changeID), revID, url.PathEscape(fileID)))
+	if err != nil {
+		return nil, err
+	}
+
+	dest := make([]byte, base64.StdEncoding.DecodedLen(len(c)))
+	n, err := base64.StdEncoding.Decode(dest, c)
+	if err != nil {
+		return nil, err
+	}
+	return dest[:n], nil
+}
+
+// GetChange returns the Change (including file contents) for a given change.
+func (g *Server) GetChange(changeID string, revID string) (*Change, error) {
+	content, err := g.GetPath(fmt.Sprintf("changes/%s/revisions/%s/files/",
+		url.PathEscape(changeID), revID))
+	if err != nil {
+		return nil, err
+	}
+	content = bytes.TrimPrefix(content, jsonPrefix)
+
+	files := map[string]*File{}
+	if err := json.Unmarshal(content, &files); err != nil {
+		return nil, err
+	}
+
+	for name := range files {
+		c, err := g.GetContent(changeID, revID, name)
+		if err != nil {
+			return nil, err
+		}
+
+		files[name].Content = c
+	}
+	return &Change{files}, nil
+}
diff --git a/gerrit/types.go b/gerrit/types.go
new file mode 100644
index 0000000..57627ea
--- /dev/null
+++ b/gerrit/types.go
@@ -0,0 +1,99 @@
+// Copyright 2019 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gerrit
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"time"
+)
+
+var jsonPrefix = []byte(")]}'")
+
+type File struct {
+	Status        string
+	LinesInserted int `json:"lines_inserted"`
+	SizeDelta     int `json:"size_delta"`
+	Size          int
+	Content       []byte
+}
+
+type Change struct {
+	Files map[string]*File
+}
+
+type CheckerInput struct {
+	UUID        string   `json:"uuid"`
+	Name        string   `json:"name"`
+	Description string   `json:"description"`
+	URL         string   `json:"url"`
+	Repository  string   `json:"repository"`
+	Status      string   `json:"status"`
+	Blocking    []string `json:"blocking"`
+	Query       string   `json:"query"`
+}
+
+const timeLayout = "2006-01-02 15:04:05.000000000"
+
+type Timestamp time.Time
+
+func (ts *Timestamp) String() string {
+	return ((time.Time)(*ts)).String()
+}
+
+func (ts *Timestamp) MarshalJSON() ([]byte, error) {
+	t := (*time.Time)(ts)
+	return []byte("\"" + t.Format(timeLayout) + "\""), nil
+}
+
+func (ts *Timestamp) UnmarshalJSON(b []byte) error {
+	b = bytes.TrimPrefix(b, []byte{'"'})
+	b = bytes.TrimSuffix(b, []byte{'"'})
+	t, err := time.Parse(timeLayout, string(b))
+	if err != nil {
+		return err
+	}
+	*ts = Timestamp(t)
+	return nil
+}
+
+type CheckerInfo struct {
+	UUID        string `json:"uuid"`
+	Name        string
+	Description string
+	URL         string `json:"url"`
+	Repository  string `json:"repository"`
+	Status      string
+	Blocking    []string  `json:"blocking"`
+	Query       string    `json:"query"`
+	Created     Timestamp `json:"created"`
+	Updated     Timestamp `json:"updated"`
+}
+
+func Unmarshal(content []byte, dest interface{}) error {
+	if !bytes.HasPrefix(content, jsonPrefix) {
+		if len(content) > 100 {
+			content = content[:100]
+		}
+		bodyStr := string(content)
+
+		return fmt.Errorf("prefix %q not found, got %s", jsonPrefix, bodyStr)
+	}
+
+	content = bytes.TrimPrefix(content, []byte(jsonPrefix))
+
+	return json.Unmarshal(content, dest)
+}