[tools][whereiscl] first change for showing whether a CL passed GI
This CL only retrieves the the CL's commit revision and the GI's current
revision. A follow-up CL will do the actual comparison of whether the
former is before or after the latter.
Bug: DX-1516 #comment
Change-Id: Id26d7eefb9455e0d7e334e3cc6d8fa489b1b0e3e
diff --git a/tools/whereiscl/OWNERS b/tools/whereiscl/OWNERS
new file mode 100644
index 0000000..210bc29
--- /dev/null
+++ b/tools/whereiscl/OWNERS
@@ -0,0 +1,2 @@
+jinwoo@google.com
+pylaligand@google.com
diff --git a/tools/whereiscl/changeinfo.go b/tools/whereiscl/changeinfo.go
new file mode 100644
index 0000000..9a93099
--- /dev/null
+++ b/tools/whereiscl/changeinfo.go
@@ -0,0 +1,79 @@
+// Copyright 2019 The Fuchsia 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 main
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+)
+
+type clStatus string
+
+const (
+ clStatusNew clStatus = "NEW"
+ clStatusMerged clStatus = "MERGED"
+ clStatusAbandoned clStatus = "ABANDONED"
+)
+
+// changeInfo is a JSON struct for ChangeInfo responses from Gerrit.
+// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info.
+// Only fields of interest are listed here.
+type changeInfo struct {
+ Project string `json:"project"`
+ Status clStatus `json:"status"`
+ CurrentRevision string `json:"current_revision"`
+}
+
+type queryInfo struct{ apiEndpoint, cl string }
+
+func makeQueryURL(qi queryInfo) (*url.URL, error) {
+ u, err := url.Parse(qi.apiEndpoint)
+ if err != nil {
+ return nil, err
+ }
+ u.Path = "/changes/"
+ q := u.Query()
+ q.Set("q", qi.cl)
+ q.Add("o", "CURRENT_REVISION")
+ u.RawQuery = q.Encode()
+ return u, nil
+}
+
+func getChangeInfo(qi queryInfo) (*changeInfo, error) {
+ q, err := makeQueryURL(qi)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := http.Get(q.String())
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ var cis []changeInfo
+ // Responses start with )]}' to prevent XSSI attacks. Discard them.
+ // See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output
+ if err := json.Unmarshal(b[4:], &cis); err != nil {
+ return nil, err
+ }
+
+ switch len(cis) {
+ case 0:
+ return nil, errors.New("CL not found")
+ case 1:
+ return &cis[0], nil
+ default:
+ return nil, fmt.Errorf("Got %d CLs while expecting only one", len(cis))
+ }
+}
diff --git a/tools/whereiscl/changeinfo_test.go b/tools/whereiscl/changeinfo_test.go
new file mode 100644
index 0000000..d2377a4
--- /dev/null
+++ b/tools/whereiscl/changeinfo_test.go
@@ -0,0 +1,82 @@
+// Copyright 2019 The Fuchsia 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 main
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestGetChangeInfo(t *testing.T) {
+ cl := "987654321"
+ http.DefaultClient.Transport = &mockTransport{
+ cl: cl,
+ body: `)]}'
+[
+ {
+ "foo": 42,
+ "status": "MERGED",
+ "current_revision": "abcdefg"
+ }
+]`,
+ }
+ got, err := getChangeInfo(queryInfo{
+ apiEndpoint: "https://fuchsia-review.googlesource.com",
+ cl: cl,
+ })
+ if err != nil {
+ t.Fatalf("getChangeInfo: %v", err)
+ }
+ want := &changeInfo{
+ Status: clStatusMerged,
+ CurrentRevision: "abcdefg",
+ }
+ if d := cmp.Diff(want, got); d != "" {
+ t.Errorf("getChangeInfo: mismatch (-want +got):\n%s", d)
+ }
+}
+
+func TestGetChangeInfo_clNotFound(t *testing.T) {
+ cl := "987654321"
+ http.DefaultClient.Transport = &mockTransport{
+ cl: cl,
+ body: `)]}'
+[]`,
+ }
+ _, err := getChangeInfo(queryInfo{
+ apiEndpoint: "https://fuchsia-review.googlesource.com",
+ cl: cl,
+ })
+ if err == nil {
+ t.Error("getChangeInfo: error expected; got nil")
+ }
+}
+
+func TestGetChangeInfo_tooManyCLs(t *testing.T) {
+ cl := "987654321"
+ http.DefaultClient.Transport = &mockTransport{
+ cl: cl,
+ body: `)]}'
+[
+ {
+ "status": "ACTIVE",
+ "current_revision": "abcdefg"
+ },
+ {
+ "status": "MERGED",
+ "current_revision": "hijklmn"
+ }
+]`,
+ }
+ _, err := getChangeInfo(queryInfo{
+ apiEndpoint: "https://fuchsia-review.googlesource.com",
+ cl: cl,
+ })
+ if err == nil {
+ t.Error("getChangeInfo: error expected; got nil")
+ }
+}
diff --git a/tools/whereiscl/gistatus.go b/tools/whereiscl/gistatus.go
new file mode 100644
index 0000000..a58578b
--- /dev/null
+++ b/tools/whereiscl/gistatus.go
@@ -0,0 +1,102 @@
+// Copyright 2019 The Fuchsia 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 main
+
+import (
+ "encoding/base64"
+ "encoding/xml"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+)
+
+const fuchsiaURL = "https://fuchsia.googlesource.com"
+
+type giStatus string
+
+const (
+ giStatusUnknown giStatus = "UNKNOWN"
+ giStatusPassed = "PASSED"
+ giStatusPending = "PENDING"
+)
+
+func downloadGIManifest(name string) ([]byte, error) {
+ u, err := url.Parse(fuchsiaURL)
+ if err != nil {
+ return nil, err
+ }
+ u.Path = "/integration/+/refs/heads/master/" + name
+ q := u.Query()
+ q.Add("format", "TEXT")
+ u.RawQuery = q.Encode()
+
+ resp, err := http.Get(u.String())
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ return base64.StdEncoding.DecodeString(string(b))
+}
+
+// Structs for unmarshalling the jiri manifest XML data.
+type manifest struct {
+ XMLName xml.Name `xml:"manifest"`
+ Projects []project `xml:"projects>project"`
+}
+type project struct {
+ XMLName xml.Name `xml:"project"`
+ Name string `xml:"name,attr"`
+ Revision string `xml:"revision,attr"`
+}
+
+func getGIRevision(content []byte, project string) (string, error) {
+ m := manifest{}
+ if err := xml.Unmarshal(content, &m); err != nil {
+ return "", err
+ }
+
+ for _, p := range m.Projects {
+ if p.Name == project {
+ return p.Revision, nil
+ }
+ }
+
+ return "", fmt.Errorf("project %q is not found in the jiri manifest", project)
+}
+
+func getGIStatus(ci *changeInfo) (giStatus, error) {
+ var name string
+
+ switch ci.Project {
+ case "fuchsia":
+ name = "stem"
+ case "topaz":
+ name = "topaz/minimal"
+ case "experiences":
+ name = "flower"
+ default:
+ return giStatusUnknown, nil
+ }
+
+ manifest, err := downloadGIManifest(name)
+ if err != nil {
+ return giStatusUnknown, err
+ }
+
+ _, err = getGIRevision(manifest, ci.Project)
+ if err != nil {
+ return giStatusUnknown, err
+ }
+
+ // TODO: Finish implementation.
+ return giStatusPassed, nil
+}
diff --git a/tools/whereiscl/gistatus_test.go b/tools/whereiscl/gistatus_test.go
new file mode 100644
index 0000000..a01f482
--- /dev/null
+++ b/tools/whereiscl/gistatus_test.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+ "encoding/base64"
+ "net/http"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestGetGIStatus(t *testing.T) {
+ body := base64.StdEncoding.EncodeToString([]byte(`
+<manifest>
+ <projects>
+ <project name="fuchsia"
+ revision="gi_revision"/>
+ </projects>
+</manifest>
+`))
+ http.DefaultClient.Transport = &mockTransport{body: body}
+
+ ci := changeInfo{Project: "fuchsia"}
+ got, err := getGIStatus(&ci)
+ if err != nil {
+ t.Fatalf("getGIStatus: %v", err)
+ }
+ // TODO: Update this after finishing implementation of giStatus().
+ var want giStatus = "PASSED"
+ if d := cmp.Diff(want, got); d != "" {
+ t.Errorf("getGIStatus: mismatch (-want +got):\n%s", d)
+ }
+}
diff --git a/tools/whereiscl/whereiscl.go b/tools/whereiscl/whereiscl.go
index ec92b6d..ab8cd58 100644
--- a/tools/whereiscl/whereiscl.go
+++ b/tools/whereiscl/whereiscl.go
@@ -5,17 +5,14 @@
package main
import (
- "encoding/json"
"errors"
"fmt"
- "io/ioutil"
"log"
- "net/http"
"os"
"regexp"
)
-const fuchsiaURL = "https://fuchsia-review.googlesource.com"
+const fuchsiaReviewURL = "https://fuchsia-review.googlesource.com"
// fuchsiaRE is a regexp for matching CL review URLs and extracting the CL numbers.
// Supports various forms. E.g.,
@@ -25,13 +22,11 @@
// - fxr/123456789/some/file
var fuchsiaRE = regexp.MustCompile(`^(?:https?://)?(?:fxr|fuchsia-review.googlesource.com/c/.+/\+)/(\d+).*`)
-type queryInfo struct{ apiEndpoint, cl string }
-
func parseReviewURL(str string) (queryInfo, error) {
match := fuchsiaRE.FindStringSubmatch(str)
if match != nil {
return queryInfo{
- apiEndpoint: fuchsiaURL,
+ apiEndpoint: fuchsiaReviewURL,
cl: match[1],
}, nil
}
@@ -39,43 +34,6 @@
return queryInfo{}, errors.New("not a valid review URL")
}
-// changeInfo is a JSON struct for ChangeInfo responses from Gerrit.
-// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info.
-// Only fields of interest are listed here.
-type changeInfo struct {
- Status string `json:"status"`
-}
-
-func getCLStatus(qi queryInfo) (string, error) {
- query := fmt.Sprintf("%s/changes/?q=%s", qi.apiEndpoint, qi.cl)
- resp, err := http.Get(query)
- if err != nil {
- return "", err
- }
- defer resp.Body.Close()
-
- b, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return "", err
- }
-
- var cis []changeInfo
- // Responses start with )]}' to prevent XSSI attacks. Discard them.
- // See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output
- if err := json.Unmarshal(b[4:], &cis); err != nil {
- return "", err
- }
-
- switch len(cis) {
- case 0:
- return "", errors.New("CL not found")
- case 1:
- return cis[0].Status, nil
- default:
- return "", fmt.Errorf("Got %d CLs while expecting only one", len(cis))
- }
-}
-
func main() {
if len(os.Args) < 2 {
// TODO: Consider alternatives. E.g., show all outstanding CLs
@@ -89,9 +47,19 @@
log.Fatalf("Error parsing the review URL: %v", err)
}
- status, err := getCLStatus(queryInfo)
+ ci, err := getChangeInfo(queryInfo)
if err != nil {
log.Fatalf("Error getting change info: %v", err)
}
- fmt.Printf("CL status: %v\n", status)
+ fmt.Printf("CL status: %v\n", ci.Status)
+
+ if ci.Status != clStatusMerged {
+ return
+ }
+
+ gs, err := getGIStatus(ci)
+ if err != nil {
+ log.Fatalf("Error getting GI status: %v", err)
+ }
+ fmt.Printf("GI status: %v\n", gs)
}
diff --git a/tools/whereiscl/whereiscl_test.go b/tools/whereiscl/whereiscl_test.go
index 09d14aa..d0846cd0 100644
--- a/tools/whereiscl/whereiscl_test.go
+++ b/tools/whereiscl/whereiscl_test.go
@@ -55,71 +55,7 @@
}
}
-func TestGetCLStatus(t *testing.T) {
- cl := "987654321"
- http.DefaultClient.Transport = &mockTransport{
- cl: cl,
- body: `)]}'
-[
- {
- "foo": 42,
- "status": "MERGED"
- }
-]`,
- }
- got, err := getCLStatus(queryInfo{
- apiEndpoint: "https://fuchsia-review.googlesource.com",
- cl: cl,
- })
- if err != nil {
- t.Fatalf("getCLStatus: %v", err)
- }
- want := "MERGED"
- if d := cmp.Diff(want, got); d != "" {
- t.Errorf("getCLStatus: mismatch (-want +got):\n%s", d)
- }
-}
-
-func TestGetCLStatus_clNotFound(t *testing.T) {
- cl := "987654321"
- http.DefaultClient.Transport = &mockTransport{
- cl: cl,
- body: `)]}'
-[]`,
- }
- _, err := getCLStatus(queryInfo{
- apiEndpoint: "https://fuchsia-review.googlesource.com",
- cl: cl,
- })
- if err == nil {
- t.Error("getCLStatus: error expected; got nil")
- }
-}
-
-func TestGetCLStatus_tooManyCLs(t *testing.T) {
- cl := "987654321"
- http.DefaultClient.Transport = &mockTransport{
- cl: cl,
- body: `)]}'
-[
- {
- "status": "ACTIVE"
- },
- {
- "status": "MERGED"
- }
-]`,
- }
- _, err := getCLStatus(queryInfo{
- apiEndpoint: "https://fuchsia-review.googlesource.com",
- cl: cl,
- })
- if err == nil {
- t.Error("getCLStatus: error expected; got nil")
- }
-}
-
-// Fake http transport.
+// Fake http transport. This is used from other test files.
type mockTransport struct{ cl, body string }
func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
@@ -132,7 +68,12 @@
log.Fatalf("RoundTrip: invalid CL: %s", cl)
}
default:
- log.Fatalf("RoundTrip: invalid path: %s", req.URL.Path)
+ log.Fatalf("RoundTrip: invalid changes path: %s", req.URL.Path)
+ }
+ case "fuchsia.googlesource.com":
+ manifestPrefix := "/integration/+/refs/heads/master/"
+ if !strings.HasPrefix(req.URL.Path, manifestPrefix) {
+ log.Fatalf("RoundTrip: invalid manifest path: %s", req.URL.Path)
}
default:
log.Fatalf("RoundTrip: invalid hostname: %s", req.URL.Hostname())