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
```