gerrit: support contexts
Switch all API methods that make requests to Gerrit to take a
context.Context as their first argument. Adds a package example and
a simple test that we make requests to the correct endpoint and that
the Client can handle correct responses and error responses from the
Gerrit server.
Switches all code in the x/build tree to use the new Gerrit
client. There are several projects outside the tree that import
x/build/gerrit; I'll submit CL's against those to pull in the new
interface once this gets merged.
Documents that the API is unstable.
Fixes golang/go#18742.
Fixes golang/go#18743.
Change-Id: Ifa78cbb058981e23cf5769955f6312fcbe08e174
Reviewed-on: https://go-review.googlesource.com/35559
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/cl/cl.go b/cmd/cl/cl.go
index fadb762..fefaef7 100644
--- a/cmd/cl/cl.go
+++ b/cmd/cl/cl.go
@@ -81,6 +81,7 @@
import (
"bytes"
+ "context"
"encoding/json"
"flag"
"fmt"
@@ -131,7 +132,9 @@
if strings.Contains(query, " is:") || strings.HasPrefix(query, "is:") {
open = ""
}
- cis, err := c.QueryChanges(open+" -project:scratch -message:do-not-review "+query, gerrit.QueryChangesOpt{
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ cis, err := c.QueryChanges(ctx, open+" -project:scratch -message:do-not-review "+query, gerrit.QueryChangesOpt{
N: 5000,
Fields: []string{
"LABELS",
diff --git a/cmd/coordinator/coordinator.go b/cmd/coordinator/coordinator.go
index 88a661a..1cf0c1a 100644
--- a/cmd/coordinator/coordinator.go
+++ b/cmd/coordinator/coordinator.go
@@ -15,6 +15,7 @@
"archive/tar"
"bytes"
"compress/gzip"
+ "context"
"crypto/rand"
"crypto/sha1"
"crypto/tls"
@@ -50,7 +51,6 @@
"golang.org/x/build/internal/singleflight"
"golang.org/x/build/livelog"
"golang.org/x/build/types"
- "golang.org/x/net/context"
"golang.org/x/time/rate"
)
@@ -788,7 +788,7 @@
if inStaging && true {
return nil
}
- cis, err := gerritClient.QueryChanges("label:Run-TryBot=1 label:TryBot-Result=0 status:open", gerrit.QueryChangesOpt{
+ cis, err := gerritClient.QueryChanges(context.Background(), "label:Run-TryBot=1 label:TryBot-Result=0 status:open", gerrit.QueryChangesOpt{
Fields: []string{"CURRENT_REVISION"},
})
if err != nil {
@@ -966,7 +966,8 @@
func (ts *trySet) notifyStarting() {
msg := "TryBots beginning. Status page: http://farmer.golang.org/try?commit=" + ts.Commit[:8]
- if ci, err := gerritClient.GetChangeDetail(ts.ChangeTriple()); err == nil {
+ ctx := context.Background()
+ if ci, err := gerritClient.GetChangeDetail(ctx, ts.ChangeTriple()); err == nil {
if len(ci.Messages) == 0 {
log.Printf("No Gerrit comments retrieved on %v", ts.ChangeTriple())
}
@@ -981,7 +982,7 @@
}
// Ignore error. This isn't critical.
- gerritClient.SetReview(ts.ChangeTriple(), ts.Commit, gerrit.ReviewInput{Message: msg})
+ gerritClient.SetReview(ctx, ts.ChangeTriple(), ts.Commit, gerrit.ReviewInput{Message: msg})
}
// awaitTryBuild runs in its own goroutine and waits for a build in a
@@ -1087,7 +1088,7 @@
ts.mu.Unlock()
if numFail == 1 && remain > 0 {
- if err := gerritClient.SetReview(ts.ChangeTriple(), ts.Commit, gerrit.ReviewInput{
+ if err := gerritClient.SetReview(bs.ctx, ts.ChangeTriple(), ts.Commit, gerrit.ReviewInput{
Message: fmt.Sprintf(
"Build is still in progress...\n"+
"This change failed on %s:\n"+
@@ -1110,7 +1111,7 @@
score, msg = -1, fmt.Sprintf("%d of %d TryBots failed:\n%s\nConsult https://build.golang.org/ to see whether they are new failures.",
numFail, len(ts.builds), errMsg)
}
- if err := gerritClient.SetReview(ts.ChangeTriple(), ts.Commit, gerrit.ReviewInput{
+ if err := gerritClient.SetReview(bs.ctx, ts.ChangeTriple(), ts.Commit, gerrit.ReviewInput{
Message: msg,
Labels: map[string]int{
"TryBot-Result": score,
diff --git a/cmd/coordinator/gce.go b/cmd/coordinator/gce.go
index 3cdc25e..71be652 100644
--- a/cmd/coordinator/gce.go
+++ b/cmd/coordinator/gce.go
@@ -8,6 +8,7 @@
package main
import (
+ "context"
"encoding/json"
"errors"
"fmt"
@@ -28,7 +29,6 @@
"golang.org/x/build/dashboard"
"golang.org/x/build/gerrit"
"golang.org/x/build/internal/lru"
- "golang.org/x/net/context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
compute "google.golang.org/api/compute/v1"
diff --git a/cmd/coordinator/kube.go b/cmd/coordinator/kube.go
index 230d669..b57677d 100644
--- a/cmd/coordinator/kube.go
+++ b/cmd/coordinator/kube.go
@@ -5,6 +5,7 @@
package main
import (
+ "context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
@@ -23,7 +24,6 @@
"golang.org/x/build/dashboard"
"golang.org/x/build/kubernetes"
"golang.org/x/build/kubernetes/api"
- "golang.org/x/net/context"
"golang.org/x/oauth2"
container "google.golang.org/api/container/v1"
)
diff --git a/cmd/coordinator/log.go b/cmd/coordinator/log.go
index d999e52..153dc0d 100644
--- a/cmd/coordinator/log.go
+++ b/cmd/coordinator/log.go
@@ -5,6 +5,7 @@
package main
import (
+ "context"
"fmt"
"log"
"time"
@@ -12,7 +13,6 @@
"cloud.google.com/go/datastore"
"golang.org/x/build/types"
- "golang.org/x/net/context"
)
// Process is a datastore record about the lifetime of a coordinator process.
diff --git a/cmd/coordinator/remote.go b/cmd/coordinator/remote.go
index 1cdd4d1..8313c4c 100644
--- a/cmd/coordinator/remote.go
+++ b/cmd/coordinator/remote.go
@@ -8,6 +8,7 @@
import (
"bytes"
+ "context"
"encoding/json"
"fmt"
"html"
@@ -21,7 +22,6 @@
"golang.org/x/build/buildlet"
"golang.org/x/build/dashboard"
- "golang.org/x/net/context"
)
var (
diff --git a/cmd/coordinator/reverse.go b/cmd/coordinator/reverse.go
index bd6ee6d..483cb16 100644
--- a/cmd/coordinator/reverse.go
+++ b/cmd/coordinator/reverse.go
@@ -29,6 +29,7 @@
import (
"bytes"
+ "context"
"errors"
"fmt"
"io"
@@ -44,7 +45,6 @@
"golang.org/x/build/buildlet"
"golang.org/x/build/dashboard"
"golang.org/x/build/revdial"
- "golang.org/x/net/context"
)
const minBuildletVersion = 1
diff --git a/gerrit/demo.go b/gerrit/demo.go
index 340bcde..9eec921 100644
--- a/gerrit/demo.go
+++ b/gerrit/demo.go
@@ -14,8 +14,10 @@
"os"
"path/filepath"
"strings"
+ "time"
"golang.org/x/build/gerrit"
+ "golang.org/x/net/context"
)
func main() {
@@ -25,7 +27,9 @@
}
c := gerrit.NewClient("https://go-review.googlesource.com",
gerrit.BasicAuth("git-gobot.golang.org", strings.TrimSpace(string(gobotPass))))
- cl, err := c.QueryChanges("label:Run-TryBot=1 label:TryBot-Result=0 project:go status:open", gerrit.QueryChangesOpt{
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ cl, err := c.QueryChanges(ctx, "label:Run-TryBot=1 label:TryBot-Result=0 project:go status:open", gerrit.QueryChangesOpt{
Fields: []string{"CURRENT_REVISION"},
})
if err != nil {
@@ -34,7 +38,7 @@
v, _ := json.MarshalIndent(cl, "", " ")
os.Stdout.Write(v)
- log.Printf("SetReview = %v", c.SetReview("I2383397c056a9ffe174ac7c2c6e5bb334406fbf9", "current", gerrit.ReviewInput{
+ log.Printf("SetReview = %v", c.SetReview(ctx, "I2383397c056a9ffe174ac7c2c6e5bb334406fbf9", "current", gerrit.ReviewInput{
Message: "test test",
Labels: map[string]int{
"TryBot-Result": 0,
diff --git a/gerrit/example_test.go b/gerrit/example_test.go
new file mode 100644
index 0000000..2161c47
--- /dev/null
+++ b/gerrit/example_test.go
@@ -0,0 +1,21 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package gerrit_test
+
+import (
+ "fmt"
+
+ "golang.org/x/build/gerrit"
+ "golang.org/x/net/context"
+)
+
+func Example() {
+ c := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth)
+ info, err := c.GetProjectInfo(context.TODO(), "go")
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println(info.Description)
+}
diff --git a/gerrit/gerrit.go b/gerrit/gerrit.go
index b983d90..cf3d5e7 100644
--- a/gerrit/gerrit.go
+++ b/gerrit/gerrit.go
@@ -3,6 +3,9 @@
// license that can be found in the LICENSE file.
// Package gerrit contains code to interact with Gerrit servers.
+//
+// The API is not subject to the Go 1 compatibility promise and may change at
+// any time.
package gerrit
import (
@@ -18,6 +21,8 @@
"strconv"
"strings"
"time"
+
+ "golang.org/x/net/context"
)
// Client is a Gerrit client.
@@ -81,7 +86,7 @@
func (urlValues) isDoArg() {}
-func (c *Client) do(dst interface{}, method, path string, opts ...doArg) error {
+func (c *Client) do(ctx context.Context, dst interface{}, method, path string, opts ...doArg) error {
var arg url.Values
var body interface{}
var wantStatus = http.StatusOK
@@ -127,7 +132,7 @@
req.Header.Set("Content-Type", contentType)
}
c.auth.setAuth(c, req)
- res, err := c.httpClient().Do(req)
+ res, err := c.httpClient().Do(withContext(req, ctx))
if err != nil {
return err
}
@@ -324,7 +329,7 @@
// QueryChanges queries changes. The q parameter is a Gerrit search query.
// For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
// For the query syntax, see https://gerrit-review.googlesource.com/Documentation/user-search.html#_search_operators
-func (c *Client) QueryChanges(q string, opts ...QueryChangesOpt) ([]*ChangeInfo, error) {
+func (c *Client) QueryChanges(ctx context.Context, q string, opts ...QueryChangesOpt) ([]*ChangeInfo, error) {
var opt QueryChangesOpt
switch len(opts) {
case 0:
@@ -334,7 +339,7 @@
return nil, errors.New("only 1 option struct supported")
}
var changes []*ChangeInfo
- err := c.do(&changes, "GET", "/changes/", urlValues{
+ err := c.do(ctx, &changes, "GET", "/changes/", urlValues{
"q": {q},
"n": condInt(opt.N),
"o": opt.Fields,
@@ -345,7 +350,7 @@
// GetChangeDetail retrieves a change with labels, detailed labels, detailed
// accounts, and messages.
// For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-change-detail
-func (c *Client) GetChangeDetail(changeID string, opts ...QueryChangesOpt) (*ChangeInfo, error) {
+func (c *Client) GetChangeDetail(ctx context.Context, changeID string, opts ...QueryChangesOpt) (*ChangeInfo, error) {
var opt QueryChangesOpt
switch len(opts) {
case 0:
@@ -355,7 +360,7 @@
return nil, errors.New("only 1 option struct supported")
}
var change ChangeInfo
- err := c.do(&change, "GET", "/changes/"+changeID+"/detail", urlValues{
+ err := c.do(ctx, &change, "GET", "/changes/"+changeID+"/detail", urlValues{
"o": opt.Fields,
})
if err != nil {
@@ -377,16 +382,16 @@
// For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#set-review
// The changeID is https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
// The revision is https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-id
-func (c *Client) SetReview(changeID, revision string, review ReviewInput) error {
+func (c *Client) SetReview(ctx context.Context, changeID, revision string, review ReviewInput) error {
var res reviewInfo
- return c.do(&res, "POST", fmt.Sprintf("/changes/%s/revisions/%s/review", changeID, revision),
+ return c.do(ctx, &res, "POST", fmt.Sprintf("/changes/%s/revisions/%s/review", changeID, revision),
reqBody{review})
}
// AbandonChange abandons the given change.
-func (c *Client) AbandonChange(changeID string) error {
+func (c *Client) AbandonChange(ctx context.Context, changeID string) error {
var change ChangeInfo
- return c.do(&change, "POST", "/changes/"+changeID+"/abandon")
+ return c.do(ctx, &change, "POST", "/changes/"+changeID+"/abandon")
}
// ProjectInput contains the options for creating a new project.
@@ -413,7 +418,7 @@
}
// CreateProject creates a new project.
-func (c *Client) CreateProject(name string, p ...ProjectInput) (ProjectInfo, error) {
+func (c *Client) CreateProject(ctx context.Context, name string, p ...ProjectInput) (ProjectInfo, error) {
var pi ProjectInput
if len(p) > 1 {
panic("invalid use of multiple project inputs")
@@ -422,7 +427,7 @@
pi = p[0]
}
var res ProjectInfo
- err := c.do(&res, "PUT", fmt.Sprintf("/projects/%s", name), reqBody{&pi}, wantResStatus(http.StatusCreated))
+ err := c.do(ctx, &res, "PUT", fmt.Sprintf("/projects/%s", name), reqBody{&pi}, wantResStatus(http.StatusCreated))
return res, err
}
@@ -433,9 +438,9 @@
// 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) {
+func (c *Client) GetProjectInfo(ctx context.Context, name string) (ProjectInfo, error) {
var res ProjectInfo
- err := c.do(&res, "GET", fmt.Sprintf("/projects/%s", name))
+ err := c.do(ctx, &res, "GET", fmt.Sprintf("/projects/%s", name))
if he, ok := err.(*HTTPError); ok && he.Res.StatusCode == 404 {
return res, ErrProjectNotExist
}
@@ -451,9 +456,9 @@
}
// GetProjectBranches returns a project's branches.
-func (c *Client) GetProjectBranches(name string) (map[string]BranchInfo, error) {
+func (c *Client) GetProjectBranches(ctx context.Context, name string) (map[string]BranchInfo, error) {
var res []BranchInfo
- err := c.do(&res, "GET", fmt.Sprintf("/projects/%s/branches/", name))
+ err := c.do(ctx, &res, "GET", fmt.Sprintf("/projects/%s/branches/", name))
if err != nil {
return nil, err
}
@@ -470,9 +475,9 @@
//
// Note that getting "self" is a good way to validate host access, since it only requires peeker
// access to the host, not to any particular repository.
-func (c *Client) GetAccountInfo(accountID string) (AccountInfo, error) {
+func (c *Client) GetAccountInfo(ctx context.Context, accountID string) (AccountInfo, error) {
var res AccountInfo
- err := c.do(&res, "GET", fmt.Sprintf("/accounts/%s", accountID))
+ err := c.do(ctx, &res, "GET", fmt.Sprintf("/accounts/%s", accountID))
return res, err
}
diff --git a/gerrit/gerrit_ctx.go b/gerrit/gerrit_ctx.go
new file mode 100644
index 0000000..1776fad
--- /dev/null
+++ b/gerrit/gerrit_ctx.go
@@ -0,0 +1,17 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build go1.7
+
+package gerrit
+
+import (
+ "net/http"
+
+ "golang.org/x/net/context"
+)
+
+func withContext(r *http.Request, ctx context.Context) *http.Request {
+ return r.WithContext(ctx)
+}
diff --git a/gerrit/gerrit_noctx.go b/gerrit/gerrit_noctx.go
new file mode 100644
index 0000000..529a4a1
--- /dev/null
+++ b/gerrit/gerrit_noctx.go
@@ -0,0 +1,18 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build !go1.7
+
+package gerrit
+
+import (
+ "net/http"
+
+ "golang.org/x/net/context"
+)
+
+func withContext(r *http.Request, ctx context.Context) *http.Request {
+ r.Cancel = ctx.Done()
+ return r
+}
diff --git a/gerrit/gerrit_test.go b/gerrit/gerrit_test.go
new file mode 100644
index 0000000..bfee24a
--- /dev/null
+++ b/gerrit/gerrit_test.go
@@ -0,0 +1,92 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package gerrit
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+ "time"
+
+ "golang.org/x/net/context"
+)
+
+// taken from https://go-review.googlesource.com/projects/go
+var exampleProjectResponse = []byte(`)]}'
+{
+ "id": "go",
+ "name": "go",
+ "parent": "All-Projects",
+ "description": "The Go Programming Language",
+ "state": "ACTIVE",
+ "web_links": [
+ {
+ "name": "gitiles",
+ "url": "https://go.googlesource.com/go/",
+ "target": "_blank"
+ }
+ ]
+}
+`)
+
+func TestGetProjectInfo(t *testing.T) {
+ hitServer := false
+ path := ""
+ s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ hitServer = true
+ path = r.URL.Path
+ w.Header().Set("Content-Type", "application/json; charset=UTF-8")
+ w.WriteHeader(200)
+ w.Write(exampleProjectResponse)
+ }))
+ defer s.Close()
+ c := NewClient(s.URL, NoAuth)
+ info, err := c.GetProjectInfo(context.Background(), "go")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !hitServer {
+ t.Errorf("expected to hit test server, didn't")
+ }
+ if path != "/projects/go" {
+ t.Errorf("expected Path to be '/projects/go', got %s", path)
+ }
+ if info.Name != "go" {
+ t.Errorf("expected Name to be 'go', got %s", info.Name)
+ }
+}
+
+func TestProjectNotFound(t *testing.T) {
+ s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
+ w.WriteHeader(404)
+ w.Write([]byte("Not found: unknown"))
+ }))
+ defer s.Close()
+ c := NewClient(s.URL, NoAuth)
+ _, err := c.GetProjectInfo(context.Background(), "unknown")
+ if err != ErrProjectNotExist {
+ t.Errorf("expected to get ErrProjectNotExist, got %v", err)
+ }
+}
+
+func TestContextError(t *testing.T) {
+ c := NewClient("http://localhost", NoAuth)
+ yearsAgo, _ := time.Parse("2006", "2006")
+ ctx, cancel := context.WithDeadline(context.Background(), yearsAgo)
+ defer cancel()
+ _, err := c.GetProjectInfo(ctx, "unknown")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+ uerr, ok := err.(*url.Error)
+ if !ok {
+ t.Errorf("expected url.Error, got %#v", err)
+ }
+ if uerr.Err != context.DeadlineExceeded {
+ t.Errorf("expected DeadlineExceeded error, got %v", uerr.Err)
+ }
+}
diff --git a/godash/gerrit.go b/godash/gerrit.go
index d5207ff..bb6ccac 100644
--- a/godash/gerrit.go
+++ b/godash/gerrit.go
@@ -4,10 +4,13 @@
package godash
-import "golang.org/x/build/gerrit"
+import (
+ "golang.org/x/build/gerrit"
+ "golang.org/x/net/context"
+)
-func fetchCLs(client *gerrit.Client, reviewers *Reviewers, goReleaseCycle int, q string) ([]*CL, error) {
- cis, err := client.QueryChanges("-project:scratch -message:do-not-review "+q, gerrit.QueryChangesOpt{
+func fetchCLs(ctx context.Context, client *gerrit.Client, reviewers *Reviewers, goReleaseCycle int, q string) ([]*CL, error) {
+ cis, err := client.QueryChanges(ctx, "-project:scratch -message:do-not-review "+q, gerrit.QueryChangesOpt{
N: 5000,
Fields: []string{
"LABELS",
diff --git a/godash/godash.go b/godash/godash.go
index fc2a545..e7d86ad 100644
--- a/godash/godash.go
+++ b/godash/godash.go
@@ -83,7 +83,7 @@
}
}
since := d.Now.Add(-(time.Duration(days)*24 + 12) * time.Hour).UTC().Round(time.Second)
- cls, err := fetchCLs(ger, d.Reviewers, d.GoReleaseCycle, "is:open")
+ cls, err := fetchCLs(ctx, ger, d.Reviewers, d.GoReleaseCycle, "is:open")
if err != nil {
return err
}
@@ -97,7 +97,7 @@
}
}
if includeMerged {
- cls, err := fetchCLs(ger, d.Reviewers, d.GoReleaseCycle, "is:merged since:\""+since.Format("2006-01-02 15:04:05")+"\"")
+ cls, err := fetchCLs(ctx, ger, d.Reviewers, d.GoReleaseCycle, "is:merged since:\""+since.Format("2006-01-02 15:04:05")+"\"")
if err != nil {
return err
}