Add support for diff web links

Plugins can now add web links to the diff screen that will be
displayed next to the navigation icons.

Change-Id: Ifa01103054c3e81e3121c219ee5b69db2fefb3bf
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 53e1173..75bee9f 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1772,6 +1772,9 @@
 FileWebLinks will appear in the side-by-side diff screen on the right
 side of the patch selection on each side.
 
+DiffWebLinks will appear in the side-by-side and unified diff screen in
+the header next to the navigation icons.
+
 ProjectWebLinks will appear in the project list in the
 `Repository Browser` column.
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index dffbf30..6fa99c0 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2972,6 +2972,9 @@
 The `base` parameter can be specified to control the base patch set from which the diff should
 be generated.
 
+[[weblinks-only]]
+If the `weblinks-only` parameter is specified, only the diff web links are returned.
+
 .Request
 ----
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/b6b9c10649b9041884046119ab794374470a1b45/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/diff?base=2 HTTP/1.0
@@ -3589,6 +3592,9 @@
 The `DiffInfo` entity contains information about the diff of a file
 in a revision.
 
+If the link:#weblinks-only[weblinks-only] parameter is specified, only
+the `web_links` field is set.
+
 [options="header",cols="1,^1,5"]
 |==========================
 |Field Name        ||Description
@@ -3605,6 +3611,9 @@
 |`diff_header`     ||A list of strings representing the patch set diff header.
 |`content`         ||The content differences in the file as a list of
 link:#diff-content[DiffContent] entities.
+|`web_links`       |optional|
+Links to the file diff in external sites as a list of
+link:rest-api-changes.html#diff-web-link-info[DiffWebLinkInfo] entries.
 |==========================
 
 [[diff-intraline-info]]
@@ -3621,6 +3630,23 @@
 Note that the implied newline character at the end of each line is included in
 the length calculation, and thus it is possible for the edits to span newlines.
 
