gerrit: add Client.GetProjectTags method, WebLinkInfo, TagInfo

Change-Id: Iebe0796ecd65b98b75b73b1f4008fef0177fb9c8
GitHub-Last-Rev: 5807f37ca6853c0a3d7036fc8fb383b687cc29a3
GitHub-Pull-Request: golang/build#13
Reviewed-on: https://go-review.googlesource.com/c/143838
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/gerrit/gerrit.go b/gerrit/gerrit.go
index dae01a7..6204ad0 100644
--- a/gerrit/gerrit.go
+++ b/gerrit/gerrit.go
@@ -307,6 +307,17 @@
 	TZOffset int       `json:"tz"`
 }
 
+func (gpi *GitPersonInfo) Equal(v *GitPersonInfo) bool {
+	if gpi == nil {
+		if gpi != v {
+			return false
+		}
+		return true
+	}
+	return gpi.Name == v.Name && gpi.Email == v.Email && gpi.Date.Equal(v.Date) &&
+		gpi.TZOffset == v.TZOffset
+}
+
 type FileInfo struct {
 	Status        string `json:"status"`
 	Binary        bool   `json:"binary"`
@@ -637,7 +648,8 @@
 	CanDelete bool   `json:"can_delete"`
 }
 
-// GetProjectBranches returns a project's branches.
+// GetProjectBranches returns the branches for the project name. The branches are stored in a map
+// keyed by reference.
 func (c *Client) GetProjectBranches(ctx context.Context, name string) (map[string]BranchInfo, error) {
 	var res []BranchInfo
 	err := c.do(ctx, &res, "GET", fmt.Sprintf("/projects/%s/branches/", name))
@@ -651,6 +663,71 @@
 	return m, nil
 }
 
+// WebLinkInfo is information about a web link.
+// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#web-link-info
+type WebLinkInfo struct {
+	Name     string `json:"name"`
+	URL      string `json:"url"`
+	ImageURL string `json:"image_url"`
+}
+
+func (wli *WebLinkInfo) Equal(v *WebLinkInfo) bool {
+	if wli == nil || v == nil {
+		return false
+	}
+	return wli.Name == v.Name && wli.URL == v.URL && wli.ImageURL == v.ImageURL
+}
+
+// TagInfo is information about a tag.
+// See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#tag-info
+type TagInfo struct {
+	Ref       string         `json:"ref"`
+	Revision  string         `json:"revision"`
+	Object    string         `json:"object,omitempty"`
+	Message   string         `json:"message,omitempty"`
+	Tagger    *GitPersonInfo `json:"tagger,omitempty"`
+	Created   TimeStamp      `json:"created,omitempty"`
+	CanDelete bool           `json:"can_delete"`
+	WebLinks  []WebLinkInfo  `json:"web_links,omitempty"`
+}
+
+func (ti *TagInfo) Equal(v *TagInfo) bool {
+	if ti == nil || v == nil {
+		return false
+	}
+	if ti.Ref != v.Ref || ti.Revision != v.Revision || ti.Object != v.Object ||
+		ti.Message != v.Message || !ti.Created.Equal(v.Created) || ti.CanDelete != v.CanDelete {
+		return false
+	}
+	if !ti.Tagger.Equal(v.Tagger) {
+		return false
+	}
+	if len(ti.WebLinks) != len(v.WebLinks) {
+		return false
+	}
+	for i := range ti.WebLinks {
+		if !ti.WebLinks[i].Equal(&v.WebLinks[i]) {
+			return false
+		}
+	}
+	return true
+}
+
+// GetProjectTags returns the tags for the project name. The tags are stored in a map keyed by
+// reference.
+func (c *Client) GetProjectTags(ctx context.Context, name string) (map[string]TagInfo, error) {
+	var res []TagInfo
+	err := c.do(ctx, &res, "GET", fmt.Sprintf("/projects/%s/tags/", name))
+	if err != nil {
+		return nil, err
+	}
+	m := map[string]TagInfo{}
+	for _, ti := range res {
+		m[ti.Ref] = ti
+	}
+	return m, nil
+}
+
 // GetAccountInfo gets the specified account's information from Gerrit.
 // For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#get-account
 // The accountID is https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-id
@@ -713,6 +790,10 @@
 
 type TimeStamp time.Time
 
+func (ts TimeStamp) Equal(v TimeStamp) bool {
+	return ts.Time().Equal(v.Time())
+}
+
 // Gerrit's timestamp layout is like time.RFC3339Nano, but with a space instead of the "T",
 // and without a timezone (it's always in UTC).
 const timeStampLayout = "2006-01-02 15:04:05.999999999"
diff --git a/gerrit/gerrit_test.go b/gerrit/gerrit_test.go
index b26c996..2ec6016 100644
--- a/gerrit/gerrit_test.go
+++ b/gerrit/gerrit_test.go
@@ -262,3 +262,79 @@
 		t.Errorf("expected %v, got %v", expected, ts.Time())
 	}
 }
+
+// taken from https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#list-tags
+var exampleProjectTagsResponse = []byte(`  )]}'
+  [
+    {
+      "ref": "refs/tags/v1.0",
+      "revision": "49ce77fdcfd3398dc0dedbe016d1a425fd52d666",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Annotated tag",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 07:35:03.000000000",
+        "tz": 540
+      }
+    },
+    {
+      "ref": "refs/tags/v2.0",
+      "revision": "1624f5af8ae89148d1a3730df8c290413e3dcf30"
+    }
+  ]
+`)
+
+func TestGetProjectTags(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(exampleProjectTagsResponse)
+	}))
+	defer s.Close()
+	c := NewClient(s.URL, NoAuth)
+	tags, err := c.GetProjectTags(context.Background(), "go")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !hitServer {
+		t.Errorf("expected to hit test server, didn't")
+	}
+	if path != "/projects/go/tags/" {
+		t.Errorf("expected Path to be '/projects/go/tags/', got %s", path)
+	}
+	expectedTags := map[string]TagInfo{
+		"refs/tags/v1.0": TagInfo{
+			Ref:      "refs/tags/v1.0",
+			Revision: "49ce77fdcfd3398dc0dedbe016d1a425fd52d666",
+			Object:   "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+			Message:  "Annotated tag",
+			Tagger: &GitPersonInfo{
+				Name:     "David Pursehouse",
+				Email:    "david.pursehouse@sonymobile.com",
+				Date:     TimeStamp(time.Date(2014, 10, 6, 7, 35, 3, 0, time.UTC)),
+				TZOffset: 540,
+			},
+		},
+		"refs/tags/v2.0": TagInfo{
+			Ref:      "refs/tags/v2.0",
+			Revision: "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+		},
+	}
+	if len(tags) != len(expectedTags) {
+		t.Errorf("expected %d tags, got %d", len(expectedTags), len(tags))
+	}
+	for ref, tag := range tags {
+		expectedTag, found := expectedTags[ref]
+		if !found {
+			t.Errorf("unexpected tag %q", ref)
+		}
+		if !tag.Equal(&expectedTag) {
+			t.Errorf("tags don't match (expected %#v and got %#v)", expectedTag, tag)
+		}
+	}
+}