Add support for tag web links

Change-Id: If431ad869e53d97e1f479edaf24e17f3b9ecbda2
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 3692dfb7..2976e96 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2193,6 +2193,8 @@
 
 FileHistoryWebLinks will appear on the access rights screen.
 
+TagWebLinks will appear in the tag list in the last column.
+
 [[lfs-extension]]
 == LFS Storage Plugins
 
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 72c6a39..46286e8 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2882,6 +2882,9 @@
 link:rest-api-changes.html#git-person-info[GitPersonInfo] entity.
 |`can_delete`|`false` if not set|
 Whether the calling user can delete this tag.
+|`web_links` |optional|
+Links to the tag in external sites as a list of
+link:rest-api-changes.html#web-link-info[WebLinkInfo] entries.
 |=========================
 
 [[tag-input]]
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebType.java
index 248de49..9cc408b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebType.java
@@ -24,6 +24,7 @@
   private String project;
   private String revision;
   private String rootTree;
+  private String tag;
 
   private char pathSeparator = '/';
   private boolean urlEncode = true;
@@ -56,6 +57,20 @@
     branch = str;
   }
 
+  /** @return parameterized string for the tag URL. */
+  public String getTag() {
+    return tag;
+  }
+
+  /**
+   * Set the parameterized string for the tag URL.
+   *
+   * @param str new string.
+   */
+  public void setTag(String str) {
+    tag = str;
+  }
+
   /** @return parameterized string for the file URL. */
   public String getFile() {
     return file;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
index ccfea46..c7b1b94 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
@@ -15,16 +15,20 @@
 package com.google.gerrit.extensions.api.projects;
 
 import com.google.gerrit.extensions.common.GitPerson;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+import java.util.List;
 
 public class TagInfo extends RefInfo {
   public String object;
   public String message;
   public GitPerson tagger;
+  public List<WebLinkInfo> webLinks;
 
-  public TagInfo(String ref, String revision, boolean canDelete) {
+  public TagInfo(String ref, String revision, boolean canDelete, List<WebLinkInfo> webLinks) {
     this.ref = ref;
     this.revision = revision;
     this.canDelete = canDelete;
+    this.webLinks = webLinks;
   }
 
   public TagInfo(
@@ -33,10 +37,12 @@
       String object,
       String message,
       GitPerson tagger,
-      boolean canDelete) {
-    this(ref, revision, canDelete);
+      boolean canDelete,
+      List<WebLinkInfo> webLinks) {
+    this(ref, revision, canDelete, webLinks);
     this.object = object;
     this.message = message;
     this.tagger = tagger;
+    this.webLinks = webLinks;
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TagWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TagWebLink.java
new file mode 100644
index 0000000..14ccb4a
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TagWebLink.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2017 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.webui;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+
+@ExtensionPoint
+public interface TagWebLink extends WebLink {
+
+  /**
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo} describing a link from a tag to an
+   * external service.
+   *
+   * <p>In order for the web link to be visible {@link
+   * com.google.gerrit.extensions.common.WebLinkInfo#url} and {@link
+   * com.google.gerrit.extensions.common.WebLinkInfo#name} must be set.
+   *
+   * <p>
+   *
+   * @param projectName Name of the project
+   * @param tagName Name of the tag
+   * @return WebLinkInfo that links to tag in external service, null if there should be no link.
+   */
+  WebLinkInfo getTagWebLink(String projectName, String tagName);
+}
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
index f38d36c..45d0fdb 100644
--- 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
@@ -23,6 +23,7 @@
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.access.AccessMap;
 import com.google.gerrit.client.access.ProjectAccessInfo;
+import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.projects.ProjectApi;
 import com.google.gerrit.client.projects.TagInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -301,6 +302,7 @@
       fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
       fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
       fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
 
       updateDeleteHandler =
           new ValueChangeHandler<Boolean>() {
@@ -431,12 +433,21 @@
         table.setText(row, 3, "");
       }
 
+      FlowPanel actionsPanel = new FlowPanel();
+      if (k.webLinks() != null) {
+        for (WebLinkInfo webLink : Natives.asList(k.webLinks())) {
+          actionsPanel.add(webLink.toAnchor());
+        }
+      }
+      table.setWidget(row, 4, actionsPanel);
+
       FlexCellFormatter fmt = table.getFlexCellFormatter();
       String iconCellStyle = Gerrit.RESOURCES.css().iconCell();
       String dataCellStyle = Gerrit.RESOURCES.css().dataCell();
       fmt.addStyleName(row, 1, iconCellStyle);
       fmt.addStyleName(row, 2, dataCellStyle);
       fmt.addStyleName(row, 3, dataCellStyle);
+      fmt.addStyleName(row, 4, dataCellStyle);
 
       setRowItem(row, k);
     }
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
index 24487ae..fc13fe1 100644
--- 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
@@ -14,9 +14,14 @@
 
 package com.google.gerrit.client.projects;
 
+import com.google.gerrit.client.info.WebLinkInfo;
+import com.google.gwt.core.client.JsArray;
+
 public class TagInfo extends RefInfo {
   public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
 
+  public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
+
   // TODO(dpursehouse) add extra tag-related fields (message, tagger, etc)
   protected TagInfo() {}
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
index 533ed9d..18fdf8f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.extensions.webui.ParentWebLink;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.extensions.webui.ProjectWebLink;
+import com.google.gerrit.extensions.webui.TagWebLink;
 import com.google.gerrit.extensions.webui.WebLink;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
@@ -70,6 +71,7 @@
   private final DynamicSet<DiffWebLink> diffLinks;
   private final DynamicSet<ProjectWebLink> projectLinks;
   private final DynamicSet<BranchWebLink> branchLinks;
+  private final DynamicSet<TagWebLink> tagLinks;
 
   @Inject
   public WebLinks(
@@ -79,7 +81,8 @@
       DynamicSet<FileHistoryWebLink> fileLogLinks,
       DynamicSet<DiffWebLink> diffLinks,
       DynamicSet<ProjectWebLink> projectLinks,
-      DynamicSet<BranchWebLink> branchLinks) {
+      DynamicSet<BranchWebLink> branchLinks,
+      DynamicSet<TagWebLink> tagLinks) {
     this.patchSetLinks = patchSetLinks;
     this.parentLinks = parentLinks;
     this.fileLinks = fileLinks;
@@ -87,6 +90,7 @@
     this.diffLinks = diffLinks;
     this.projectLinks = projectLinks;
     this.branchLinks = branchLinks;
+    this.tagLinks = tagLinks;
   }
 
   /**
@@ -194,6 +198,15 @@
     return filterLinks(branchLinks, webLink -> webLink.getBranchWebLink(project, branch));
   }
 
+  /**
+   * @param project Project name
+   * @param tag Tag name
+   * @return Links for tags.
+   */
+  public List<WebLinkInfo> getTagLinks(String project, String tag) {
+    return filterLinks(tagLinks, webLink -> webLink.getTagWebLink(project, tag));
+  }
+
   private <T extends WebLink> List<WebLinkInfo> filterLinks(
       DynamicSet<T> links, Function<T, WebLinkInfo> transformer) {
     return FluentIterable.from(links).transform(transformer).filter(INVALID_WEBLINK).toList();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 4f948d7..c7d7f8a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -67,6 +67,7 @@
 import com.google.gerrit.extensions.webui.ParentWebLink;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.extensions.webui.ProjectWebLink;
+import com.google.gerrit.extensions.webui.TagWebLink;
 import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.rules.PrologModule;
@@ -366,6 +367,7 @@
     DynamicSet.setOf(binder(), DiffWebLink.class);
     DynamicSet.setOf(binder(), ProjectWebLink.class);
     DynamicSet.setOf(binder(), BranchWebLink.class);
+    DynamicSet.setOf(binder(), TagWebLink.class);
     DynamicMap.mapOf(binder(), OAuthLoginProvider.class);
     DynamicItem.itemOf(binder(), OAuthTokenEncrypter.class);
     DynamicSet.setOf(binder(), AccountExternalIdCreator.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebConfig.java
index 723e9bf..6c0f769 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebConfig.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.extensions.webui.ParentWebLink;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.extensions.webui.ProjectWebLink;
+import com.google.gerrit.extensions.webui.TagWebLink;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -64,6 +65,10 @@
           DynamicSet.bind(binder(), BranchWebLink.class).to(GitwebLinks.class);
         }
 
+        if (!isNullOrEmpty(type.getTag())) {
+          DynamicSet.bind(binder(), TagWebLink.class).to(GitwebLinks.class);
+        }
+
         if (!isNullOrEmpty(type.getFile()) || !isNullOrEmpty(type.getRootTree())) {
           DynamicSet.bind(binder(), FileWebLink.class).to(GitwebLinks.class);
         }
@@ -101,6 +106,7 @@
     type.setLinkName(
         firstNonNull(cfg.getString("gitweb", null, "linkname"), defaultType.getLinkName()));
     type.setBranch(firstNonNull(cfg.getString("gitweb", null, "branch"), defaultType.getBranch()));
+    type.setTag(firstNonNull(cfg.getString("gitweb", null, "tag"), defaultType.getTag()));
     type.setProject(
         firstNonNull(cfg.getString("gitweb", null, "project"), defaultType.getProject()));
     type.setRevision(
@@ -135,6 +141,7 @@
         type.setProject("?p=${project}.git;a=summary");
         type.setRevision("?p=${project}.git;a=commit;h=${commit}");
         type.setBranch("?p=${project}.git;a=shortlog;h=${branch}");
+        type.setTag("?p=${project}.git;a=tag;h=${tag}");
         type.setRootTree("?p=${project}.git;a=tree;hb=${commit}");
         type.setFile("?p=${project}.git;hb=${commit};f=${file}");
         type.setFileHistory("?p=${project}.git;a=history;hb=${branch};f=${file}");
@@ -144,6 +151,7 @@
         type.setProject("${project}.git/summary");
         type.setRevision("${project}.git/commit/?id=${commit}");
         type.setBranch("${project}.git/log/?h=${branch}");
+        type.setTag("${project}.git/tag/?h=${tag}");
         type.setRootTree("${project}.git/tree/?h=${commit}");
         type.setFile("${project}.git/tree/${file}?h=${commit}");
         type.setFileHistory("${project}.git/log/${file}?h=${branch}");
@@ -154,6 +162,7 @@
         type.setProject("");
         type.setRevision("");
         type.setBranch("");
+        type.setTag("");
         type.setRootTree("");
         type.setFile("");
         type.setFileHistory("");
@@ -236,7 +245,8 @@
           FileWebLink,
           PatchSetWebLink,
           ParentWebLink,
-          ProjectWebLink {
+          ProjectWebLink,
+          TagWebLink {
     private final String url;
     private final GitwebType type;
     private final ParameterizedString branch;
@@ -244,6 +254,7 @@
     private final ParameterizedString fileHistory;
     private final ParameterizedString project;
     private final ParameterizedString revision;
+    private final ParameterizedString tag;
 
     @Inject
     GitwebLinks(GitwebConfig config, GitwebType type) {
@@ -254,6 +265,7 @@
       this.fileHistory = parse(type.getFileHistory());
       this.project = parse(type.getProject());
       this.revision = parse(type.getRevision());
+      this.tag = parse(type.getTag());
     }
 
     @Override
@@ -269,6 +281,15 @@
     }
 
     @Override
+    public WebLinkInfo getTagWebLink(String projectName, String tagName) {
+      if (tag != null) {
+        return link(
+            tag.replace("project", encode(projectName)).replace("tag", encode(tagName)).toString());
+      }
+      return null;
+    }
+
+    @Override
     public WebLinkInfo getFileHistoryWebLink(String projectName, String revision, String fileName) {
       if (fileHistory != null) {
         return link(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
index f674d17..3a6db40 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.TagCache;
@@ -60,6 +61,7 @@
   private final GitRepositoryManager repoManager;
   private final TagCache tagCache;
   private final GitReferenceUpdated referenceUpdated;
+  private final WebLinks links;
   private String ref;
 
   @Inject
@@ -68,11 +70,13 @@
       GitRepositoryManager repoManager,
       TagCache tagCache,
       GitReferenceUpdated referenceUpdated,
+      WebLinks webLinks,
       @Assisted String ref) {
     this.identifiedUser = identifiedUser;
     this.repoManager = repoManager;
     this.tagCache = tagCache;
     this.referenceUpdated = referenceUpdated;
+    this.links = webLinks;
     this.ref = ref;
   }
 
@@ -134,7 +138,8 @@
             result.getObjectId(),
             identifiedUser.get().getAccount());
         try (RevWalk w = new RevWalk(repo)) {
-          return ListTags.createTagInfo(result, w, refControl);
+          ProjectControl pctl = resource.getControl();
+          return ListTags.createTagInfo(result, w, refControl, pctl, links);
         }
       }
     } catch (InvalidRevisionException e) {
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 7f1ee60..d7385fe 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
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -24,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommonConverters;
+import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
@@ -54,6 +56,7 @@
   private final TagCache tagCache;
   private final ChangeNotes.Factory changeNotesFactory;
   @Nullable private final SearchingChangeCacheImpl changeCache;
+  private final WebLinks links;
 
   @Option(
     name = "--limit",
@@ -106,12 +109,14 @@
       Provider<ReviewDb> dbProvider,
       TagCache tagCache,
       ChangeNotes.Factory changeNotesFactory,
-      @Nullable SearchingChangeCacheImpl changeCache) {
+      @Nullable SearchingChangeCacheImpl changeCache,
+      WebLinks webLinks) {
     this.repoManager = repoManager;
     this.dbProvider = dbProvider;
     this.tagCache = tagCache;
     this.changeNotesFactory = changeNotesFactory;
     this.changeCache = changeCache;
+    this.links = webLinks;
   }
 
   @Override
@@ -121,11 +126,11 @@
 
     try (Repository repo = getRepository(resource.getNameKey());
         RevWalk rw = new RevWalk(repo)) {
-      ProjectControl control = resource.getControl();
+      ProjectControl pctl = resource.getControl();
       Map<String, Ref> all =
-          visibleTags(control, repo, repo.getRefDatabase().getRefs(Constants.R_TAGS));
+          visibleTags(pctl, repo, repo.getRefDatabase().getRefs(Constants.R_TAGS));
       for (Ref ref : all.values()) {
-        tags.add(createTagInfo(ref, rw, control.controlForRef(ref.getName())));
+        tags.add(createTagInfo(ref, rw, pctl.controlForRef(ref.getName()), pctl, links));
       }
     }
 
@@ -155,18 +160,19 @@
         tagName = Constants.R_TAGS + tagName;
       }
       Ref ref = repo.getRefDatabase().exactRef(tagName);
-      ProjectControl control = resource.getControl();
-      if (ref != null
-          && !visibleTags(control, repo, ImmutableMap.of(ref.getName(), ref)).isEmpty()) {
-        return createTagInfo(ref, rw, control.controlForRef(ref.getName()));
+      ProjectControl pctl = resource.getControl();
+      if (ref != null && !visibleTags(pctl, repo, ImmutableMap.of(ref.getName(), ref)).isEmpty()) {
+        return createTagInfo(ref, rw, pctl.controlForRef(ref.getName()), pctl, links);
       }
     }
     throw new ResourceNotFoundException(id);
   }
 
-  public static TagInfo createTagInfo(Ref ref, RevWalk rw, RefControl control)
+  public static TagInfo createTagInfo(
+      Ref ref, RevWalk rw, RefControl control, ProjectControl pctl, WebLinks links)
       throws MissingObjectException, IOException {
     RevObject object = rw.parseAny(ref.getObjectId());
+    List<WebLinkInfo> webLinks = links.getTagLinks(pctl.getProject().getName(), ref.getName());
     if (object instanceof RevTag) {
       // Annotated or signed tag
       RevTag tag = (RevTag) object;
@@ -177,10 +183,15 @@
           tag.getObject().getName(),
           tag.getFullMessage().trim(),
           tagger != null ? CommonConverters.toGitPerson(tag.getTaggerIdent()) : null,
-          control.canDelete());
+          control.canDelete(),
+          webLinks.isEmpty() ? null : webLinks);
     }
     // Lightweight tag
-    return new TagInfo(ref.getName(), ref.getObjectId().getName(), control.canDelete());
+    return new TagInfo(
+        ref.getName(),
+        ref.getObjectId().getName(),
+        control.canDelete(),
+        webLinks.isEmpty() ? null : webLinks);
   }
 
   private Repository getRepository(Project.NameKey project)
@@ -193,9 +204,9 @@
   }
 
   private Map<String, Ref> visibleTags(
-      ProjectControl control, Repository repo, Map<String, Ref> tags) {
+      ProjectControl pctl, Repository repo, Map<String, Ref> tags) {
     return new VisibleRefFilter(
-            tagCache, changeNotesFactory, changeCache, repo, control, dbProvider.get(), false)
+            tagCache, changeNotesFactory, changeCache, repo, pctl, dbProvider.get(), false)
         .filter(tags, true);
   }
 }