Merge "Add REST client"
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportCapability.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportCapability.java
new file mode 100644
index 0000000..bb18ef3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportCapability.java
@@ -0,0 +1,26 @@
+// 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.googlesource.gerrit.plugins.importer;
+
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+
+public class ImportCapability extends CapabilityDefinition {
+  public final static String ID = "import";
+
+  @Override
+  public String getDescription() {
+    return "Import";
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/Module.java b/src/main/java/com/googlesource/gerrit/plugins/importer/Module.java
index 48b00d4..b0190a9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/Module.java
@@ -16,17 +16,27 @@
 
 import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
 
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.inject.AbstractModule;
+import com.google.inject.internal.UniqueAnnotations;
 
 class Module extends AbstractModule {
   @Override
   protected void configure() {
+    bind(CapabilityDefinition.class)
+        .annotatedWith(Exports.named(ImportCapability.ID))
+        .to(ImportCapability.class);
     install(new RestApiModule() {
       @Override
       protected void configure() {
         post(CONFIG_KIND, "project").to(ProjectRestEndpoint.class);
       }
     });
+    bind(LifecycleListener.class)
+      .annotatedWith(UniqueAnnotations.create())
+      .to(ProjectRestEndpoint.class);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectCommand.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectCommand.java
index 8319ec9..e140b8a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectCommand.java
@@ -17,6 +17,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -32,6 +33,7 @@
 import java.io.UnsupportedEncodingException;
 import java.util.List;
 
+@RequiresCapability(ImportCapability.ID)
 @CommandMetaData(name = "project", description = "Imports a project")
 public class ProjectCommand extends SshCommand {
   @Option(name = "--from", aliases = {"-f"}, required = true, metaVar = "URL",
@@ -66,12 +68,12 @@
   }
 
   private String readPassword() throws UnsupportedEncodingException,
-      IOException {
+      IOException, UnloggedFailure {
     if ("-".equals(pass)) {
       BufferedReader br = new BufferedReader(new InputStreamReader(in, UTF_8));
       pass = Strings.nullToEmpty(br.readLine());
       if (br.readLine() != null) {
-        die("multi-line password not allowed");
+        throw die("multi-line password not allowed");
       }
     }
     return pass;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectRestEndpoint.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectRestEndpoint.java
index d699c6e..8101e3e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectRestEndpoint.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectRestEndpoint.java
@@ -14,16 +14,45 @@
 
 package com.googlesource.gerrit.plugins.importer;
 
+import static java.lang.String.format;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import com.googlesource.gerrit.plugins.importer.ProjectRestEndpoint.Input;
 
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.internal.storage.file.LockFile;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.FetchResult;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 
+@RequiresCapability(ImportCapability.ID)
 @Singleton
-class ProjectRestEndpoint implements RestModifyView<ConfigResource, Input> {
+class ProjectRestEndpoint implements RestModifyView<ConfigResource, Input>,
+    LifecycleListener {
   public static class Input {
     public String from;
     public String user;
@@ -31,8 +60,157 @@
     public List<String> projects;
   }
 
+  private static class ImportProjectTask implements Runnable  {
+
+    private final GitRepositoryManager git;
+    private final File lockRoot;
+    private final Project.NameKey name;
+    private final CredentialsProvider cp;
+    private final String fromGerrit;
+    private final StringBuffer logger;
+
+    ImportProjectTask(GitRepositoryManager git, File lockRoot,
+        Project.NameKey name, CredentialsProvider cp, String fromGerrit,
+      StringBuffer logger) {
+      this.git = git;
+      this.lockRoot = lockRoot;
+      this.name = name;
+      this.cp = cp;
+      this.fromGerrit = fromGerrit;
+      this.logger = logger;
+    }
+
+    @Override
+    public void run() {
+      LockFile importing = lockForImport(name);
+      if (importing == null) {
+        return;
+      }
+
+      try {
+        try {
+          git.openRepository(name);
+          logger.append(format("Repository %s already exists.", name.get()));
+          return;
+        } catch (RepositoryNotFoundException e) {
+          // Ideal, project doesn't exist
+        } catch (IOException e) {
+          logger.append(e.getMessage());
+          return;
+        }
+
+        Repository repo;
+        try {
+          repo = git.createRepository(name);
+        } catch(IOException e) {
+          logger.append(format("Error: %s, skipping project %s", e, name.get()));
+          return;
+        }
+
+        try {
+          setupProjectConfiguration(fromGerrit, name.get(), repo.getConfig());
+          FetchResult fetchResult = Git.wrap(repo).fetch()
+              .setCredentialsProvider(cp)
+              .setRemote("origin")
+              .call();
+          logger.append(format("[INFO] Project '%s' imported: %s",
+              name.get(), fetchResult.getMessages()));
+        } catch(IOException | GitAPIException e) {
+          logger.append(format("[ERROR] Unable to transfere project '%s' from"
+              + " source gerrit host '%s': %s",
+              name.get(), fromGerrit, e.getMessage()));
+        } finally {
+          repo.close();
+        }
+
+      } finally {
+        importing.unlock();
+        importing.commit();
+      }
+    }
+
+    private LockFile lockForImport(Project.NameKey project) {
+      File importStatus = new File(lockRoot, project.get());
+      LockFile lockFile = new LockFile(importStatus, FS.DETECTED);
+      try {
+        if (lockFile.lock()) {
+          return lockFile;
+        } else {
+          logger.append(format("Project %s is being imported from another session"
+              + ", skipping", name.get()));
+          return null;
+        }
+      } catch (IOException e1) {
+        logger.append(format(
+            "Error while trying to lock the project %s for import", name.get()));
+        return null;
+      }
+    }
+  }
+
+  // TODO: this should go into the plugin configuration.
+  private final static int maxNumberOfImporterThreads = 20;
+
+  private final GitRepositoryManager git;
+  private final WorkQueue queue;
+  private final File data;
+
+  private WorkQueue.Executor executor;
+  private ListeningExecutorService pool;
+
+  @Inject
+  ProjectRestEndpoint(GitRepositoryManager git, WorkQueue queue,
+      @PluginData File data) {
+    this.git = git;
+    this.queue = queue;
+    this.data = data;
+  }
+
   @Override
   public String apply(ConfigResource rsrc, Input input) {
-    return "TODO";
+
+    long startTime = System.currentTimeMillis();
+    StringBuffer result = new StringBuffer();
+    CredentialsProvider cp =
+        new UsernamePasswordCredentialsProvider(input.user, input.pass);
+
+    List<ListenableFuture<?>> tasks = new ArrayList<>();
+    for(String projectName : input.projects) {
+      Project.NameKey name = new Project.NameKey(projectName);
+      Runnable task = new ImportProjectTask(
+          git, data, name, cp, input.from, result);
+      tasks.add(pool.submit(task));
+    }
+    Futures.getUnchecked(Futures.allAsList(tasks));
+    // TODO: the log message below does not take the failed imports into account.
+    result.append(format("[INFO] %d projects imported in %d milliseconds.%n",
+        input.projects.size(), (System.currentTimeMillis() - startTime)));
+    return result.toString();
+  }
+
+  @Override
+  public void start() {
+    executor = queue.createQueue(maxNumberOfImporterThreads, "ProjectImporter");
+    pool = MoreExecutors.listeningDecorator(executor);
+  }
+
+  @Override
+  public void stop() {
+    if (executor != null) {
+      executor.shutdown();
+      executor.unregisterWorkQueue();
+      executor = null;
+      pool = null;
+    }
+  }
+
+  private static void setupProjectConfiguration(String sourceGerritServerUrl,
+      String projectName, StoredConfig config) throws IOException {
+    config.setString("remote", "origin", "url", sourceGerritServerUrl
+        .concat("/")
+        .concat(projectName));
+    config.setString("remote", "origin", "fetch", "+refs/*:refs/*");
+    config.setString("http", null, "sslVerify", Boolean.FALSE.toString());
+    config.save();
   }
 }
diff --git a/src/main/resources/Documentation/cmd-project.md b/src/main/resources/Documentation/cmd-project.md
index dfe6971..a918c4f 100644
--- a/src/main/resources/Documentation/cmd-project.md
+++ b/src/main/resources/Documentation/cmd-project.md
@@ -21,7 +21,9 @@
 
 ACCESS
 ------
-TODO
+Caller must be a member of a group that is granted the 'Import'
+capability (provided by this plugin) or the 'Administrate Server'
+capability.
 
 SCRIPTING
 ---------
diff --git a/src/main/resources/Documentation/rest-api-config.md b/src/main/resources/Documentation/rest-api-config.md
index e7df0ff..b00c1b3 100644
--- a/src/main/resources/Documentation/rest-api-config.md
+++ b/src/main/resources/Documentation/rest-api-config.md
@@ -18,6 +18,10 @@
 The information about which project should be imported must be provided
 in the request body as a [ProjectInput](#project-input) entity.
 
+Caller must be a member of a group that is granted the 'Import'
+capability (provided by this plugin) or the 'Administrate Server'
+capability.
+
 #### Request
 
 ```