Merge changes from topics 'tags-ui', 'projects-ui-refactoring'

* changes:
  Add new screen to list tags of a project
  Projects UI: Factor common code out to PaginatedProjectScreen
  Projects UI: Move common parts out of BranchInfo into RefInfo
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index ff2121d..bc445a0 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -89,6 +89,10 @@
     return "/admin/projects/" + p.get() + ",branches";
   }
 
+  public static String toProjectTags(Project.NameKey p) {
+    return "/admin/projects/" + p.get() + ",tags";
+  }
+
   public static String toAccountQuery(String fullname, Status status) {
     return toChangeQuery(op("owner", fullname) + " " + status(status));
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index 17d073d..f78230b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -66,6 +66,7 @@
 import com.google.gerrit.client.admin.ProjectInfoScreen;
 import com.google.gerrit.client.admin.ProjectListScreen;
 import com.google.gerrit.client.admin.ProjectScreen;
+import com.google.gerrit.client.admin.ProjectTagsScreen;
 import com.google.gerrit.client.api.ExtensionScreen;
 import com.google.gerrit.client.api.ExtensionSettingsScreen;
 import com.google.gerrit.client.change.ChangeScreen;
@@ -738,6 +739,11 @@
             return new ProjectBranchesScreen(k);
           }
 
+          if (ProjectScreen.TAGS.equals(panel)
+              || matchPrefix(ProjectScreen.TAGS, panel)) {
+            return new ProjectTagsScreen(k);
+          }
+
           if (ProjectScreen.ACCESS.equals(panel)) {
             return new ProjectAccessScreen(k);
           }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index d99b792..a22c972 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -708,6 +708,7 @@
     addLink(projectsBar, C.menuProjectsList(), PageLinks.ADMIN_PROJECTS);
     projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsInfo(), ProjectScreen.INFO));
     projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsBranches(), ProjectScreen.BRANCHES));
