Merge changes from topic 'tags-api'

* changes:
  ListTags: Support filtering by substring and regex
  ProjectApi: Refactor to reduce duplicate code in tags and branches
  ListTags: Add support for pagination with --start and --limit
  ListBranches: Split filtering and pagination out to a utility class
  Make BranchInfo and TagInfo share a common base class
  Implement tags API
  ListTags: Create RevWalk in try-with-resource
  RefNames: Add support for refs/tags/ in shortName()
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 4658e2c..7ae320d 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1453,6 +1453,80 @@
   ]
 ----
 
+[[tag-options]]
+==== Tag Options
+
+Limit(n)::
+Limit the number of tags to be included in the results.
++
+.Request
+----
+  GET /projects/work%2Fmy-project/tags?n=2 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "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"
+    }
+  ]
+----
+
+Skip(s)::
+Skip the given number of tags from the beginning of the list.
++
+.Request
+----
+  GET /projects/work%2Fmy-project/tags?n=2&s=1 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "refs/tags/v2.0",
+      "revision": "1624f5af8ae89148d1a3730df8c290413e3dcf30"
+    },
+    {
+      "ref": "refs/tags/v3.0",
+      "revision": "c628685b3c5a3614571ecb5c8fceb85db9112313",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Signed tag\n-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v1.4.11 (GNU/Linux)\n\niQEcBAABAgAGBQJUMlqYAAoJEPI2qVPgglptp7MH/j+KFcittFbxfSnZjUl8n5IZ\nveZo7wE+syjD9sUbMH4EGv0WYeVjphNTyViBof+stGTNkB0VQzLWI8+uWmOeiJ4a\nzj0LsbDOxodOEMI5tifo02L7r4Lzj++EbqtKv8IUq2kzYoQ2xjhKfFiGjeYOn008\n9PGnhNblEHZgCHguGR6GsfN8bfA2XNl9B5Ysl5ybX1kAVs/TuLZR4oDMZ/pW2S75\nhuyNnSgcgq7vl2gLGefuPs9lxkg5Fj3GZr7XPZk4pt/x1oiH7yXxV4UzrUwg2CC1\nfHrBlNbQ4uJNY8TFcwof52Z0cagM5Qb/ZSLglHbqEDGA68HPqbwf5z2hQyU2/U4\u003d\n\u003dZtUX\n-----END PGP SIGNATURE-----",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 09:02:16.000000000",
+        "tz": 540
+      }
+    }
+  ]
+----
+
+
 [[get-tag]]
 === Get Tag
 --
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index 1b79bc3..6617127 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
-import com.google.gerrit.extensions.api.projects.ProjectApi.ListBranchesRequest;
+import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 
 import org.junit.Test;
@@ -155,7 +155,7 @@
         list().withRegex(".*ast.*r").get());
   }
 
