gitiles: add Refs API.
With docs, including best practice recommendation, and tests.
BUG=646067
Review-Url: https://codereview.chromium.org/2981263002
diff --git a/common/api/gitiles/gitiles.go b/common/api/gitiles/gitiles.go
index 9d9ec0b..ac6dfd9 100644
--- a/common/api/gitiles/gitiles.go
+++ b/common/api/gitiles/gitiles.go
@@ -160,8 +160,72 @@
}
}
+// Refs returns a map resolving each ref in a repo to git revision.
+//
+// refsPath limits which refs to resolve to only those matching {refsPath}/*.
+// refsPath should start with "refs" and should not include glob '*'.
+// Typically, "refs/heads" should be used.
+//
+// To fetch **all** refs in a repo, specify just "refs" but beware of two
+// caveats:
+// * refs returned include a ref for each patchset for each Gerrit change
+// associated with the repo
+// * returned map will contain special "HEAD" ref whose value in resulting map
+// will be name of the actual ref to which "HEAD" points, which is typically
+// "refs/heads/master".
+//
+// Thus, if you are looking for all tags and all branches of repo, it's
+// recommended to issue two Refs calls limited to "refs/tags" and "refs/heads"
+// instead of one call for "refs".
+//
+// Since Gerrit allows per-ref ACLs, it is possible that some refs matching
+// refPrefix would not be present in results because current user isn't granted
+// read permission on them.
+func (c *Client) Refs(ctx context.Context, repoURL, refsPath string) (map[string]string, error) {
+ repoURL, err := NormalizeRepoURL(repoURL)
+ if err != nil {
+ return nil, err
+ }
+ if refsPath != "refs" && !strings.HasPrefix(refsPath, "refs/") {
+ return nil, fmt.Errorf("refsPath must start with \"refs\": %q", refsPath)
+ }
+ refsPath = strings.TrimRight(refsPath, "/")
+
+ subPath := fmt.Sprintf("+%s?format=json", url.PathEscape(refsPath))
+ resp := refsResponse{}
+ if err := c.get(ctx, repoURL, subPath, &resp); err != nil {
+ return nil, err
+ }
+ r := make(map[string]string, len(resp))
+ for ref, v := range resp {
+ switch {
+ case v.Value == "":
+ // Weird case of what looks like hash with a target in at least Chromium
+ // repo.
+ continue
+ case ref == "HEAD":
+ r["HEAD"] = v.Target
+ case refsPath != "refs":
+ // Gitiles omits refsPath from each ref if refsPath != "refs". Undo this
+ // inconsistency.
+ r[refsPath+"/"+ref] = v.Value
+ default:
+ r[ref] = v.Value
+ }
+ }
+ return r, nil
+}
+
////////////////////////////////////////////////////////////////////////////////
+type refsResponseRefInfo struct {
+ Value string `json:"value"`
+ Target string `json:"target"`
+}
+
+// refsResponse is the JSON response from querying gitiles for a Refs request.
+type refsResponse map[string]refsResponseRefInfo
+
// logResponse is the JSON response from querying gitiles for a log request.
type logResponse struct {
Log []Commit `json:"log"`
diff --git a/common/api/gitiles/gitiles_test.go b/common/api/gitiles/gitiles_test.go
index b5e2290..83bfb7b 100644
--- a/common/api/gitiles/gitiles_test.go
+++ b/common/api/gitiles/gitiles_test.go
@@ -134,6 +134,66 @@
})
}
+func TestRefs(t *testing.T) {
+ t.Parallel()
+ ctx := context.Background()
+
+ Convey("Refs Bad RefsPath", t, func() {
+
+ c := Client{nil, "https://a.googlesource.com/a/repo"}
+ _, err := c.Refs(context.Background(), "https://c.googlesource.com/repo", "bad")
+ So(err, ShouldNotBeNil)
+ })
+
+ Convey("Refs All", t, func() {
+ srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(200)
+ w.Header().Set("Content-Type", "application/json")
+ fmt.Fprintln(w, `)]}'
+ {
+ "refs/heads/master": { "value": "deadbeef" },
+ "refs/heads/infra/config": { "value": "0000beef" },
+ "refs/changes/01/123001/1": { "value": "123dead001beef1" },
+ "refs/other/ref": { "value": "ba6" },
+ "123deadbeef123": { "target": "f00" },
+ "HEAD": { "value": "deadbeef",
+ "target": "refs/heads/master" }
+ }
+ `)
+ })
+ defer srv.Close()
+ refs, err := c.Refs(ctx, "https://c.googlesource.com/repo", "refs")
+ So(err, ShouldBeNil)
+ So(refs, ShouldResemble, map[string]string{
+ "HEAD": "refs/heads/master",
+ "refs/heads/master": "deadbeef",
+ "refs/heads/infra/config": "0000beef",
+ "refs/other/ref": "ba6",
+ "refs/changes/01/123001/1": "123dead001beef1",
+ // Skipping "123dead001beef1" which has no value.
+ })
+ })
+ Convey("Refs heads", t, func() {
+ srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(200)
+ w.Header().Set("Content-Type", "application/json")
+ fmt.Fprintln(w, `)]}'
+ {
+ "master": { "value": "deadbeef" },
+ "infra/config": { "value": "0000beef" }
+ }
+ `)
+ })
+ defer srv.Close()
+ refs, err := c.Refs(ctx, "https://c.googlesource.com/repo", "refs/heads")
+ So(err, ShouldBeNil)
+ So(refs, ShouldResemble, map[string]string{
+ "refs/heads/master": "deadbeef",
+ "refs/heads/infra/config": "0000beef",
+ })
+ })
+}
+
////////////////////////////////////////////////////////////////////////////////
var (