+[[diff-web-link-info]]
+=== DiffWebLinkInfo
+The `DiffWebLinkInfo` entity describes a link on a diff screen to an
+external site.
+
+[options="header",cols="1,6"]
+|=======================
+|Field Name|Description
+|`name`     |The link name.
+|`url`      |The link URL.
+|`image_url`|URL to the icon of the link.
+|show_on_side_by_side_diff_view|
+Whether the web link should be shown on the side-by-side diff screen.
+|show_on_unified_diff_view|
+Whether the web link should be shown on the unified diff screen.
+|=======================
+
 [[fetch-info]]
 === FetchInfo
 The `FetchInfo` entity contains information about how to fetch a patch
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffWebLinkInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffWebLinkInfo.java
new file mode 100644
index 0000000..71acca3
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffWebLinkInfo.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2014 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.common;
+
+public class DiffWebLinkInfo extends WebLinkInfo {
+  public Boolean showOnSideBySideDiffView;
+  public Boolean showOnUnifiedDiffView;
+
+  public static DiffWebLinkInfo forSideBySideDiffView(String name,
+      String imageUrl, String url, String target) {
+    return new DiffWebLinkInfo(name, imageUrl, url, target, true, false);
+  }
+
+  public static DiffWebLinkInfo forUnifiedDiffView(String name,
+      String imageUrl, String url, String target) {
+    return new DiffWebLinkInfo(name, imageUrl, url, target, false, true);
+  }
+
+  public static DiffWebLinkInfo forSideBySideAndUnifiedDiffView(String name,
+      String imageUrl, String url, String target) {
+    return new DiffWebLinkInfo(name, imageUrl, url, target, true, true);
+  }
+
+  private DiffWebLinkInfo(String name, String imageUrl, String url,
+      String target, boolean showOnSideBySideDiffView,
+      boolean showOnUnifiedDiffView) {
+    super(name, imageUrl, url, target);
+    this.showOnSideBySideDiffView = showOnSideBySideDiffView ? true : null;
+    this.showOnUnifiedDiffView = showOnUnifiedDiffView ? true : null;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/DiffWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/DiffWebLink.java
new file mode 100644
index 0000000..ad53519
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/DiffWebLink.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2014 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.DiffWebLinkInfo;
+
+@ExtensionPoint
+public interface DiffWebLink extends WebLink {
+
+  /**
+   * {@link com.google.gerrit.extensions.common.DiffWebLinkInfo}
+   * describing a link from a file diff 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 changeId ID of the change
+   * @param patchSetIdA Patch set ID of side A, <code>null</code> if no base
+   *        patch set was selected
+   * @param revisionA Name of the revision of side A (e.g. branch or commit ID)
+   * @param fileNameA Name of the file of side A
+   * @param patchSetIdB Patch set ID of side B
+   * @param revisionB Name of the revision of side B (e.g. branch or commit ID)
+   * @param fileNameB Name of the file of side B
+   * @return WebLinkInfo that links to file diff in external service,
+   * null if there should be no link.
+   */
+  DiffWebLinkInfo getDiffLink(String projectName, int changeId,
+        Integer patchSetIdA, String revisionA, String fileNameA,
+        int patchSetIdB, String revisionB, String fileNameB);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java
new file mode 100644
index 0000000..bcf4256
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2014 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;
+
+public class DiffWebLinkInfo extends WebLinkInfo {
+  public final native boolean showOnSideBySideDiffView()
+  /*-{ return this.show_on_side_by_side_diff_view || false; }-*/;
+
+  public final native boolean showOnUnifiedDiffView()
+  /*-{ return this.show_on_unified_diff_view || false; }-*/;
+
+  protected DiffWebLinkInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index 42a8d74..6891b68 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -161,6 +161,7 @@
   String link();
   String linkMenuBar();
   String linkMenuItemNotLast();
+  String linkPanel();
   String maxObjectSizeLimitEffectiveLabel();
   String menuBarUserName();
   String menuBarUserNameAvatar();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
index 5e88ac9..321883d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
@@ -63,6 +63,11 @@
     return this;
   }
 
+  public DiffApi webLinksOnly() {
+    call.addParameter("weblinks-only", true);
+    return this;
+  }
+
   public DiffApi ignoreWhitespace(AccountDiffPreference.Whitespace w) {
     switch (w) {
       default:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
index ff99d7b..8d94438 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
@@ -14,12 +14,18 @@
 
 package com.google.gerrit.client.diff;
 
+import com.google.gerrit.client.DiffWebLinkInfo;
 import com.google.gerrit.client.WebLinkInfo;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.JsArrayString;
 
+import java.util.LinkedList;
+import java.util.List;
+
 public class DiffInfo extends JavaScriptObject {
   public static final String GITLINK = "x-git/gitlink";
   public static final String SYMLINK = "x-git/symlink";
@@ -28,6 +34,33 @@
   public final native FileMeta meta_b() /*-{ return this.meta_b; }-*/;
   public final native JsArrayString diff_header() /*-{ return this.diff_header; }-*/;
   public final native JsArray<Region> content() /*-{ return this.content; }-*/;
+  public final native JsArray<DiffWebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+
+  public final List<WebLinkInfo> side_by_side_web_links() {
+    return filterWebLinks(DiffView.SIDE_BY_SIDE);
+  }
+
+  public final List<WebLinkInfo> unified_web_links() {
+    return filterWebLinks(DiffView.UNIFIED_DIFF);
+  }
+
+  private final List<WebLinkInfo> filterWebLinks(DiffView diffView) {
+    List<WebLinkInfo> filteredDiffWebLinks = new LinkedList<>();
+    List<DiffWebLinkInfo> allDiffWebLinks = Natives.asList(web_links());
+    if (allDiffWebLinks != null) {
+      for (DiffWebLinkInfo webLink : allDiffWebLinks) {
+        if (diffView == DiffView.SIDE_BY_SIDE
+            && webLink.showOnSideBySideDiffView()) {
+          filteredDiffWebLinks.add(webLink);
+        }
+        if (diffView == DiffView.UNIFIED_DIFF
+            && webLink.showOnUnifiedDiffView()) {
+          filteredDiffWebLinks.add(webLink);
+        }
+      }
+    }
+    return filteredDiffWebLinks;
+  }
 
   public final ChangeType change_type() {
     return ChangeType.valueOf(change_typeRaw());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
index f7ae24a..e91a28c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
@@ -211,13 +212,17 @@
     project.setInnerText(info.project());
   }
 
-  void init(PreferencesAction pa, List<InlineHyperlink> links) {
+  void init(PreferencesAction pa, List<InlineHyperlink> links,
+      List<WebLinkInfo> webLinks) {
     prefsAction = pa;
     prefsAction.setPartner(preferences);
 
     for (InlineHyperlink link : links) {
       linkPanel.add(link);
     }
+    for (WebLinkInfo webLink : webLinks) {
+      linkPanel.add(webLink.toAnchor());
+    }
   }
 
   @UiHandler("reviewed")
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
index e86801f..0b38e67 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
@@ -617,7 +617,7 @@
             chunkManager.getLineMapper());
 
     prefsAction = new PreferencesAction(this, prefs);
-    header.init(prefsAction, getLinks());
+    header.init(prefsAction, getLinks(), diff.side_by_side_web_links());
 
     if (prefs.syntaxHighlighting() && fileSize.compareTo(FileSize.SMALL) > 0) {
       Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index ec2b3bb..5fd8fa2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -684,6 +684,10 @@
   font-size: small;
 }
 
+.linkPanel img {
+  padding-right: 3px;
+}
+
 
 /** PatchContentTable **/
 .patchContentTable {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
index 907a7f6..f908c79 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.patches;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.ui.ChangeLink;
@@ -75,7 +76,7 @@
   }
 
   void display(int patchIndex, PatchScreen.Type type, PatchTable fileList,
-      List<InlineHyperlink> links) {
+      List<InlineHyperlink> links, List<WebLinkInfo> webLinks) {
     if (fileList != null) {
       setupNav(Nav.PREV, fileList.getPreviousPatchLink(patchIndex, type));
       setupNav(Nav.NEXT, fileList.getNextPatchLink(patchIndex, type));
@@ -83,10 +84,15 @@
       setupNav(Nav.PREV, null);
       setupNav(Nav.NEXT, null);
     }
+
     FlowPanel linkPanel = new FlowPanel();
+    linkPanel.setStyleName(Gerrit.RESOURCES.css().linkPanel());
     for (InlineHyperlink link : links) {
       linkPanel.add(link);
     }
+    for (WebLinkInfo webLink : webLinks) {
+      linkPanel.add(webLink.toAnchor());
+    }
     table.setWidget(0, 2, linkPanel);
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
index 67db7d1..e333c26 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
@@ -18,9 +18,12 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.RpcStatus;
+import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.changes.CommitMessageBlock;
 import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.diff.DiffApi;
+import com.google.gerrit.client.diff.DiffInfo;
 import com.google.gerrit.client.projects.ConfigInfoCache;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -276,11 +279,25 @@
     }
 
     if (fileList != null) {
-      topNav.display(patchIndex, getPatchScreenType(), fileList, getLinks());
-      bottomNav.display(patchIndex, getPatchScreenType(), fileList, getLinks());
+      displayNav();
     }
   }
 
+  private void displayNav() {
+    DiffApi.diff(idSideB, patchKey.getFileName())
+      .base(idSideA)
+      .webLinksOnly()
+      .get(new GerritCallback<DiffInfo>() {
+        @Override
+        public void onSuccess(DiffInfo diffInfo) {
+          topNav.display(patchIndex, getPatchScreenType(), fileList,
+              getLinks(), getWebLinks(diffInfo));
+          bottomNav.display(patchIndex, getPatchScreenType(), fileList,
+              getLinks(), getWebLinks(diffInfo));
+        }
+      });
+  }
+
   private List<InlineHyperlink> getLinks() {
     if (contentTable instanceof SideBySideTable) {
       InlineHyperlink toUnifiedDiffLink = new InlineHyperlink();
@@ -300,6 +317,17 @@
     }
   }
 
+  private List<WebLinkInfo> getWebLinks(DiffInfo diffInfo) {
+    if (contentTable instanceof SideBySideTable) {
+      return diffInfo.side_by_side_web_links();
+    } else if (contentTable instanceof UnifiedDiffTable) {
+      return diffInfo.unified_web_links();
+    } else {
+      throw new IllegalStateException("unknown table type: "
+          + contentTable.getClass().getSimpleName());
+    }
+  }
+
   private String getSideBySideDiffUrl() {
     StringBuilder url = new StringBuilder();
     url.append("/c/");
@@ -528,8 +556,7 @@
     lastScript = script;
 
     if (fileList != null) {
-      topNav.display(patchIndex, getPatchScreenType(), fileList, getLinks());
-      bottomNav.display(patchIndex, getPatchScreenType(), fileList, getLinks());
+      displayNav();
     }
 
     if (Gerrit.isSignedIn()) {
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 96298eb..588d4d0 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,9 +18,11 @@
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
+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.FileWebLink;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.extensions.webui.ProjectWebLink;
@@ -53,16 +55,19 @@
 
   private final DynamicSet<PatchSetWebLink> patchSetLinks;
   private final DynamicSet<FileWebLink> fileLinks;
+  private final DynamicSet<DiffWebLink> diffLinks;
   private final DynamicSet<ProjectWebLink> projectLinks;
   private final DynamicSet<BranchWebLink> branchLinks;
 
   @Inject
   public WebLinks(DynamicSet<PatchSetWebLink> patchSetLinks,
       DynamicSet<FileWebLink> fileLinks,
+      DynamicSet<DiffWebLink> diffLinks,
       DynamicSet<ProjectWebLink> projectLinks,
       DynamicSet<BranchWebLink> branchLinks) {
     this.patchSetLinks = patchSetLinks;
     this.fileLinks = fileLinks;
+    this.diffLinks = diffLinks;
     this.projectLinks = projectLinks;
     this.branchLinks = branchLinks;
   }
@@ -105,6 +110,34 @@
   /**
    *
    * @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.
+   * @param fileA File name of side A.
+   * @param patchSetIdB Patch set ID of side B.
+   * @param revisionB SHA1 of revision of side B.
+   * @param fileB File name of side B.
+   * @return Links for file diffs.
+   */
+  public FluentIterable<DiffWebLinkInfo> getDiffLinks(final String project, final int changeId,
+      final Integer patchSetIdA, final String revisionA, final String fileA,
+      final int patchSetIdB, final String revisionB, final String fileB) {
+   return FluentIterable
+       .from(diffLinks)
+       .transform(new Function<WebLink, DiffWebLinkInfo>() {
+         @Override
+         public DiffWebLinkInfo apply(WebLink webLink) {
+            return ((DiffWebLink) webLink).getDiffLink(project, changeId,
+                patchSetIdA, revisionA, fileA,
+                patchSetIdB, revisionB, fileB);
+          }
+       })
+       .filter(INVALID_WEBLINK);
+ }
+
+  /**
+   *
+   * @param project Project name.
    * @return Links for projects.
    */
   public FluentIterable<WebLinkInfo> getProjectLinks(final String project) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index bcda2b0..c183d5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
 import com.google.gerrit.common.data.PatchScript.FileMode;
+import com.google.gerrit.extensions.common.DiffWebLinkInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.CacheControl;
@@ -81,6 +82,9 @@
   @Option(name = "--intraline")
   boolean intraline;
 
+  @Option(name = "--weblinks-only")
+  boolean webLinksOnly;
+
   @Inject
   GetDiff(ProjectCache projectCache,
       PatchScriptFactory.Factory patchScriptFactoryFactory,
@@ -148,49 +152,63 @@
           projectCache.get(resource.getRevision().getChange().getProject());
 
       Result result = new Result();
-      if (ps.getDisplayMethodA() != DisplayMethod.NONE) {
-        result.metaA = new FileMeta();
-        result.metaA.name = MoreObjects.firstNonNull(ps.getOldName(),
-            ps.getNewName());
-        setContentType(result.metaA, state, ps.getFileModeA(), ps.getMimeTypeA());
-        result.metaA.lines = ps.getA().size();
+      // TODO referring to the parent commit by refs/changes/12/60012/1^1
+      // will likely not work for inline edits
+      String revA = basePatchSet != null
+          ? basePatchSet.getRefName()
+          : resource.getRevision().getPatchSet().getRefName() + "^1";
+      String revB = resource.getRevision().getEdit().isPresent()
+           ? resource.getRevision().getEdit().get().getRefName()
+           : resource.getRevision().getPatchSet().getRefName();
 
-        // TODO referring to the parent commit by refs/changes/12/60012/1^1
-        // will likely not work for inline edits
-        String rev = basePatchSet != null
-            ? basePatchSet.getRefName()
-            : resource.getRevision().getPatchSet().getRefName() + "^1";
-        result.metaA.webLinks =
-            getFileWebLinks(state.getProject(), rev, result.metaA.name);
-      }
+      FluentIterable<DiffWebLinkInfo> links =
+          webLinks.getDiffLinks(state.getProject().getName(),
+              resource.getPatchKey().getParentKey().getParentKey().get(),
+              basePatchSet != null ? basePatchSet.getId().get() : null,
+              revA,
+              MoreObjects.firstNonNull(ps.getOldName(), ps.getNewName()),
+              resource.getPatchKey().getParentKey().get(),
+              revB,
+              ps.getNewName());
+      result.webLinks = links.isEmpty() ? null : links.toList();
 
-      if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
-        result.metaB = new FileMeta();
-        result.metaB.name = ps.getNewName();
-        setContentType(result.metaB, state, ps.getFileModeB(), ps.getMimeTypeB());
-        result.metaB.lines = ps.getB().size();
-        String rev = resource.getRevision().getEdit().isPresent()
-            ? resource.getRevision().getEdit().get().getRefName()
-            : resource.getRevision().getPatchSet().getRefName();
-        result.metaB.webLinks =
-            getFileWebLinks(state.getProject(), rev, result.metaB.name);
-      }
-
-      if (intraline) {
-        if (ps.hasIntralineTimeout()) {
-          result.intralineStatus = IntraLineStatus.TIMEOUT;
-        } else if (ps.hasIntralineFailure()) {
-          result.intralineStatus = IntraLineStatus.FAILURE;
-        } else {
-          result.intralineStatus = IntraLineStatus.OK;
+      if (!webLinksOnly) {
+        if (ps.getDisplayMethodA() != DisplayMethod.NONE) {
+          result.metaA = new FileMeta();
+          result.metaA.name = MoreObjects.firstNonNull(ps.getOldName(),
+              ps.getNewName());
+          setContentType(result.metaA, state, ps.getFileModeA(), ps.getMimeTypeA());
+          result.metaA.lines = ps.getA().size();
+          result.metaA.webLinks =
+              getFileWebLinks(state.getProject(), revA, result.metaA.name);
         }
+
+        if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
+          result.metaB = new FileMeta();
+          result.metaB.name = ps.getNewName();
+          setContentType(result.metaB, state, ps.getFileModeB(), ps.getMimeTypeB());
+          result.metaB.lines = ps.getB().size();
+          result.metaB.webLinks =
+              getFileWebLinks(state.getProject(), revB, result.metaB.name);
+        }
+
+        if (intraline) {
+          if (ps.hasIntralineTimeout()) {
+            result.intralineStatus = IntraLineStatus.TIMEOUT;
+          } else if (ps.hasIntralineFailure()) {
+            result.intralineStatus = IntraLineStatus.FAILURE;
+          } else {
+            result.intralineStatus = IntraLineStatus.OK;
+          }
+        }
+
+        result.changeType = ps.getChangeType();
+        if (ps.getPatchHeader().size() > 0) {
+          result.diffHeader = ps.getPatchHeader();
+        }
+        result.content = content.lines;
       }
 
-      result.changeType = ps.getChangeType();
-      if (ps.getPatchHeader().size() > 0) {
-        result.diffHeader = ps.getPatchHeader();
-      }
-      result.content = content.lines;
       Response<Result> r = Response.ok(result);
       if (resource.isCacheable()) {
         r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
@@ -217,6 +235,7 @@
     ChangeType changeType;
     List<String> diffHeader;
     List<ContentEntry> content;
+    List<DiffWebLinkInfo> webLinks;
   }
 
   static class FileMeta {
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 02ade49..5c91aed 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
@@ -33,6 +33,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 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.FileWebLink;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.extensions.webui.ProjectWebLink;
@@ -287,6 +288,7 @@
     DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
     DynamicSet.setOf(binder(), PatchSetWebLink.class);
     DynamicSet.setOf(binder(), FileWebLink.class);
+    DynamicSet.setOf(binder(), DiffWebLink.class);
     DynamicSet.setOf(binder(), ProjectWebLink.class);
     DynamicSet.setOf(binder(), BranchWebLink.class);