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)
+}