Add hook to allow manifest update to be triggered externally

This allow the plugin to generate snapshot manifest even when the
underlying git repositories were updated outside of Gerrit.

(Example: when a project hosted by Gerrit is a mirror of an upstream
project and it is periodically fetch by an external script.  RefUpdated
event is not available from Gerrit in this case.)

Change-Id: Ie119ad393fd51d6aba4679973c0b7d9db3fbde47
diff --git a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/ManifestSubscription.java b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/ManifestSubscription.java
index ed72252..29699e3 100644
--- a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/ManifestSubscription.java
+++ b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/ManifestSubscription.java
@@ -137,7 +137,7 @@
     String projectName = event.getProjectName();
     String refName = event.getRefName();
     String branchName = refName.startsWith("refs/heads/") ?
-        refName.substring(11) : "";
+        refName.substring(11) : refName;
     ProjectBranchKey pbKey = new ProjectBranchKey(projectName, branchName);
 
     if (event.getNewObjectId().equals(ObjectId.zeroId().toString())) {
@@ -154,58 +154,59 @@
 
     } else if (subscribedRepos.containsRow(pbKey)) {
       //updates in subscribed repos
-
-      // Manifest store and branch
-      Map<String, Map<String, Set<
-          com.amd.gerrit.plugins.manifestsubscription.manifest.Project>>>
-          destinations = subscribedRepos.row(pbKey);
-
-      for (String store : destinations.keySet()) {
-        for (String storeBranch : destinations.get(store).keySet()) {
-          Set<com.amd.gerrit.plugins.manifestsubscription.manifest.Project> ps
-              = destinations.get(store).get(storeBranch);
-
-          Manifest manifest = manifestStores.get(store, storeBranch);
-          String manifestSrc = manifestSource.get(store, storeBranch);
-          StringBuilder extraCommitMsg = new StringBuilder();
-
-          Project.NameKey p = new Project.NameKey(projectName);
-          try (Repository r = gitRepoManager.openRepository(p);
-               RevWalk walk = new RevWalk(r)) {
-
-            RevCommit c = walk.parseCommit(
-                ObjectId.fromString(event.getNewObjectId()));
-
-            extraCommitMsg.append(event.getNewObjectId().substring(0,7));
-            extraCommitMsg.append(" ");
-            extraCommitMsg.append(projectName);
-            extraCommitMsg.append(" ");
-            extraCommitMsg.append(c.getShortMessage());
-          } catch (IOException e) {
-            e.printStackTrace();
-          }
-
-          // these are project from the above manifest previously
-          // cached in the lookup table
-          for (com.amd.gerrit.plugins.manifestsubscription.manifest.Project
-              updateProject : ps) {
-            updateProject.setRevision(event.getNewObjectId());
-          }
-
-          try {
-            Utilities.updateManifest(gitRepoManager, metaDataUpdateFactory,
-                changeHooks, store, STORE_BRANCH_PREFIX + storeBranch,
-                manifest, manifestSrc, extraCommitMsg.toString(), null);
-          } catch (JAXBException | IOException e) {
-            e.printStackTrace();
-          }
-
-        }
-      }
-
+      processRepoChange(event.getNewObjectId(), projectName, pbKey);
     }
+  }
 
+  void processRepoChange(String refUpdatedHash, String projectName,
+                                 ProjectBranchKey pbKey) {
+    // Manifest store and branch
+    Map<String, Map<String, Set<
+            com.amd.gerrit.plugins.manifestsubscription.manifest.Project>>>
+        destinations = subscribedRepos.row(pbKey);
 
+    for (String store : destinations.keySet()) {
+      for (String storeBranch : destinations.get(store).keySet()) {
+        Set<com.amd.gerrit.plugins.manifestsubscription.manifest.Project> ps
+            = destinations.get(store).get(storeBranch);
+
+        Manifest manifest = manifestStores.get(store, storeBranch);
+        String manifestSrc = manifestSource.get(store, storeBranch);
+        StringBuilder extraCommitMsg = new StringBuilder();
+
+        Project.NameKey p = new Project.NameKey(projectName);
+        try (Repository r = gitRepoManager.openRepository(p);
+             RevWalk walk = new RevWalk(r)) {
+
+          RevCommit c = walk.parseCommit(
+              ObjectId.fromString(refUpdatedHash));
+
+          extraCommitMsg.append(refUpdatedHash.substring(0,7));
+          extraCommitMsg.append(" ");
+          extraCommitMsg.append(projectName);
+          extraCommitMsg.append(" ");
+          extraCommitMsg.append(c.getShortMessage());
+        } catch (IOException e) {
+          e.printStackTrace();
+        }
+
+        // these are project from the above manifest previously
+        // cached in the lookup table
+        for (com.amd.gerrit.plugins.manifestsubscription.manifest.Project
+            updateProject : ps) {
+          updateProject.setRevision(refUpdatedHash);
+        }
+
+        try {
+          Utilities.updateManifest(gitRepoManager, metaDataUpdateFactory,
+              changeHooks, store, STORE_BRANCH_PREFIX + storeBranch,
+              manifest, manifestSrc, extraCommitMsg.toString(), null);
+        } catch (JAXBException | IOException e) {
+          e.printStackTrace();
+        }
+
+      }
+    }
   }
 
   private void updateProjectRev(String projectName, String branch, String rev,
diff --git a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/SshModule.java b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/SshModule.java
index c514182..3ea815a 100644
--- a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/SshModule.java
+++ b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/SshModule.java
@@ -22,5 +22,6 @@
     command(ShowSubscriptionCommand.class);
     command(BranchManifestCommand.class);
     command(TagManifestCommand.class);
+    command(TriggerManifestUpdateCommand.class);
   }
 }