-  private ListBranchesRequest list() throws Exception {
+  private ListRefsRequest<BranchInfo> list() throws Exception {
     return gApi.projects().name(project.get()).branches();
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 7efefa7..72584b7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -16,12 +16,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.extensions.common.TagInfo;
-import com.google.gson.reflect.TypeToken;
+import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
@@ -29,14 +31,25 @@
 import java.util.List;
 
 public class TagsIT extends AbstractDaemonTest {
+  private static final List<String> testTags = ImmutableList.of(
+      "tag-A", "tag-B", "tag-C", "tag-D", "tag-E", "tag-F", "tag-G", "tag-H");
+
   @Test
-  public void listTagsOfNonExistingProject_NotFound() throws Exception {
+  public void listTagsOfNonExistingProject() throws Exception {
     assertThat(adminSession.get("/projects/non-existing/tags").getStatusCode())
         .isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void listTagsOfNonVisibleProject_NotFound() throws Exception {
+  public void listTagsOfNonExistingProjectWithApi() throws Exception {
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name("does-not-exist").tags();
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name("does-not-exist").tag("tag").get();
+  }
+
+  @Test
+  public void listTagsOfNonVisibleProject() throws Exception {
     blockRead(project, "refs/*");
     assertThat(
         userSession.get("/projects/" + project.get() + "/tags").getStatusCode())
@@ -44,6 +57,15 @@
   }
 
   @Test
+  public void listTagsOfNonVisibleProjectWithApi() throws Exception {
+    blockRead(project, "refs/*");
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name(project.get()).tags();
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name(project.get()).tag("tag").get();
+  }
+
+  @Test
   public void listTags() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
     grant(Permission.CREATE, project, "refs/tags/*");
@@ -62,8 +84,7 @@
     PushOneCommit.Result r2 = push2.to("refs/for/master%submit");
     r2.assertOkStatus();
 
-    List<TagInfo> result =
-        toTagInfoList(adminSession.get("/projects/" + project.get() + "/tags"));
+    List<TagInfo> result = getTags().get();
     assertThat(result).hasSize(2);
 
     TagInfo t = result.get(0);
@@ -78,6 +99,61 @@
     assertThat(t.tagger.email).isEqualTo(tag2.tagger.getEmailAddress());
   }
 
+  private void assertTagList(FluentIterable<String> expected, List<TagInfo> actual)
+      throws Exception {
+    assertThat(actual).hasSize(expected.size());
+    for (int i = 0; i < expected.size(); i ++) {
+      assertThat(actual.get(i).ref).isEqualTo("refs/tags/" + expected.get(i));
+    }
+  }
+
+  @Test
+  public void listTagsWithoutOptions() throws Exception {
+    createTags();
+    List<TagInfo> result = getTags().get();
+    assertTagList(FluentIterable.from(testTags), result);
+  }
+
+  @Test
+  public void listTagsWithStartOption() throws Exception {
+    createTags();
+    List<TagInfo> result = getTags().withStart(1).get();
+    assertTagList(FluentIterable.from(testTags).skip(1), result);
+  }
+
+  @Test
+  public void listTagsWithLimitOption() throws Exception {
+    createTags();
+    int limit = testTags.size() - 1;
+    List<TagInfo> result = getTags().withLimit(limit).get();
+    assertTagList(FluentIterable.from(testTags).limit(limit), result);
+  }
+
+  @Test
+  public void listTagsWithLimitAndStartOption() throws Exception {
+    createTags();
+    int limit = testTags.size() - 3;
+    List<TagInfo> result = getTags().withStart(1).withLimit(limit).get();
+    assertTagList(FluentIterable.from(testTags).skip(1).limit(limit), result);
+  }
+
+  @Test
+  public void listTagsWithRegexFilter() throws Exception {
+    createTags();
+    List<TagInfo> result = getTags().withRegex("^tag-[C|D]$").get();
+    assertTagList(
+        FluentIterable.from(ImmutableList.of("tag-C", "tag-D")), result);
+  }
+
+  @Test
+  public void listTagsWithSubstringFilter() throws Exception {
+    createTags();
+    List<TagInfo> result = getTags().withSubstring("tag-").get();
+    assertTagList(FluentIterable.from(testTags), result);
+    result = getTags().withSubstring("ag-B").get();
+    assertTagList(FluentIterable.from(ImmutableList.of("tag-B")), result);
+  }
+
   @Test
   public void listTagsOfNonVisibleBranch() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
@@ -98,8 +174,7 @@
     PushOneCommit.Result r2 = push2.to("refs/for/hidden%submit");
     r2.assertOkStatus();
 
-    List<TagInfo> result =
-        toTagInfoList(adminSession.get("/projects/" + project.get() + "/tags"));
+    List<TagInfo> result = getTags().get();
     assertThat(result).hasSize(2);
     assertThat(result.get(0).ref).isEqualTo("refs/tags/" + tag1.name);
     assertThat(result.get(0).revision).isEqualTo(r1.getCommitId().getName());
@@ -107,8 +182,7 @@
     assertThat(result.get(1).revision).isEqualTo(r2.getCommitId().getName());
 
     blockRead(project, "refs/heads/hidden");
-    result =
-        toTagInfoList(adminSession.get("/projects/" + project.get() + "/tags"));
+    result = getTags().get();
     assertThat(result).hasSize(1);
     assertThat(result.get(0).ref).isEqualTo("refs/tags/" + tag1.name);
     assertThat(result.get(0).revision).isEqualTo(r1.getCommitId().getName());
@@ -126,18 +200,29 @@
     PushOneCommit.Result r1 = push1.to("refs/for/master%submit");
     r1.assertOkStatus();
 
-    RestResponse response =
-        adminSession.get("/projects/" + project.get() + "/tags/" + tag1.name);
-    TagInfo tagInfo =
-        newGson().fromJson(response.getReader(), TagInfo.class);
+    TagInfo tagInfo = getTag(tag1.name);
     assertThat(tagInfo.ref).isEqualTo("refs/tags/" + tag1.name);
     assertThat(tagInfo.revision).isEqualTo(r1.getCommitId().getName());
   }
 
-  private static List<TagInfo> toTagInfoList(RestResponse r) throws Exception {
-    List<TagInfo> result =
-        newGson().fromJson(r.getReader(),
-            new TypeToken<List<TagInfo>>() {}.getType());
-    return result;
+  private void createTags() throws Exception {
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
+    grant(Permission.CREATE, project, "refs/tags/*");
+    grant(Permission.PUSH, project, "refs/tags/*");
+    for (String tagname : testTags) {
+      PushOneCommit.Tag tag = new PushOneCommit.Tag(tagname);
+      PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+      push.setTag(tag);
+      PushOneCommit.Result result = push.to("refs/for/master%submit");
+      result.assertOkStatus();
+    }
+  }
+
+  private ListRefsRequest<TagInfo> getTags() throws Exception {
+    return gApi.projects().name(project.get()).tags();
+  }
+
+  private TagInfo getTag(String ref) throws Exception {
+    return gApi.projects().name(project.get()).tag(ref).get();
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java
index b973806..77513a2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java
@@ -20,9 +20,7 @@
 import java.util.List;
 import java.util.Map;
 
-public class BranchInfo {
-  public String ref;
-  public String revision;
+public class BranchInfo extends RefInfo {
   public Boolean canDelete;
   public Map<String, ActionInfo> actions;
   public List<WebLinkInfo> webLinks;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 102b1ce..e3eb4be 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -28,32 +28,33 @@
   String description() throws RestApiException;
   void description(PutDescriptionInput in) throws RestApiException;
 
-  ListBranchesRequest branches();
+  ListRefsRequest<BranchInfo> branches();
+  ListRefsRequest<TagInfo> tags();
 
-  public abstract class ListBranchesRequest {
-    private int limit;
-    private int start;
-    private String substring;
-    private String regex;
+  public abstract class ListRefsRequest<T extends RefInfo> {
+    protected int limit;
+    protected int start;
+    protected String substring;
+    protected String regex;
 
-    public abstract List<BranchInfo> get() throws RestApiException;
+    public abstract List<T> get() throws RestApiException;
 
-    public ListBranchesRequest withLimit(int limit) {
+    public ListRefsRequest<T> withLimit(int limit) {
       this.limit = limit;
       return this;
     }
 
-    public ListBranchesRequest withStart(int start) {
+    public ListRefsRequest<T> withStart(int start) {
       this.start = start;
       return this;
     }
 
-    public ListBranchesRequest withSubstring(String substring) {
+    public ListRefsRequest<T> withSubstring(String substring) {
       this.substring = substring;
       return this;
     }
 
-    public ListBranchesRequest withRegex(String regex) {
+    public ListRefsRequest<T> withRegex(String regex) {
       this.regex = regex;
       return this;
     }
@@ -73,7 +74,6 @@
     public String getRegex() {
       return regex;
     }
-
   }
 
   List<ProjectInfo> children() throws RestApiException;
@@ -96,6 +96,15 @@
   BranchApi branch(String ref) throws RestApiException;
 
   /**
+   * Look up a tag by refname.
+   * <p>
+   * @param ref tag name, with or without "refs/tags/" prefix.
+   * @throws RestApiException if a problem occurred reading the project.
+   * @return API for accessing the tag.
+   */
+  TagApi tag(String ref) throws RestApiException;
+
+  /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
@@ -127,7 +136,12 @@
     }
 
     @Override
-    public ListBranchesRequest branches() {
+    public ListRefsRequest<BranchInfo> branches() {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ListRefsRequest<TagInfo> tags() {
       throw new NotImplementedException();
     }
 
@@ -150,5 +164,10 @@
     public BranchApi branch(String ref) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public TagApi tag(String ref) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java
new file mode 100644
index 0000000..1844a76
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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 com.google.gerrit.extensions.api.projects;
+
+public class RefInfo {
+  public String ref;
+  public String revision;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
new file mode 100644
index 0000000..6cc1ba4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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 com.google.gerrit.extensions.api.projects;
+
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface TagApi {
+  TagInfo get() throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements TagApi {
+    @Override
+    public TagInfo get() throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
similarity index 87%
rename from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
index 3e3d8db..b531d67 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.common;
+package com.google.gerrit.extensions.api.projects;
 
-public class TagInfo {
-  public String ref;
-  public String revision;
+import com.google.gerrit.extensions.common.GitPerson;
+
+public class TagInfo extends RefInfo {
   public String object;
   public String message;
   public GitPerson tagger;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index da66929..5d2a1fd 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -21,6 +21,8 @@
 
   public static final String REFS_HEADS = "refs/heads/";
 
+  public static final String REFS_TAGS = "refs/tags/";
+
   public static final String REFS_CHANGES = "refs/changes/";
 
   /** Note tree listing commits we refuse {@code refs/meta/reject-commits} */
@@ -62,9 +64,12 @@
   }
 
   public static final String shortName(String ref) {
-    return ref.startsWith(REFS_HEADS)
-        ? ref.substring(REFS_HEADS.length())
-        : ref;
+    if (ref.startsWith(REFS_HEADS)) {
+      return ref.substring(REFS_HEADS.length());
+    } else if (ref.startsWith(REFS_TAGS)) {
+      return ref.substring(REFS_TAGS.length());
+    }
+    return ref;
   }
 
   public static String refsUsers(Account.Id accountId) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java
index 3113d07..975e6c1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java
@@ -23,6 +23,7 @@
     bind(Projects.class).to(ProjectsImpl.class);
 
     factory(BranchApiImpl.Factory.class);
+    factory(TagApiImpl.Factory.class);
     factory(ProjectApiImpl.Factory.class);
     factory(ChildProjectApiImpl.Factory.class);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 84b219b5..dbd246c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -22,6 +22,8 @@
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.api.projects.PutDescriptionInput;
+import com.google.gerrit.extensions.api.projects.TagApi;
+import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -35,6 +37,7 @@
 import com.google.gerrit.server.project.GetDescription;
 import com.google.gerrit.server.project.ListBranches;
 import com.google.gerrit.server.project.ListChildProjects;
+import com.google.gerrit.server.project.ListTags;
 import com.google.gerrit.server.project.ProjectJson;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectsCollection;
@@ -66,7 +69,9 @@
   private final ProjectJson projectJson;
   private final String name;
   private final BranchApiImpl.Factory branchApi;
+  private final TagApiImpl.Factory tagApi;
   private final Provider<ListBranches> listBranchesProvider;
+  private final Provider<ListTags> listTagsProvider;
 
   @AssistedInject
   ProjectApiImpl(Provider<CurrentUser> user,
@@ -79,11 +84,13 @@
       ChildProjectsCollection children,
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
+      TagApiImpl.Factory tagApiFactory,
       Provider<ListBranches> listBranchesProvider,
+      Provider<ListTags> listTagsProvider,
       @Assisted ProjectResource project) {
     this(user, createProjectFactory, projectApi, projects, getDescription,
         putDescription, childApi, children, projectJson, branchApiFactory,
-        listBranchesProvider, project, null);
+        tagApiFactory, listBranchesProvider, listTagsProvider, project, null);
   }
 
   @AssistedInject
@@ -97,11 +104,13 @@
       ChildProjectsCollection children,
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
+      TagApiImpl.Factory tagApiFactory,
       Provider<ListBranches> listBranchesProvider,
+      Provider<ListTags> listTagsProvider,
       @Assisted String name) {
     this(user, createProjectFactory, projectApi, projects, getDescription,
         putDescription, childApi, children, projectJson, branchApiFactory,
-        listBranchesProvider, null, name);
+        tagApiFactory, listBranchesProvider, listTagsProvider, null, name);
   }
 
   private ProjectApiImpl(Provider<CurrentUser> user,
@@ -114,7 +123,9 @@
       ChildProjectsCollection children,
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
+      TagApiImpl.Factory tagApiFactory,
       Provider<ListBranches> listBranchesProvider,
+      Provider<ListTags> listTagsProvider,
       ProjectResource project,
       String name) {
     this.user = user;
@@ -129,7 +140,9 @@
     this.project = project;
     this.name = name;
     this.branchApi = branchApiFactory;
+    this.tagApi = tagApiFactory;
     this.listBranchesProvider = listBranchesProvider;
+    this.listTagsProvider = listTagsProvider;
   }
 
   @Override
@@ -179,8 +192,8 @@
   }
 
   @Override
-  public ListBranchesRequest branches() {
-    return new ListBranchesRequest() {
+  public ListRefsRequest<BranchInfo> branches() {
+    return new ListRefsRequest<BranchInfo>() {
       @Override
       public List<BranchInfo> get() throws RestApiException {
         return listBranches(this);
@@ -188,7 +201,7 @@
     };
   }
 
-  private List<BranchInfo> listBranches(ListBranchesRequest request)
+  private List<BranchInfo> listBranches(ListRefsRequest<BranchInfo> request)
       throws RestApiException {
     ListBranches list = listBranchesProvider.get();
     list.setLimit(request.getLimit());
@@ -203,6 +216,30 @@
   }
 
   @Override
+  public ListRefsRequest<TagInfo> tags() {
+    return new ListRefsRequest<TagInfo>() {
+      @Override
+      public List<TagInfo> get() throws RestApiException {
+        return listTags(this);
+      }
+    };
+  }
+
+  private List<TagInfo> listTags(ListRefsRequest<TagInfo> request)
+      throws RestApiException {
+    ListTags list = listTagsProvider.get();
+    list.setLimit(request.getLimit());
+    list.setStart(request.getStart());
+    list.setMatchSubstring(request.getSubstring());
+    list.setMatchRegex(request.getRegex());
+    try {
+      return list.apply(checkExists());
+    } catch (IOException e) {
+      throw new RestApiException("Cannot list tags", e);
+    }
+  }
+
+  @Override
   public List<ProjectInfo> children() throws RestApiException {
     return children(false);
   }
@@ -229,6 +266,11 @@
     return branchApi.create(checkExists(), ref);
   }
 
+  @Override
+  public TagApi tag(String ref) throws ResourceNotFoundException {
+    return tagApi.create(checkExists(), ref);
+  }
+
   private ProjectResource checkExists() throws ResourceNotFoundException {
     if (project == null) {
       throw new ResourceNotFoundException(name);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java
new file mode 100644
index 0000000..086447d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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 com.google.gerrit.server.api.projects;
+
+import com.google.gerrit.extensions.api.projects.TagApi;
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.project.ListTags;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.io.IOException;
+
+public class TagApiImpl implements TagApi {
+  interface Factory {
+    TagApiImpl create(ProjectResource project, String ref);
+  }
+
+  private final ListTags listTags;
+  private final String ref;
+  private final ProjectResource project;
+
+  @Inject
+  TagApiImpl(ListTags listTags,
+      @Assisted ProjectResource project,
+      @Assisted String ref) {
+    this.listTags = listTags;
+    this.project = project;
+    this.ref = ref;
+  }
+
+  @Override
+  public TagInfo get() throws RestApiException {
+    try {
+      return listTags.get(project, IdString.fromDecoded(ref));
+    } catch (IOException e) {
+      throw new RestApiException(e.getMessage());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
index 5b78e08..a94d17e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.extensions.common.TagInfo;
+import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.inject.Singleton;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index f7914e9..370ced2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.base.Predicate;
-import com.google.common.base.Strings;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Sets;
@@ -35,9 +33,6 @@
 import com.google.inject.Inject;
 import com.google.inject.util.Providers;
 
-import dk.brics.automaton.RegExp;
-import dk.brics.automaton.RunAutomaton;
-
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
@@ -50,7 +45,6 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Locale;
 import java.util.Set;
 import java.util.TreeMap;
 
@@ -96,18 +90,15 @@
   @Override
   public List<BranchInfo> apply(ProjectResource rsrc)
       throws ResourceNotFoundException, IOException, BadRequestException {
-    FluentIterable<BranchInfo> branches = allBranches(rsrc);
-    branches = filterBranches(branches);
-    if (start > 0) {
-      branches = branches.skip(start);
-    }
-    if (limit > 0) {
-      branches = branches.limit(limit);
-    }
-    return branches.toList();
+    return new RefFilter<BranchInfo>(Constants.R_HEADS)
+        .subString(matchSubstring)
+        .regex(matchRegex)
+        .start(start)
+        .limit(limit)
+        .filter(allBranches(rsrc));
   }
 
-  private FluentIterable<BranchInfo> allBranches(ProjectResource rsrc)
+  private List<BranchInfo> allBranches(ProjectResource rsrc)
       throws IOException, ResourceNotFoundException {
     List<Ref> refs;
     try (Repository db = repoManager.openRepository(rsrc.getNameKey())) {
@@ -162,7 +153,7 @@
       }
     }
     Collections.sort(branches, new BranchComparator());
-    return FluentIterable.from(branches);
+    return branches;
   }
 
   private static class BranchComparator implements Comparator<BranchInfo> {
@@ -184,61 +175,6 @@
     }
   }
 
-  private FluentIterable<BranchInfo> filterBranches(
-      FluentIterable<BranchInfo> branches) throws BadRequestException {
-    if (!Strings.isNullOrEmpty(matchSubstring)) {
-      branches = branches.filter(new SubstringPredicate(matchSubstring));
-    } else if (!Strings.isNullOrEmpty(matchRegex)) {
-      branches = branches.filter(new RegexPredicate(matchRegex));
-    }
-    return branches;
-  }
-
-  private static class SubstringPredicate implements Predicate<BranchInfo> {
-    private final String substring;
-
-    private SubstringPredicate(String substring) {
-      this.substring = substring.toLowerCase(Locale.US);
-    }
-
-    @Override
-    public boolean apply(BranchInfo in) {
-      String ref = in.ref;
-      if (ref.startsWith(Constants.R_HEADS)) {
-        ref = ref.substring(Constants.R_HEADS.length());
-      }
-      ref = ref.toLowerCase(Locale.US);
-      return ref.contains(substring);
-    }
-  }
-
-  private static class RegexPredicate implements Predicate<BranchInfo> {
-    private final RunAutomaton a;
-
-    private RegexPredicate(String regex) throws BadRequestException {
-      if (regex.startsWith("^")) {
-        regex = regex.substring(1);
-        if (regex.endsWith("$") && !regex.endsWith("\\$")) {
-          regex = regex.substring(0, regex.length() - 1);
-        }
-      }
-      try {
-        a = new RunAutomaton(new RegExp(regex).toAutomaton());
-      } catch (IllegalArgumentException e) {
-        throw new BadRequestException(e.getMessage());
-      }
-    }
-
-    @Override
-    public boolean apply(BranchInfo in) {
-      if (!in.ref.startsWith(Constants.R_HEADS)){
-        return a.run(in.ref);
-      } else {
-        return a.run(in.ref.substring(Constants.R_HEADS.length()));
-      }
-    }
-  }
-
   private BranchInfo createBranchInfo(Ref ref, RefControl refControl,
       Set<String> targets) {
     BranchInfo info = new BranchInfo();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
index 1c140e3..83759d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
@@ -16,7 +16,8 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
-import com.google.gerrit.extensions.common.TagInfo;
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -39,6 +39,7 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
 import java.util.Collections;
@@ -46,13 +47,37 @@
 import java.util.List;
 import java.util.Map;
 
-@Singleton
 public class ListTags implements RestReadView<ProjectResource> {
   private final GitRepositoryManager repoManager;
   private final Provider<ReviewDb> dbProvider;
   private final TagCache tagCache;
   private final ChangeCache changeCache;
 
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of tags to list")
+  public void setLimit(int limit) {
+    this.limit = limit;
+  }
+
+  @Option(name = "--start", aliases = {"-s"}, metaVar = "CNT", usage = "number of tags to skip")
+  public void setStart(int start) {
+    this.start = start;
+  }
+
+  @Option(name = "--match", aliases = {"-m"}, metaVar = "MATCH", usage = "match tags substring")
+  public void setMatchSubstring(String matchSubstring) {
+    this.matchSubstring = matchSubstring;
+  }
+
+  @Option(name = "--regex", aliases = {"-r"}, metaVar = "REGEX", usage = "match tags regex")
+  public void setMatchRegex(String matchRegex) {
+    this.matchRegex = matchRegex;
+  }
+
+  private int limit;
+  private int start;
+  private String matchSubstring;
+  private String matchRegex;
+
   @Inject
   public ListTags(GitRepositoryManager repoManager,
       Provider<ReviewDb> dbProvider,
@@ -66,19 +91,15 @@
 
   @Override
   public List<TagInfo> apply(ProjectResource resource) throws IOException,
-      ResourceNotFoundException {
+      ResourceNotFoundException, BadRequestException {
     List<TagInfo> tags = Lists.newArrayList();
 
-    try (Repository repo = getRepository(resource.getNameKey())) {
-      RevWalk rw = new RevWalk(repo);
-      try {
-        Map<String, Ref> all = visibleTags(resource.getControl(), repo,
-            repo.getRefDatabase().getRefs(Constants.R_TAGS));
-        for (Ref ref : all.values()) {
-          tags.add(createTagInfo(ref, rw));
-        }
-      } finally {
-        rw.dispose();
+    try (Repository repo = getRepository(resource.getNameKey());
+        RevWalk rw = new RevWalk(repo)) {
+      Map<String, Ref> all = visibleTags(resource.getControl(), repo,
+          repo.getRefDatabase().getRefs(Constants.R_TAGS));
+      for (Ref ref : all.values()) {
+        tags.add(createTagInfo(ref, rw));
       }
     }
 
@@ -89,7 +110,12 @@
       }
     });
 
-    return tags;
+    return new RefFilter<TagInfo>(Constants.R_TAGS)
+        .start(start)
+        .limit(limit)
+        .subString(matchSubstring)
+        .regex(matchRegex)
+        .filter(tags);
   }
 
   public TagInfo get(ProjectResource resource, IdString id)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefFilter.java
new file mode 100644
index 0000000..63fb595
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefFilter.java
@@ -0,0 +1,120 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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 com.google.gerrit.server.project;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+import com.google.common.collect.FluentIterable;
+import com.google.gerrit.extensions.api.projects.RefInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+import java.util.List;
+import java.util.Locale;
+
+public class RefFilter<T extends RefInfo> {
+  private final String prefix;
+  private String matchSubstring;
+  private String matchRegex;
+  private int start;
+  private int limit;
+
+  public RefFilter(String prefix) {
+    this.prefix = prefix;
+  }
+
+  public RefFilter<T> subString(String subString) {
+    this.matchSubstring = subString;
+    return this;
+  }
+
+  public RefFilter<T> regex(String regex) {
+    this.matchRegex = regex;
+    return this;
+  }
+
+  public RefFilter<T> start(int start) {
+    this.start = start;
+    return this;
+  }
+
+  public RefFilter<T> limit(int limit) {
+    this.limit = limit;
+    return this;
+  }
+
+  public List<T> filter(List<T> refs) throws BadRequestException {
+    FluentIterable<T> results = FluentIterable.from(refs);
+    if (!Strings.isNullOrEmpty(matchSubstring)) {
+      results = results.filter(new SubstringPredicate(matchSubstring));
+    } else if (!Strings.isNullOrEmpty(matchRegex)) {
+      results = results.filter(new RegexPredicate(matchRegex));
+    }
+    if (start > 0) {
+      results = results.skip(start);
+    }
+    if (limit > 0) {
+      results = results.limit(limit);
+    }
+    return results.toList();
+  }
+
+  private class SubstringPredicate implements Predicate<T> {
+    private final String substring;
+
+    private SubstringPredicate(String substring) {
+      this.substring = substring.toLowerCase(Locale.US);
+    }
+
+    @Override
+    public boolean apply(T in) {
+      String ref = in.ref;
+      if (ref.startsWith(prefix)) {
+        ref = ref.substring(prefix.length());
+      }
+      ref = ref.toLowerCase(Locale.US);
+      return ref.contains(substring);
+    }
+  }
+
+  private class RegexPredicate implements Predicate<T> {
+    private final RunAutomaton a;
+
+    private RegexPredicate(String regex) throws BadRequestException {
+      if (regex.startsWith("^")) {
+        regex = regex.substring(1);
+        if (regex.endsWith("$") && !regex.endsWith("\\$")) {
+          regex = regex.substring(0, regex.length() - 1);
+        }
+      }
+      try {
+        a = new RunAutomaton(new RegExp(regex).toAutomaton());
+      } catch (IllegalArgumentException e) {
+        throw new BadRequestException(e.getMessage());
+      }
+    }
+
+    @Override
+    public boolean apply(T in) {
+      String ref = in.ref;
+      if (ref.startsWith(prefix)) {
+        ref = ref.substring(prefix.length());
+      }
+      return a.run(ref);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
index 12be5d3..afbd3be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.extensions.common.TagInfo;
+import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.inject.TypeLiteral;