Merge "Add WebLink extension point for file history"
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
index 7041dcc..a83f46c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
@@ -32,6 +32,7 @@
   protected LabelTypes labelTypes;
   protected Map<String, String> capabilities;
   protected Map<AccountGroup.UUID, GroupInfo> groupInfo;
+  protected List<WebLinkInfoCommon> fileHistoryLinks;
 
   public ProjectAccess() {
   }
@@ -132,4 +133,12 @@
   public void setGroupInfo(Map<AccountGroup.UUID, GroupInfo> m) {
     groupInfo = m;
   }
+
+  public void setFileHistoryLinks(List<WebLinkInfoCommon> links) {
+    fileHistoryLinks = links;
+  }
+
+  public List<WebLinkInfoCommon> getFileHistoryLinks() {
+    return fileHistoryLinks;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/WebLinkInfoCommon.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/WebLinkInfoCommon.java
new file mode 100644
index 0000000..dd0a70a
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/WebLinkInfoCommon.java
@@ -0,0 +1,24 @@
+// 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.common.data;
+
+public class WebLinkInfoCommon {
+  public WebLinkInfoCommon() {}
+
+  public String name;
+  public String imageUrl;
+  public String url;
+  public String target;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileHistoryWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileHistoryWebLink.java
new file mode 100644
index 0000000..f9f9e58
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileHistoryWebLink.java
@@ -0,0 +1,37 @@
+// 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.webui;
+
+import com.google.gerrit.extensions.common.WebLinkInfo;
+
+public interface FileHistoryWebLink extends WebLink {
+  /**
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo}
+   * describing a link from a file to an external service displaying
+   * a log for that file.
+   *
+   * <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 revision Name of the revision (e.g. branch or commit ID)
+   * @param fileName Name of the file
+   * @return WebLinkInfo that links to a log for the file in external
+   * service, null if there should be no link.
+   */
+  WebLinkInfo getFileHistoryWebLink(String projectName, String revision, String fileName);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
index 13b3a54..177faff0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.client.ui.ParentProjectBox;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ProjectAccess;
+import com.google.gerrit.common.data.WebLinkInfoCommon;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -40,6 +41,7 @@
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Image;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -65,7 +67,7 @@
   DivElement history;
 
   @UiField
-  Anchor gitweb;
+  FlowPanel webLinkPanel;
 
   @UiField
   FlowPanel localContainer;
@@ -120,16 +122,7 @@
     } else {
       inheritsFrom.getStyle().setDisplay(Display.NONE);
     }
-
-    GitwebInfo c = Gerrit.info().gitweb();
-    if (value.isConfigVisible() && c != null) {
-      history.getStyle().setDisplay(Display.BLOCK);
-      gitweb.setText(c.getLinkName());
-      gitweb.setHref(c.toFileHistory(new Branch.NameKey(value.getProjectName(),
-          RefNames.REFS_CONFIG), "project.config"));
-    } else {
-      history.getStyle().setDisplay(Display.NONE);
-    }
+    setUpWebLinks();
 
     addSection.setVisible(editing && (!value.getOwnerOf().isEmpty() || value.canUpload()));
   }
@@ -162,6 +155,53 @@
     addSection.setVisible(editing);
   }
 