+    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsTags(), ProjectScreen.TAGS));
     projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsAccess(), ProjectScreen.ACCESS));
     final LinkMenuItem dashboardsMenuItem =
         new ProjectLinkMenuItem(C.menuProjectsDashboards(),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index 6bbc8f1..269999c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -77,6 +77,7 @@
   String menuProjectsList();
   String menuProjectsInfo();
   String menuProjectsBranches();
+  String menuProjectsTags();
   String menuProjectsAccess();
   String menuProjectsDashboards();
   String menuProjectsCreate();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index 05de983..fb74506 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -60,6 +60,7 @@
 menuProjectsList = List
 menuProjectsInfo = General
 menuProjectsBranches = Branches
+menuProjectsTags = Tags
 menuProjectsAccess = Access
 menuProjectsDashboards = Dashboards
 menuProjectsCreate = Create New Project
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index 66a64b4..f0928040 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -104,6 +104,7 @@
   String buttonDeleteBranch();
   String saveHeadButton();
   String cancelHeadButton();
+  String columnTagName();
 
   String groupItemHelp();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 952ea5f..da260de 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -83,6 +83,7 @@
 buttonDeleteBranch = Delete
 saveHeadButton = Save
 cancelHeadButton = Cancel
+columnTagName = Tag Name
 
 groupItemHelp = group
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
new file mode 100644
index 0000000..6349803
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
@@ -0,0 +1,76 @@
+// 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.client.admin;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.http.client.URL;
+
+abstract class PaginatedProjectScreen extends ProjectScreen {
+  protected int pageSize;
+  protected String match;
+  protected int start;
+
+  PaginatedProjectScreen(Project.NameKey toShow) {
+    super(toShow);
+    pageSize = Gerrit.getUserPreferences().changesPerPage();
+  }
+
+  protected void parseToken(String token) {
+    for (String kvPair : token.split("[,;&/?]")) {
+      String[] kv = kvPair.split("=", 2);
+      if (kv.length != 2 || kv[0].isEmpty()) {
+        continue;
+      }
+
+      if ("filter".equals(kv[0])) {
+        match = URL.decodeQueryString(kv[1]);
+      }
+
+      if ("skip".equals(kv[0])
+          && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
+        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
+      }
+    }
+  }
+
+  protected void parseToken() {
+    parseToken(getToken());
+  }
+
+  protected String getTokenForScreen(String filter, int skip) {
+    String token = getScreenToken();
+    if (filter != null && !filter.isEmpty()) {
+      token += "?filter=" + URL.encodeQueryString(filter);
+    }
+    if (skip > 0) {
+      if (token.contains("?filter=")) {
+        token += ",";
+      } else {
+        token += "?";
+      }
+      token += "skip=" + skip;
+    }
+    return token;
+  }
+
+  protected abstract String getScreenToken();
+
+  protected void setupNavigationLink(Hyperlink link, String filter, int skip) {
+    link.setTargetHistoryToken(getTokenForScreen(filter, skip));
+    link.setVisible(true);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index 8ecf650..c6d4f42 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -53,7 +53,6 @@
 import com.google.gwt.event.dom.client.KeyUpHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.http.client.URL;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
@@ -74,7 +73,7 @@
 import java.util.List;
 import java.util.Set;
 
-public class ProjectBranchesScreen extends ProjectScreen {
+public class ProjectBranchesScreen extends PaginatedProjectScreen {
   private Hyperlink prev;
   private Hyperlink next;
   private BranchesTable branchTable;
@@ -83,56 +82,16 @@
   private HintTextBox nameTxtBox;
   private HintTextBox irevTxtBox;
   private FlowPanel addPanel;
-  private int pageSize;
-  private int start;
   private NpTextBox filterTxt;
-  private String match;
   private Query query;
 
   public ProjectBranchesScreen(final Project.NameKey toShow) {
     super(toShow);
-    pageSize = Gerrit.getUserPreferences().changesPerPage();
   }
 
-  private void parseToken() {
-    String token = getToken();
-
-    for (String kvPair : token.split("[,;&/?]")) {
-      String[] kv = kvPair.split("=", 2);
-      if (kv.length != 2 || kv[0].isEmpty()) {
-        continue;
-      }
-
-      if ("filter".equals(kv[0])) {
-        match = URL.decodeQueryString(kv[1]);
-      }
-
-      if ("skip".equals(kv[0])
-          && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
-        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
-      }
-    }
-  }
-
-  private void setupNavigationLink(Hyperlink link, String filter, int skip) {
-    link.setTargetHistoryToken(getTokenForScreen(filter, skip));
-    link.setVisible(true);
-  }
-
-  private String getTokenForScreen(String filter, int skip) {
-    String token = PageLinks.toProjectBranches(getProjectKey());
-    if (filter != null && !filter.isEmpty()) {
-      token += "?filter=" + URL.encodeQueryString(filter);
-    }
-    if (skip > 0) {
-      if (token.contains("?filter=")) {
-        token += ",";
-      } else {
-        token += "?";
-      }
-      token += "skip=" + skip;
-    }
-    return token;
+  @Override
+  public String getScreenToken() {
+    return PageLinks.toProjectBranches(getProjectKey());
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index f9904cd..7cac8ec 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -28,12 +28,10 @@
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.ProjectSearchLink;
 import com.google.gerrit.client.ui.ProjectsTable;
-import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyUpEvent;
 import com.google.gwt.event.dom.client.KeyUpHandler;
-import com.google.gwt.http.client.URL;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -44,37 +42,21 @@
 
 import java.util.List;
 
-public class ProjectListScreen extends Screen {
+public class ProjectListScreen extends PaginatedProjectScreen {
   private Hyperlink prev;
   private Hyperlink next;
   private ProjectsTable projects;
   private NpTextBox filterTxt;
-  private int pageSize;
 
-  private String match = "";
-  private int start;
   private Query query;
 
   public ProjectListScreen() {
-    pageSize = Gerrit.getUserPreferences().changesPerPage();
+    super(null);
   }
 
   public ProjectListScreen(String params) {
     this();
-    for (String kvPair : params.split("[,;&]")) {
-      String[] kv = kvPair.split("=", 2);
-      if (kv.length != 2 || kv[0].isEmpty()) {
-        continue;
-      }
-
-      if ("filter".equals(kv[0])) {
-        match = URL.decodeQueryString(kv[1]);
-      }
-
-      if ("skip".equals(kv[0]) && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
-        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
-      }
-    }
+    parseToken(params);
   }
 
   @Override
@@ -83,25 +65,9 @@
     query = new Query(match).start(start).run();
   }
 
-  private void setupNavigationLink(Hyperlink link, String filter, int skip) {
-    link.setTargetHistoryToken(getTokenForScreen(filter, skip));
-    link.setVisible(true);
-  }
-
-  private String getTokenForScreen(String filter, int skip) {
-    String token = ADMIN_PROJECTS;
-    if (filter != null && !filter.isEmpty()) {
-      token += "?filter=" + URL.encodeQueryString(filter);
-    }
-    if (skip > 0) {
-      if (token.contains("?filter=")) {
-        token += ",";
-      } else {
-        token += "?";
-      }
-      token += "skip=" + skip;
-    }
-    return token;
+  @Override
+  public String getScreenToken() {
+    return ADMIN_PROJECTS;
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
index 45334db..a63dae4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
@@ -22,6 +22,7 @@
   public static final String BRANCHES = "branches";
   public static final String ACCESS = "access";
   public static final String DASHBOARDS = "dashboards";
+  public static final String TAGS = "tags";
 
   protected static String savedPanel;
   protected static Project.NameKey savedKey;
@@ -47,7 +48,9 @@
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    setPageTitle(Util.M.project(name.get()));
+    if (name != null) {
+      setPageTitle(Util.M.project(name.get()));
+    }
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
new file mode 100644
index 0000000..a1ea5a8
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
@@ -0,0 +1,232 @@
+// 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.client.admin;
+
+import static com.google.gerrit.client.ui.Util.highlight;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.projects.ProjectApi;
+import com.google.gerrit.client.projects.TagInfo;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.InlineHTML;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+import java.util.List;
+
+public class ProjectTagsScreen extends PaginatedProjectScreen {
+  private NpTextBox filterTxt;
+  private Query query;
+  private Hyperlink prev;
+  private Hyperlink next;
+  private TagsTable tagsTable;
+
+  public ProjectTagsScreen(Project.NameKey toShow) {
+    super(toShow);
+  }
+
+  @Override
+  public String getScreenToken() {
+    return PageLinks.toProjectTags(getProjectKey());
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    initPageHeader();
+    prev = new Hyperlink(Util.C.pagedListPrev(), true, "");
+    prev.setVisible(false);
+
+    next = new Hyperlink(Util.C.pagedListNext(), true, "");
+    next.setVisible(false);
+
+    tagsTable = new TagsTable();
+
+    HorizontalPanel buttons = new HorizontalPanel();
+    buttons.setStyleName(Gerrit.RESOURCES.css().branchTablePrevNextLinks());
+    buttons.add(prev);
+    buttons.add(next);
+    add(tagsTable);
+    add(buttons);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    query = new Query(match).start(start).run();
+    savedPanel = TAGS;
+  }
+
+  private void initPageHeader() {
+    parseToken();
+    HorizontalPanel hp = new HorizontalPanel();
+    hp.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel());
+    Label filterLabel = new Label(Util.C.projectFilter());
+    filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
+    hp.add(filterLabel);
+    filterTxt = new NpTextBox();
+    filterTxt.setValue(match);
+    filterTxt.addKeyUpHandler(new KeyUpHandler() {
+      @Override
+      public void onKeyUp(KeyUpEvent event) {
+        Query q = new Query(filterTxt.getValue());
+        if (match.equals(q.qMatch)) {
+          q.start(start);
+        } else if (query == null) {
+          q.run();
+          query = q;
+        }
+      }
+    });
+    hp.add(filterTxt);
+    add(hp);
+  }
+
+  private class TagsTable extends NavigationTable<TagInfo> {
+
+    TagsTable() {
+      table.setWidth("");
+      table.setText(0, 0, Util.C.columnTagName());
+      table.setText(0, 1, Util.C.columnBranchRevision());
+
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
+    }
+
+    void display(List<TagInfo> tags) {
+      displaySubset(tags, 0, tags.size());
+    }
+
+    void displaySubset(List<TagInfo> tags, int fromIndex, int toIndex) {
+      while (1 < table.getRowCount()) {
+        table.removeRow(table.getRowCount() - 1);
+      }
+
+      for (TagInfo k : tags.subList(fromIndex, toIndex)) {
+        int row = table.getRowCount();
+        table.insertRow(row);
+        applyDataRowStyle(row);
+        populate(row, k);
+      }
+    }
+
+    void populate(int row, TagInfo k) {
+      table.setWidget(row, 0, new InlineHTML(highlight(k.getShortName(), match)));
+
+      if (k.revision() != null) {
+        table.setText(row, 1, k.revision());
+      } else {
+        table.setText(row, 1, "");
+      }
+
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      String dataCellStyle = Gerrit.RESOURCES.css().dataCell();
+      fmt.addStyleName(row, 0, dataCellStyle);
+      fmt.addStyleName(row, 1, dataCellStyle);
+
+      setRowItem(row, k);
+    }
+
+    @Override
+    protected void onOpenRow(int row) {
+      if (row > 0) {
+        movePointerTo(row);
+      }
+    }
+
+    @Override
+    protected Object getRowItemKey(TagInfo item) {
+      return item.ref();
+    }
+  }
+
+  @Override
+  public void onShowView() {
+    super.onShowView();
+    if (match != null) {
+      filterTxt.setCursorPos(match.length());
+    }
+    filterTxt.setFocus(true);
+  }
+
+  private class Query {
+    private String qMatch;
+    private int qStart;
+
+    Query(String match) {
+      this.qMatch = match;
+    }
+
+    Query start(int start) {
+      this.qStart = start;
+      return this;
+    }
+
+    Query run() {
+      // Retrieve one more tag than page size to determine if there are more
+      // tags to display
+      ProjectApi.getTags(getProjectKey(), pageSize + 1, qStart, qMatch,
+          new ScreenLoadCallback<JsArray<TagInfo>>(ProjectTagsScreen.this) {
+            @Override
+            public void preDisplay(JsArray<TagInfo> result) {
+              if (!isAttached()) {
+                // View has been disposed.
+              } else if (query == Query.this) {
+                query = null;
+                showList(result);
+              } else {
+                query.run();
+              }
+            }
+          });
+      return this;
+    }
+
+    void showList(JsArray<TagInfo> result) {
+      setToken(getTokenForScreen(qMatch, qStart));
+      ProjectTagsScreen.this.match = qMatch;
+      ProjectTagsScreen.this.start = qStart;
+
+      if (result.length() <= pageSize) {
+        tagsTable.display(Natives.asList(result));
+        next.setVisible(false);
+      } else {
+        tagsTable.displaySubset(Natives.asList(result), 0,
+            result.length() - 1);
+        setupNavigationLink(next, qMatch, qStart + pageSize);
+      }
+      if (qStart > 0) {
+        setupNavigationLink(prev, qMatch, qStart - pageSize);
+      } else {
+        prev.setVisible(false);
+      }
+
+      if (!isCurrentView()) {
+        display();
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
index b94f5d5..8d166a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
@@ -17,17 +17,9 @@
 import com.google.gerrit.client.info.ActionInfo;
 import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 
-public class BranchInfo extends JavaScriptObject {
-  public final String getShortName() {
-    return RefNames.shortName(ref());
-  }
-
-  public final native String ref() /*-{ return this.ref; }-*/;
-  public final native String revision() /*-{ return this.revision; }-*/;
+public class BranchInfo extends RefInfo {
   public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
   public final native NativeMap<ActionInfo> actions() /*-{ return this.actions }-*/;
   public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index 53eba42..f9c3640 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -44,6 +44,32 @@
         .put(input, cb);
   }
 
+  private static RestApi getRestApi(Project.NameKey name, String viewName,
+      int limit, int start, String match) {
+    RestApi call = project(name).view(viewName);
+    call.addParameter("n", limit);
+    call.addParameter("s", start);
+    if (match != null) {
+      if (match.startsWith("^")) {
+        call.addParameter("r", match);
+      } else {
+        call.addParameter("m", match);
+      }
+    }
+    return call;
+  }
+
+  /** Retrieve all visible tags of the project */
+  public static void getTags(Project.NameKey name,
+      AsyncCallback<JsArray<TagInfo>> cb) {
+    project(name).view("tags").get(cb);
+  }
+
+  public static void getTags(Project.NameKey name, int limit, int start,
+      String match, AsyncCallback<JsArray<TagInfo>> cb) {
+    getRestApi(name, "tags", limit, start, match).get(cb);
+  }
+
   /** Create a new branch */
   public static void createBranch(Project.NameKey name, String ref,
       String revision, AsyncCallback<BranchInfo> cb) {
@@ -60,17 +86,7 @@
 
   public static void getBranches(Project.NameKey name, int limit, int start,
        String match, AsyncCallback<JsArray<BranchInfo>> cb) {
-    RestApi call = project(name).view("branches");
-    call.addParameter("n", limit);
-    call.addParameter("s", start);
-    if (match != null) {
-      if (match.startsWith("^")) {
-        call.addParameter("r", match);
-      } else {
-        call.addParameter("m", match);
-      }
-    }
-    call.get(cb);
+    getRestApi(name, "branches", limit, start, match).get(cb);
   }
 
   /**
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java
new file mode 100644
index 0000000..053dbd3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java
@@ -0,0 +1,30 @@
+// 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.client.projects;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class RefInfo extends JavaScriptObject {
+  public final String getShortName() {
+    return RefNames.shortName(ref());
+  }
+
+  public final native String ref() /*-{ return this.ref; }-*/;
+  public final native String revision() /*-{ return this.revision; }-*/;
+
+  protected RefInfo() {
+  }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java
new file mode 100644
index 0000000..ee1d1af
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java
@@ -0,0 +1,22 @@
+// 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.client.projects;
+
+public class TagInfo extends RefInfo {
+
+  // TODO(dpursehouse) add extra tag-related fields (message, tagger, etc)
+  protected TagInfo() {
+  }
+}