diff --git a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/TriggerManifestUpdateCommand.java b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/TriggerManifestUpdateCommand.java
new file mode 100644
index 0000000..36445ca
--- /dev/null
+++ b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/TriggerManifestUpdateCommand.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2017 Advanced Micro Devices, Inc. All rights reserved.
+//
+// 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.amd.gerrit.plugins.manifestsubscription;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import org.kohsuke.args4j.Option;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "project-trigger", description = "Trigger snapshot manifest update for externally updated project")
+public class TriggerManifestUpdateCommand extends SshCommand {
+
+  @Inject
+  private ManifestSubscription manifestSubscription;
+
+  @Inject
+  private GitRepositoryManager gitRepositoryManager;
+
+  @Option(name = "-b", aliases = {"--project-branch"},
+          usage = "", required = true)
+  private String projectBranch;
+
+  @Option(name = "-n", aliases = {"--project-name"},
+          usage = "", required = true)
+  private String projectName;
+
+  @Option(name = "-r", aliases = {"--project-hash"},
+          usage = "", required = true)
+  private String projectHash;
+
+  @Override
+  protected void run() {
+      Utilities.triggerManifestUpdate(gitRepositoryManager, manifestSubscription,
+              projectHash, projectName, projectBranch,
+              stdout, false);
+  }
+}
diff --git a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/Utilities.java b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/Utilities.java
index 850507b..92a1a82 100644
--- a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/Utilities.java
+++ b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/Utilities.java
@@ -16,7 +16,6 @@
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableCollection;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableTable;
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.ChangeHooks;
@@ -426,4 +425,52 @@
 
     }
   }
+
+  static void triggerManifestUpdate(GitRepositoryManager gitRepositoryManager,
+                                    ManifestSubscription manifestSubscription,
+                                    String refUpdatedHash, String projectName, String projectBranch,
+                                    Writer output, boolean inJSON) {
+    PrintWriter writer;
+    if (output instanceof PrintWriter) {
+      writer = (PrintWriter) output;
+    } else {
+      writer = new PrintWriter(output);
+    }
+
+    projectBranch = projectBranch.startsWith("refs/heads/") ?
+                      projectBranch.substring(11) : projectBranch;
+
+    if (!refUpdatedHash.matches("[0-9a-fA-F]+")) {
+      writer.println("Invalid project-hash");
+      return;
+    }
+
+    ProjectBranchKey pbKey = new ProjectBranchKey(projectName, projectBranch);
+    if (!manifestSubscription.getSubscribedProjects().contains(pbKey)) {
+      writer.println(
+              String.format("Project '%s' with branch '%s' is not being monitored for manifest update",
+              projectName, projectBranch));
+      return;
+    }
+
+    try (Repository project =
+                 gitRepositoryManager.openRepository(
+                         new Project.NameKey(projectName))) {
+      ObjectId commit = project.resolve(refUpdatedHash);
+      refUpdatedHash = ObjectId.toString(commit);
+    } catch (Exception e) {
+      e.printStackTrace();
+      writer.println(e.toString());
+      writer.println(
+              String.format("Project '%s' with hash '%s' not found.",
+                      projectName, refUpdatedHash));
+      return;
+    }
+
+    manifestSubscription.processRepoChange(refUpdatedHash, projectName, pbKey);
+    writer.println("Manifest update triggered by:");
+    writer.println("  - Project: " + projectName);
+    writer.println("  - Branch: " + projectBranch);
+    writer.println("  - Updated hash: " + refUpdatedHash);
+  }
 }
diff --git a/src/main/resources/Documentation/cmd-project-trigger.md b/src/main/resources/Documentation/cmd-project-trigger.md
new file mode 100644
index 0000000..90c3570
--- /dev/null
+++ b/src/main/resources/Documentation/cmd-project-trigger.md
@@ -0,0 +1,39 @@
+@PLUGIN@ project-trigger
+========================
+
+NAME
+----
+@PLUGIN@ project-trigger - Trigger snapshot manifest update for externally updated project
+
+SYNOPSIS
+--------
+```
+ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ project-trigger
+  {-n/--project-name <updated project name>}
+  {-b/--project-branch <updated project branch>}
+  {-r/--project-hash <git commit hash that was updated to>}
+  [--help]
+```
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group
+
+OPTIONS
+-------
+
+`-n/--project-name <updated project name>`
+`-b/--project-branch <updated project branch>`
+`-r/--project-hash <git commit hash that was updated to>`
+: This specify the project on Gerrit that was updated outside of Gerrit
+(by a pull script directly on the server, for example.)
+
+EXAMPLE
+-------
+```
+ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ project-trigger -n demo/project2 -b master -r 5f51acb585b6a
+```
+
+[Back to @PLUGIN@ documentation index][index]
+
+[index]: index.html
\ No newline at end of file