Add extension point for the 'Included In' drop-down panel

The new extension point allows plugins to add additional rows in the
'Included In' drop-down panel on the change screen.

E.g. if there is a fixed set of production servers a plugin can show
on which servers this change was deployed. To provide this information
the plugin could e.g. ask each production server which tag it runs and
then include the name of this server if the change is included in this
tag. Being able to immediately see on which production servers a
change is running is very useful for users to know if a bug-fix has
reached a certain server or which servers are effected by a bad
change.

Change-Id: I93ba27fe84e6830f485b5117afe9c951e8dc885a
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 9bb0007..a86c1fd 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1777,6 +1777,18 @@
 The download schemes and download commands which are used most often
 are provided by the Gerrit core plugin `download-commands`.
 
+[[included-in]]
+== Included In
+
+For merged changes the link:user-review-ui.html#included-in[Included In]
+drop-down panel shows the branches and tags in which the change is
+included.
+
+Plugins can add additional systems in which the change can be included
+by implementing `com.google.gerrit.extensions.config.ExternalIncludedIn`,
+e.g. a plugin can provide a list of servers on which the change was
+deployed.
+
 [[links-to-external-tools]]
 == Links To External Tools
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 138f85f..97169ab 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -3912,14 +3912,17 @@
 The `IncludedInInfo` entity contains information about the branches a
 change was merged into and tags it was tagged with.
 
-[options="header",cols="1,6"]
-|==========================
-|Field Name |Description
-|`branches` | The list of branches this change was merged into.
+[options="header",cols="1,^1,5"]
+|=======================
+|Field Name||Description
+|`branches`||The list of branches this change was merged into.
 Each branch is listed without the 'refs/head/' prefix.
-|`tags`     | The list of tags this change was tagged with.
+|`tags`    ||The list of tags this change was tagged with.
 Each tag is listed without the 'refs/tags/' prefix.
-|==========================
+|`external`|optional|A map that maps a name to a list of external
+systems that include this change, e.g. a list of servers on which this
+change is deployed.
+|=======================
 
 [[label-info]]
 === LabelInfo
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java
new file mode 100644
index 0000000..072799f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java
@@ -0,0 +1,41 @@
+// 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.config;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.util.Collection;
+import java.util.List;
+
+@ExtensionPoint
+public interface ExternalIncludedIn {
+
+  /**
+   * Returns a list of systems that include the given commit.
+   *
+   * The tags and branches in which the commit is included are provided so that
+   * a RevWalk can be avoided when a system runs a certain tag or branch.
+   *
+   * @param project the name of the project
+   * @param commit the ID of the commit for which it should be checked if it is
+   *        included
+   * @param tags the tags that include the commit
+   * @param branches the branches that include the commit
+   * @return a list of systems that contain the given commit, e.g. names of
+   *         servers on which this commit is deployed
+   */
+  List<String> getIncludedIn(String project, String commit,
+      Collection<String> tags, Collection<String> branches);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java
index 927e500..74c187e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java
@@ -19,7 +19,11 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.dom.client.TableElement;
+import com.google.gwt.dom.client.TableRowElement;
 import com.google.gwt.resources.client.CssResource;
 import com.google.gwt.safehtml.shared.SafeHtml;
 import com.google.gwt.uibinder.client.UiBinder;
@@ -41,6 +45,7 @@
   private boolean loaded;
 
   @UiField Style style;
+  @UiField TableElement table;
   @UiField Element branches;
   @UiField Element tags;
 
@@ -58,6 +63,12 @@
         public void onSuccess(IncludedInInfo r) {
           branches.setInnerSafeHtml(formatList(r.branches()));
           tags.setInnerSafeHtml(formatList(r.tags()));
+          for (String n : r.externalNames()) {
+            JsArrayString external = r.external(n);
+            if (external.length() > 0) {
+              appendRow(n, external);
+            }
+          }
           loaded = true;
         }
 
@@ -82,4 +93,12 @@
     }
     return html;
   }