+  private void setUpWebLinks() {
+    if (!value.isConfigVisible()) {
+      history.getStyle().setDisplay(Display.NONE);
+    } else {
+      GitwebInfo c = Gerrit.info().gitweb();
+      List<WebLinkInfoCommon> links = value.getFileHistoryLinks();
+      if (c == null && links == null) {
+        history.getStyle().setDisplay(Display.NONE);
+      }
+      if (c != null) {
+        webLinkPanel.add(toAnchor(c.toFileHistory(new Branch.NameKey(value.getProjectName(),
+            RefNames.REFS_CONFIG), "project.config"), c.getLinkName()));
+      }
+
+      if (links != null) {
+        for (WebLinkInfoCommon link : links) {
+          webLinkPanel.add(toAnchor(link));
+        }
+      }
+    }
+  }
+
+  private Anchor toAnchor(String href, String name) {
+    Anchor a = new Anchor();
+    a.setHref(href);
+    a.setText(name);
+    return a;
+  }
+
+  private static Anchor toAnchor(WebLinkInfoCommon info) {
+    Anchor a = new Anchor();
+    a.setHref(info.url);
+    if (info.target != null && !info.target.isEmpty()) {
+      a.setTarget(info.target);
+    }
+    if (info.imageUrl != null && !info.imageUrl.isEmpty()) {
+      Image img = new Image();
+      img.setAltText(info.name);
+      img.setUrl(info.imageUrl);
+      img.setTitle(info.name);
+      a.getElement().appendChild(img.getElement());
+    } else {
+      a.setText("(" + info.name + ")");
+    }
+    return a;
+  }
+
   private class Source extends EditorSource<AccessSectionEditor> {
     private final FlowPanel container;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
index 0db4779..ebe6caf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
@@ -39,9 +39,12 @@
   .historyTitle {
     font-weight: bold;
   }
-  .gitwebLink {
+  .webLinkPanel a {
     display: inline;
   }
+  .webLinkPanel>a {
+    margin-left:2px;
+  }
 
   .addContainer {
     margin-top: 5px;
@@ -62,7 +65,9 @@
   </div>
   <div ui:field='history' class='{style.history}'>
     <span class='{style.historyTitle}'><ui:msg>History:</ui:msg></span>
-    <g:Anchor ui:field='gitweb' styleName='{style.gitwebLink}'></g:Anchor>
+    <td>
+      <g:FlowPanel ui:field="webLinkPanel" styleName='{style.webLinkPanel}'/>
+    </td>
   </div>
 
   <g:FlowPanel ui:field='localContainer'/>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index 3615610..aee238d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.rpc.project;
 
 import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupDescription;
@@ -23,11 +24,13 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.ProjectAccess;
 import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.common.data.WebLinkInfoCommon;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -64,6 +67,7 @@
 
   private final Project.NameKey projectName;
   private ProjectControl pc;
+  private WebLinks webLinks;
 
   @Inject
   ProjectAccessFactory(final GroupBackend groupBackend,
@@ -72,6 +76,7 @@
       final GroupControl.Factory groupControlFactory,
       final MetaDataUpdate.Server metaDataUpdateFactory,
       final AllProjectsName allProjectsName,
+      final WebLinks webLinks,
 
       @Assisted final Project.NameKey name) {
     this.groupBackend = groupBackend;
@@ -80,6 +85,7 @@
     this.groupControlFactory = groupControlFactory;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.allProjectsName = allProjectsName;
+    this.webLinks = webLinks;
 
     this.projectName = name;
   }
@@ -209,9 +215,17 @@
     detail.setConfigVisible(pc.isOwner() || metaConfigControl.isVisible());
     detail.setGroupInfo(buildGroupInfo(local));
     detail.setLabelTypes(pc.getLabelTypes());
+    detail.setFileHistoryLinks(getConfigFileLogLinks(projectName.get()));
     return detail;
   }
 
+  private List<WebLinkInfoCommon> getConfigFileLogLinks(String projectName) {
+    FluentIterable<WebLinkInfoCommon> links =
+        webLinks.getFileHistoryLinksCommon(projectName, RefNames.REFS_CONFIG,
+            ProjectConfig.PROJECT_CONFIG);
+    return links.isEmpty() ? null : links.toList();
+  }
+
   private Map<AccountGroup.UUID, GroupInfo> buildGroupInfo(List<AccessSection> local) {
     Map<AccountGroup.UUID, GroupInfo> infos = new HashMap<>();
     for (AccessSection section : local) {
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 1403e60..351de5e 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
@@ -18,11 +18,13 @@
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
+import com.google.gerrit.common.data.WebLinkInfoCommon;
 import com.google.gerrit.extensions.common.DiffWebLinkInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.BranchWebLink;
 import com.google.gerrit.extensions.webui.DiffWebLink;
+import com.google.gerrit.extensions.webui.FileHistoryWebLink;
 import com.google.gerrit.extensions.webui.FileWebLink;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.extensions.webui.ProjectWebLink;
@@ -53,9 +55,26 @@
           return true;
         }
       };
+  private static final Predicate<WebLinkInfoCommon> INVALID_WEBLINK_COMMON =
+      new Predicate<WebLinkInfoCommon>() {
+
+        @Override
+        public boolean apply(WebLinkInfoCommon link) {
+          if (link == null) {
+            return false;
+          } else if (Strings.isNullOrEmpty(link.name)
+              || Strings.isNullOrEmpty(link.url)) {
+            log.warn(String.format("%s is missing name and/or url", link
+                .getClass().getName()));
+            return false;
+          }
+          return true;
+        }
+      };
 
   private final DynamicSet<PatchSetWebLink> patchSetLinks;
   private final DynamicSet<FileWebLink> fileLinks;
+  private final DynamicSet<FileHistoryWebLink> fileHistoryLinks;
   private final DynamicSet<DiffWebLink> diffLinks;
   private final DynamicSet<ProjectWebLink> projectLinks;
   private final DynamicSet<BranchWebLink> branchLinks;
@@ -63,11 +82,14 @@
   @Inject
   public WebLinks(DynamicSet<PatchSetWebLink> patchSetLinks,
       DynamicSet<FileWebLink> fileLinks,
+      DynamicSet<FileHistoryWebLink> fileLogLinks,
       DynamicSet<DiffWebLink> diffLinks,
       DynamicSet<ProjectWebLink> projectLinks,
-      DynamicSet<BranchWebLink> branchLinks) {
+      DynamicSet<BranchWebLink> branchLinks
+      ) {
     this.patchSetLinks = patchSetLinks;
     this.fileLinks = fileLinks;
+    this.fileHistoryLinks = fileLogLinks;
     this.diffLinks = diffLinks;
     this.projectLinks = projectLinks;
     this.branchLinks = branchLinks;
@@ -111,6 +133,46 @@
   /**
    *
    * @param project Project name.
+   * @param revision SHA1 of revision.
+   * @param file File name.
+   * @return Links for file history
+   */
+  public FluentIterable<WebLinkInfo> getFileHistoryLinks(final String project,
+      final String revision, final String file) {
+    return filterLinks(fileHistoryLinks, new Function<WebLink, WebLinkInfo>() {
+
+      @Override
+      public WebLinkInfo apply(WebLink webLink) {
+        return ((FileHistoryWebLink) webLink).getFileHistoryWebLink(project,
+            revision, file);
+      }
+    });
+  }
+
+  public FluentIterable<WebLinkInfoCommon> getFileHistoryLinksCommon(
+      final String project, final String revision, final String file) {
+    return FluentIterable
+        .from(fileHistoryLinks)
+        .transform(new Function<WebLink, WebLinkInfoCommon>() {
+          @Override
+          public WebLinkInfoCommon apply(WebLink webLink) {
+            WebLinkInfo info =
+                ((FileHistoryWebLink) webLink).getFileHistoryWebLink(project,
+                    revision, file);
+            WebLinkInfoCommon commonInfo = new WebLinkInfoCommon();
+            commonInfo.name = info.name;
+            commonInfo.imageUrl = info.imageUrl;
+            commonInfo.url = info.url;
+            commonInfo.target = info.target;
+            return commonInfo;
+          }
+        })
+        .filter(INVALID_WEBLINK_COMMON);
+  }
+
+  /**
+   *
+   * @param project Project name.
    * @param patchSetIdA Patch set ID of side A, <code>null</code> if no base
    *        patch set was selected.
    * @param revisionA SHA1 of revision of side A.
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 478febe..a452561 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
@@ -37,6 +37,7 @@
 import com.google.gerrit.extensions.systemstatus.MessageOfTheDay;
 import com.google.gerrit.extensions.webui.BranchWebLink;
 import com.google.gerrit.extensions.webui.DiffWebLink;
+import com.google.gerrit.extensions.webui.FileHistoryWebLink;
 import com.google.gerrit.extensions.webui.FileWebLink;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.extensions.webui.ProjectWebLink;
@@ -285,6 +286,7 @@
     DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
     DynamicSet.setOf(binder(), PatchSetWebLink.class);
     DynamicSet.setOf(binder(), FileWebLink.class);
+    DynamicSet.setOf(binder(), FileHistoryWebLink.class);
     DynamicSet.setOf(binder(), DiffWebLink.class);
     DynamicSet.setOf(binder(), ProjectWebLink.class);
     DynamicSet.setOf(binder(), BranchWebLink.class);
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index ac84984..7306135 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit ac84984d90e09be358754e707a192ddce2ea6d63
+Subproject commit 730613516a733fa33f684cbe03fe22ecf811216e