Hookup a REST API handler to allow manual triggers of SuperManifest.

This allows for the initial population of a superproject, and recovery
for failed runs.

Change-Id: I9b650b6b7729d268b54828c89136e8d40b8d2993
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestModule.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestModule.java
index fd65725..33aa1dd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestModule.java
@@ -14,14 +14,15 @@
 
 package com.googlesource.gerrit.plugins.supermanifest;
 
+import static com.google.gerrit.server.project.BranchResource.BRANCH_KIND;
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.inject.AbstractModule;
+import com.google.gerrit.extensions.restapi.RestApiModule;
 
-public class SuperManifestModule extends AbstractModule {
+public class SuperManifestModule extends RestApiModule {
   SuperManifestModule() {}
 
   @Override
@@ -32,5 +33,6 @@
     DynamicSet.bind(binder(), LifecycleListener.class)
         .to(SuperManifestRefUpdatedListener.class)
         .in(SINGLETON);
+    post(BRANCH_KIND, "update_manifest").to(SuperManifestRefUpdatedListener.class).in(SINGLETON);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java
index ca339ac..1bc9976 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java
@@ -18,17 +18,27 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.BranchResource;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.Closeable;
 import java.io.IOException;
@@ -62,7 +72,9 @@
  */
 @Singleton
 public class SuperManifestRefUpdatedListener
-    implements GitReferenceUpdatedListener, LifecycleListener {
+    implements GitReferenceUpdatedListener,
+        LifecycleListener,
+        RestModifyView<BranchResource, BranchInput> {
   private static final Logger log = LoggerFactory.getLogger(SuperManifestRefUpdatedListener.class);
 
   private final GitRepositoryManager repoManager;
@@ -72,6 +84,8 @@
   private final AllProjectsName allProjectsName;
   private final ProjectCache projectCache;
   private final PersonIdent serverIdent;
+  private final Provider<IdentifiedUser> identifiedUser;
+  private final PermissionBackend permissionBackend;
 
   // Mutable.
   private Set<ConfigEntry> config;
@@ -84,7 +98,10 @@
       PluginConfigFactory cfgFactory,
       ProjectCache projectCache,
       @GerritPersonIdent PersonIdent serverIdent,
-      GitRepositoryManager repoManager) {
+      GitRepositoryManager repoManager,
+      Provider<IdentifiedUser> identifiedUser,
+      PermissionBackend permissionBackend) {
+
     this.pluginName = pluginName;
     this.serverIdent = serverIdent;
     this.allProjectsName = allProjectsName;
@@ -97,6 +114,8 @@
 
     this.cfgFactory = cfgFactory;
     this.projectCache = projectCache;
+    this.identifiedUser = identifiedUser;
+    this.permissionBackend = permissionBackend;
   }
 
   private void warn(String formatStr, Object... args) {
@@ -217,24 +236,48 @@
       }
       return;
     }
+    try {
+      update(event.getProjectName(), event.getRefName(), true);
+    } catch (Exception e) {
+      // no exceptions since we set continueOnError = true.
+    }
+  }
 
+  @Override
+  public Response<?> apply(BranchResource resource, BranchInput input)
+      throws IOException, ConfigInvalidException, GitAPIException, AuthException,
+          PermissionBackendException {
+    permissionBackend.user(identifiedUser).check(GlobalPermission.ADMINISTRATE_SERVER);
+    update(resource.getProjectState().getProject().getName(), resource.getRef(), false);
+    return Response.none();
+  }
+
+  /**
+   * Updates projects in response to update in given project/ref. Only throws exceptions if
+   * continueOnError is false.
+   */
+  private void update(String project, String refName, boolean continueOnError)
+      throws IOException, GitAPIException, ConfigInvalidException {
     for (ConfigEntry c : config) {
-      if (!c.srcRepoKey.get().equals(event.getProjectName())) {
+      if (!c.srcRepoKey.get().equals(project)) {
         continue;
       }
 
-      if (!(c.destBranch.equals("*") || c.srcRef.equals(event.getRefName()))) {
+      if (!(c.destBranch.equals("*") || c.srcRef.equals(refName))) {
         continue;
       }
 
-      if (c.destBranch.equals("*") && !event.getRefName().startsWith(REFS_HEADS)) {
+      if (c.destBranch.equals("*") && !refName.startsWith(REFS_HEADS)) {
         continue;
       }
 
       try {
-        updateForConfig(c, event);
+        updateForConfig(c, refName);
       } catch (ConfigInvalidException | IOException | GitAPIException e) {
-        // We only want the trace up to here. We could recurse into the exception, but this at least
+        if (!continueOnError) {
+          throw e;
+        }
+       // We only want the trace up to here. We could recurse into the exception, but this at least
         // trims the very common jgit.gitrepo.RepoCommand.RemoteUnavailableException.
         StackTraceElement here = Thread.currentThread().getStackTrace()[1];
         e.setStackTrace(trimStack(e.getStackTrace(), here));
@@ -246,12 +289,13 @@
         StringWriter sw = new StringWriter();
         PrintWriter pw = new PrintWriter(sw);
         e.printStackTrace(pw);
-        error("update for %s (ref %s) failed: %s", c.toString(), event.getRefName(), sw);
+        error("update for %s (ref %s) failed: %s", c.toString(), refName, sw);
       }
     }
   }
 
-  private void updateForConfig(ConfigEntry c, Event event) throws ConfigInvalidException, IOException, GitAPIException {
+  private void updateForConfig(ConfigEntry c, String refName)
+      throws ConfigInvalidException, IOException, GitAPIException {
     SubModuleUpdater subModuleUpdater;
     switch (c.getToolType()) {
       case Repo:
@@ -265,7 +309,7 @@
             String.format("invalid toolType: %s", c.getToolType().name()));
     }
     try (GerritRemoteReader reader = new GerritRemoteReader()) {
-      subModuleUpdater.update(reader, c, event.getRefName());
+      subModuleUpdater.update(reader, c, refName);
     }
   }
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/supermanifest/RepoSuperManifestIT.java b/src/test/java/com/googlesource/gerrit/plugins/supermanifest/RepoSuperManifestIT.java
index 6775998..494648c 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/supermanifest/RepoSuperManifestIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/supermanifest/RepoSuperManifestIT.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -166,6 +167,54 @@
     assertThat(branch.file("project3").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
   }
 
+  @Test
+  public void httpEndpoint() throws Exception {
+    setupTestRepos("project");
+
+    // Make sure the manifest exists so the configuration loads successfully.
+    Project.NameKey manifestKey = createProject("manifest");
+    TestRepository<InMemoryRepository> manifestRepo = cloneProject(manifestKey, admin);
+
+    Project.NameKey superKey = createProject("superproject");
+    cloneProject(superKey, admin);
+
+    String remoteXml = "  <remote name=\"origin\" fetch=\"" + canonicalWebUrl.get() + "\" />\n";
+    String defaultXml = "  <default remote=\"origin\" revision=\"refs/heads/master\" />\n";
+    String xml =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            + "<manifest>\n"
+            + remoteXml
+            + defaultXml
+            + "  <project name=\""
+            + testRepoKeys[0].get()
+            + "\" path=\"project1\" />\n"
+            + "</manifest>\n";
+
+    pushFactory
+        .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
+        .to("refs/heads/srcbranch")
+        .assertOkStatus();
+
+    // Push config after XML. Needs a manual trigger to create the destination.
+    pushConfig(
+        "[superproject \""
+            + superKey.get()
+            + ":refs/heads/destbranch\"]\n"
+            + "  srcRepo = "
+            + manifestKey.get()
+            + "\n"
+            + "  srcRef = refs/heads/srcbranch\n"
+            + "  srcPath = default.xml\n");
+
+    RestResponse r = userRestSession.post("/projects/"  + manifestKey + "/branches/srcbranch/update_manifest");
+    r.assertForbidden();
+    r = adminRestSession.post("/projects/"  + manifestKey + "/branches/srcbranch/update_manifest");
+    r.assertNoContent();
+
+    BranchApi branch = gApi.projects().name(superKey.get()).branch("refs/heads/destbranch");
+    assertThat(branch.file("project1").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+  }
+
   private void outer() {
     inner();
   }