+
+  private void appendRow(String title, JsArrayString l) {
+    TableRowElement row = table.insertRow(-1);
+    TableCellElement th = Document.get().createTHElement();
+    th.setInnerText(title);
+    row.appendChild(th);
+    row.insertCell(-1).setInnerSafeHtml(formatList(l));
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
index e59420c..36ac734 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
@@ -63,7 +63,7 @@
     }
   </ui:style>
   <g:HTMLPanel styleName='{style.includedInBox}'>
-    <table class='{style.includedInTable}'>
+    <table class='{style.includedInTable}' ui:field='table'>
       <tr>
         <th><ui:msg>Branches</ui:msg></th>
           <td ui:field='branches'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
index 9052fef..d4f4421 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -423,8 +423,14 @@
   }
 
   public static class IncludedInInfo extends JavaScriptObject {
+    public final Set<String> externalNames() {
+      return Natives.keys(external());
+    }
+
     public final native JsArrayString branches() /*-{ return this.branches; }-*/;
     public final native JsArrayString tags() /*-{ return this.tags; }-*/;
+    public final native JsArrayString external(String n) /*-{ return this.external[n]; }-*/;
+    private final native NativeMap<JsArrayString> external() /*-{ return this.external; }-*/;
 
     protected IncludedInInfo() {
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
index 7e9bb14..61b9545 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.common.data.IncludedInDetail;
+import com.google.gerrit.extensions.config.ExternalIncludedIn;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -37,17 +39,23 @@
 
 import java.io.IOException;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
 
 @Singleton
 class IncludedIn implements RestReadView<ChangeResource> {
 
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager repoManager;
+  private final DynamicMap<ExternalIncludedIn> includedIn;
 
   @Inject
-  IncludedIn(Provider<ReviewDb> db, GitRepositoryManager repoManager) {
+  IncludedIn(Provider<ReviewDb> db,
+      GitRepositoryManager repoManager,
+      DynamicMap<ExternalIncludedIn> includedIn) {
     this.db = db;
     this.repoManager = repoManager;
+    this.includedIn = includedIn;
   }
 
   @Override
@@ -68,17 +76,27 @@
       } catch (MissingObjectException err) {
         throw new ResourceConflictException(err.getMessage());
       }
-      return new IncludedInInfo(IncludedInResolver.resolve(r, rw, rev));
+
+      IncludedInDetail d = IncludedInResolver.resolve(r, rw, rev);
+      Map<String, Collection<String>> external = new HashMap<>();
+      for (DynamicMap.Entry<ExternalIncludedIn> i : includedIn) {
+        external.put(i.getExportName(),
+            i.getProvider().get().getIncludedIn(
+                project.get(), rev.name(), d.getTags(), d.getBranches()));
+      }
+      return new IncludedInInfo(d, (!external.isEmpty() ? external : null));
     }
   }
 
   static class IncludedInInfo {
     Collection<String> branches;
     Collection<String> tags;
+    Map<String, Collection<String>> external;
 
-    IncludedInInfo(IncludedInDetail in) {
+    IncludedInInfo(IncludedInDetail in, Map<String, Collection<String>> e) {
       branches = in.getBranches();
       tags = in.getTags();
+      external = e;
     }
   }
 }
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 8e9d7e8..7f7a1a4 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
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
+import com.google.gerrit.extensions.config.ExternalIncludedIn;
 import com.google.gerrit.extensions.events.GarbageCollectorListener;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
@@ -274,6 +275,7 @@
     DynamicSet.setOf(binder(), MessageOfTheDay.class);
     DynamicMap.mapOf(binder(), DownloadScheme.class);
     DynamicMap.mapOf(binder(), DownloadCommand.class);
+    DynamicMap.mapOf(binder(), ExternalIncludedIn.class);
     DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
     DynamicSet.setOf(binder(), PatchSetWebLink.class);
     DynamicSet.setOf(binder(), FileWebLink.class);