Merge "Allow import log to be configured through external log4j.properties"
diff --git a/BUCK b/BUCK
index 8f8013f..63446b4 100644
--- a/BUCK
+++ b/BUCK
@@ -20,6 +20,7 @@
'Gerrit-ApiVersion: 2.11-SNAPSHOT',
'Gerrit-Module: com.googlesource.gerrit.plugins.importer.Module',
'Gerrit-SshModule: com.googlesource.gerrit.plugins.importer.SshModule',
+ 'Gerrit-HttpModule: com.googlesource.gerrit.plugins.importer.HttpModule',
],
deps = [
HTTP_LIB,
diff --git a/pom.xml b/pom.xml
index 0870deb..2698efb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,6 +41,7 @@
<Gerrit-PluginName>importer</Gerrit-PluginName>
<Gerrit-Module>com.googlesource.gerrit.plugins.importer.Module</Gerrit-Module>
<Gerrit-SshModule>com.googlesource.gerrit.plugins.importer.SshModule</Gerrit-SshModule>
+ <Gerrit-HttpModule>com.googlesource.gerrit.plugins.importer.HttpModule</Gerrit-HttpModule>
<Implementation-Vendor>Gerrit Code Review</Implementation-Vendor>
<Implementation-URL>http://code.google.com/p/gerrit/</Implementation-URL>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/AccountUtil.java b/src/main/java/com/googlesource/gerrit/plugins/importer/AccountUtil.java
index c5c5f42..341081f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/AccountUtil.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/AccountUtil.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/AddApprovalsStep.java b/src/main/java/com/googlesource/gerrit/plugins/importer/AddApprovalsStep.java
index 7a71153..f5afd95 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/AddApprovalsStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/AddApprovalsStep.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
@@ -40,7 +40,7 @@
class AddApprovalsStep {
interface Factory {
- AddApprovalsStep create(Change change, ChangeInfo changeInfo);
+ AddApprovalsStep create(Change change, ChangeInfo changeInfo, boolean resume);
}
private final AccountUtil accountUtil;
@@ -50,6 +50,7 @@
private final ChangeControl.GenericFactory changeControlFactory;
private final Change change;
private final ChangeInfo changeInfo;
+ private final boolean resume;
@Inject
public AddApprovalsStep(AccountUtil accountUtil,
@@ -58,7 +59,8 @@
IdentifiedUser.GenericFactory genericUserFactory,
ChangeControl.GenericFactory changeControlFactory,
@Assisted Change change,
- @Assisted ChangeInfo changeInfo) {
+ @Assisted ChangeInfo changeInfo,
+ @Assisted boolean resume) {
this.accountUtil = accountUtil;
this.updateFactory = updateFactory;
this.db = db;
@@ -66,10 +68,16 @@
this.changeControlFactory = changeControlFactory;
this.change = change;
this.changeInfo = changeInfo;
+ this.resume = resume;
}
void add() throws OrmException, NoSuchChangeException, IOException,
NoSuchAccountException {
+ if (resume) {
+ db.patchSetApprovals().delete(
+ db.patchSetApprovals().byChange(change.getId()));
+ }
+
List<PatchSetApproval> approvals = new ArrayList<>();
for (Entry<String, LabelInfo> e : changeInfo.labels.entrySet()) {
String labelName = e.getKey();
@@ -83,12 +91,16 @@
new PatchSetApproval.Key(change.currentPatchSetId(), user,
labelType.getLabelId()), a.value.shortValue(), a.date));
ChangeUpdate update = updateFactory.create(ctrl);
- update.putApproval(labelName, a.value.shortValue());
+ if (a.value != 0) {
+ update.putApproval(labelName, a.value.shortValue());
+ } else {
+ update.removeApproval(labelName);
+ }
update.commit();
}
}
}
- db.patchSetApprovals().upsert(approvals);
+ db.patchSetApprovals().insert(approvals);
}
private ChangeControl control(Change change, AccountInfo acc)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/AddHashtagsStep.java b/src/main/java/com/googlesource/gerrit/plugins/importer/AddHashtagsStep.java
index e0b668d..e40fc5e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/AddHashtagsStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/AddHashtagsStep.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
@@ -35,7 +35,7 @@
class AddHashtagsStep {
interface Factory {
- AddHashtagsStep create(Change change, ChangeInfo changeInfo);
+ AddHashtagsStep create(Change change, ChangeInfo changeInfo, boolean resume);
}
private final HashtagsUtil hashtagsUtil;
@@ -43,26 +43,36 @@
private final ChangeControl.GenericFactory changeControlFactory;
private final Change change;
private final ChangeInfo changeInfo;
+ private final boolean resume;
@Inject
AddHashtagsStep(HashtagsUtil hashtagsUtil,
IdentifiedUser.GenericFactory genericUserFactory,
ChangeControl.GenericFactory changeControlFactory,
@Assisted Change change,
- @Assisted ChangeInfo changeInfo) {
+ @Assisted ChangeInfo changeInfo,
+ @Assisted boolean resume) {
this.hashtagsUtil = hashtagsUtil;
this.change = change;
this.changeInfo = changeInfo;
this.genericUserFactory = genericUserFactory;
this.changeControlFactory = changeControlFactory;
+ this.resume = resume;
}
void add() throws IllegalArgumentException, AuthException, IOException,
ValidationException, OrmException, NoSuchChangeException {
+ ChangeControl ctrl = control(change, changeInfo.owner);
+
+ if (resume) {
+ HashtagsInput input = new HashtagsInput();
+ input.remove = ctrl.getNotes().load().getHashtags();
+ hashtagsUtil.setHashtags(ctrl, input, false, false);
+ }
+
HashtagsInput input = new HashtagsInput();
input.add = new HashSet<>(changeInfo.hashtags);
- hashtagsUtil.setHashtags(control(change, changeInfo.owner),
- input, false, false);
+ hashtagsUtil.setHashtags(ctrl, input, false, false);
}
private ChangeControl control(Change change, AccountInfo acc)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ConfigureProjectStep.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ConfigureProjectStep.java
index 787ae3b..6aefa97 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ConfigureProjectStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ConfigureProjectStep.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ConfigureRepositoryStep.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ConfigureRepositoryStep.java
index ff35988..f3f4bef 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ConfigureRepositoryStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ConfigureRepositoryStep.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/CopyProject.java b/src/main/java/com/googlesource/gerrit/plugins/importer/CopyProject.java
new file mode 100644
index 0000000..eba963c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/CopyProject.java
@@ -0,0 +1,105 @@
+// 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.common.base.Strings;
+import com.google.gerrit.common.errors.NoSuchAccountException;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import com.googlesource.gerrit.plugins.importer.CopyProject.Input;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+
+import java.io.IOException;
+
+@Singleton
+@RequiresCapability(CopyProjectCapability.ID)
+class CopyProject implements RestModifyView<ProjectResource, Input>,
+ UiAction<ProjectResource> {
+ public static class Input {
+ public String name;
+ }
+
+ private final ImportProject.Factory importProjectFactory;
+ private final String canonicalWebUrl;
+ private final Provider<CurrentUser> currentUserProvider;
+ private final String pluginName;
+
+ @Inject
+ CopyProject(
+ ImportProject.Factory importProjectFactory,
+ @CanonicalWebUrl String canonicalWebUrl,
+ Provider<CurrentUser> currentUserProvider,
+ @PluginName String pluginName) {
+ this.importProjectFactory = importProjectFactory;
+ this.canonicalWebUrl = canonicalWebUrl;
+ this.currentUserProvider = currentUserProvider;
+ this.pluginName = pluginName;
+ }
+
+ @Override
+ public Response<String> apply(ProjectResource rsrc, Input input)
+ throws RestApiException, OrmException, IOException, ValidationException,
+ GitAPIException, NoSuchChangeException, NoSuchAccountException {
+ if (Strings.isNullOrEmpty(input.name)) {
+ throw new BadRequestException("name is required");
+ }
+
+ AccountState s = ((IdentifiedUser) currentUserProvider.get()).state();
+
+ ImportProject.Input in = new ImportProject.Input();
+ in.from = canonicalWebUrl;
+ in.name = rsrc.getName();
+ in.user = s.getUserName();
+ in.pass = s.getPassword(s.getUserName());
+
+ return importProjectFactory.create(new Project.NameKey(input.name))
+ .apply(new ConfigResource(), in);
+ }
+
+ @Override
+ public UiAction.Description getDescription(ProjectResource rsrc) {
+ return new UiAction.Description()
+ .setLabel("Copy...")
+ .setTitle(String.format("Copy project %s", rsrc.getName()))
+ .setVisible(canCopy(rsrc));
+ }
+
+ private boolean canCopy(ProjectResource rsrc) {
+ CapabilityControl ctl = currentUserProvider.get().getCapabilities();
+ return ctl.canAdministrateServer()
+ || ctl.canPerform(pluginName + "-" + CopyProjectCapability.ID);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/CopyProjectCapability.java b/src/main/java/com/googlesource/gerrit/plugins/importer/CopyProjectCapability.java
new file mode 100644
index 0000000..fe75fa6
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/CopyProjectCapability.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 CopyProjectCapability extends CapabilityDefinition {
+ public final static String ID = "copyProject";
+
+ @Override
+ public String getDescription() {
+ return "Copy Project";
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/GitFetchStep.java b/src/main/java/com/googlesource/gerrit/plugins/importer/GitFetchStep.java
index 22404a4..be89bf3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/GitFetchStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/GitFetchStep.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
@@ -54,7 +54,11 @@
Map<String, Ref> refs = repo.getRefDatabase().getRefs(
ConfigureRepositoryStep.R_IMPORTS);
for (Map.Entry<String, Ref> e : refs.entrySet()) {
- if (e.getKey().startsWith("changes/")) {
+ String name = e.getKey();
+ if (name.startsWith("changes/")) {
+ continue;
+ }
+ if (name.startsWith("users/") && name.contains("/edit")) {
continue;
}
String targetRef = Constants.R_REFS + e.getKey();
@@ -65,6 +69,7 @@
case NEW:
case FAST_FORWARD:
case FORCED:
+ case NO_CHANGE:
break;
default:
throw new IOException(String.format(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/importer/HttpModule.java
new file mode 100644
index 0000000..e5b6ce5
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/HttpModule.java
@@ -0,0 +1,30 @@
+// 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.registration.DynamicSet;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
+import com.google.gerrit.extensions.webui.WebUiPlugin;
+import com.google.gerrit.httpd.plugins.HttpPluginModule;
+
+public class HttpModule extends HttpPluginModule {
+ @Override
+ protected void configureServlets() {
+ DynamicSet.bind(binder(), WebUiPlugin.class)
+ .toInstance(new JavaScriptPlugin("copy-project.js"));
+ DynamicSet.bind(binder(), WebUiPlugin.class)
+ .toInstance(new JavaScriptPlugin("resume-copy-project.js"));
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/HttpResponse.java b/src/main/java/com/googlesource/gerrit/plugins/importer/HttpResponse.java
index 4cd58dd..34786d3 100755
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/HttpResponse.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/HttpResponse.java
@@ -16,6 +16,7 @@
import com.google.common.base.Preconditions;
+import org.apache.http.client.methods.CloseableHttpResponse;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
@@ -24,12 +25,12 @@
import java.io.Reader;
import java.nio.ByteBuffer;
-public class HttpResponse {
+public class HttpResponse implements AutoCloseable {
- protected org.apache.http.HttpResponse response;
+ protected CloseableHttpResponse response;
protected Reader reader;
- HttpResponse(org.apache.http.HttpResponse response) {
+ HttpResponse(CloseableHttpResponse response) {
this.response = response;
}
@@ -40,10 +41,15 @@
return reader;
}
- public void consume() throws IllegalStateException, IOException {
- Reader reader = getReader();
- if (reader != null) {
- while (reader.read() != -1);
+ @Override
+ public void close() throws IOException {
+ try {
+ Reader reader = getReader();
+ if (reader != null) {
+ while (reader.read() != -1);
+ }
+ } finally {
+ response.close();
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/HttpSession.java b/src/main/java/com/googlesource/gerrit/plugins/importer/HttpSession.java
index 31329b3..cf49557 100755
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/HttpSession.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/HttpSession.java
@@ -18,11 +18,11 @@
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
-import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import java.io.IOException;
@@ -43,7 +43,7 @@
protected final String url;
private final String user;
private final String pass;
- private HttpClient client;
+ private CloseableHttpClient client;
public HttpSession(String url, String user, String pass) {
this.url = CharMatcher.is('/').trimTrailingFrom(url);
@@ -56,7 +56,7 @@
return new HttpResponse(getClient().execute(get));
}
- protected HttpClient getClient() throws IOException {
+ protected CloseableHttpClient getClient() throws IOException {
if (client == null) {
URI uri = URI.create(url);
BasicCredentialsProvider creds = new BasicCredentialsProvider();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportInfo.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportInfo.java
index ce64c60..16be1c6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportInfo.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportInfo.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportJson.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportJson.java
index 9e63a37..f5cd59a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportJson.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportJson.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
@@ -54,22 +54,29 @@
this.accountLoaderFactory = accountLoaderFactory;
}
- public ImportProjectInfo format(Input input) throws OrmException {
- ImportProjectInfo info = new ImportProjectInfo();
- info.from = input.from;
- info.parent = input.parent;
- info.imports = new ArrayList<>();
+ public ImportProjectInfo format(Input input, ImportProjectInfo info)
+ throws OrmException {
+ if (info == null) {
+ info = new ImportProjectInfo();
+ info.from = input.from;
+ info.name = input.name;
+ info.parent = input.parent;
+ info.imports = new ArrayList<>();
+ }
+ info.imports.add(createImportInfo(input.user));
+ return info;
+ }
+
+ private ImportInfo createImportInfo(String remoteUser) throws OrmException {
AccountLoader accountLoader = accountLoaderFactory.create(true);
ImportInfo importInfo = new ImportInfo();
importInfo.timestamp = new Timestamp(TimeUtil.nowMs());
importInfo.user =
accountLoader.get(((IdentifiedUser) currentUser.get()).getAccountId());
- importInfo.remoteUser = input.user;
- info.imports.add(importInfo);
+ importInfo.remoteUser = remoteUser;
accountLoader.fill();
-
- return info;
+ return importInfo;
}
public static void persist(LockFile lockFile, ImportProjectInfo info,
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportLog.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportLog.java
index cf0da0f..9a1ef18 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportLog.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportLog.java
@@ -41,7 +41,8 @@
public static String ACCOUNT_ID = "accountId";
public static String USER_NAME = "userName";
public static String FROM = "from";
- public static String PROJECT_NAME = "projectName";
+ public static String SRC_PROJECT_NAME = "srcProjectName";
+ public static String TARGET_PROJECT_NAME = "targetProjectName";
public static String ERROR = "error";
private final SystemLog systemLog;
@@ -58,12 +59,13 @@
this.auditService = auditService;
}
- public void onImport(IdentifiedUser user, Project.NameKey project, String from) {
- onImport(user, project, from, null);
+ public void onImport(IdentifiedUser user, Project.NameKey srcProject,
+ Project.NameKey targetProject, String from) {
+ onImport(user, srcProject, targetProject, from, null);
}
- public void onImport(IdentifiedUser user, Project.NameKey project,
- String from, Exception ex) {
+ public void onImport(IdentifiedUser user, Project.NameKey srcProject,
+ Project.NameKey targetProject, String from, Exception ex) {
long ts = TimeUtil.nowMs();
LoggingEvent event = new LoggingEvent( //
Logger.class.getName(), // fqnOfCategoryClass
@@ -85,7 +87,8 @@
event.setProperty(ACCOUNT_ID, user.getAccountId().toString());
event.setProperty(USER_NAME, user.getUserName());
event.setProperty(FROM, from);
- event.setProperty(PROJECT_NAME, project.get());
+ event.setProperty(SRC_PROJECT_NAME, srcProject.get());
+ event.setProperty(TARGET_PROJECT_NAME, targetProject.get());
if (ex != null) {
event.setProperty(ERROR, ex.toString());
@@ -93,7 +96,7 @@
log.callAppenders(event);
- audit(user, ts, project, from, ex);
+ audit(user, ts, srcProject, from, ex);
}
private void audit(IdentifiedUser user, long ts, Project.NameKey project,
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportLogLayout.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportLogLayout.java
index 8dc1bf0..cc29340 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportLogLayout.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportLogLayout.java
@@ -43,12 +43,12 @@
*
* A successful project import will result in a log entry like this:
* [2015-03-05 09:13:28,912 +0100] INFO 1000000 admin OK \
- * https://some-gerrit-server:8080 myProject
+ * https://some-gerrit-server:8080 srcName targetName
*
* The log entry for a failed project import will look like this:
* [2015-03-05 12:14:30,180 +0100] ERROR 1000000 admin FAIL \
- * https://some-gerrit-server:8080 myProject com.google.gwtorm.server.OrmException: \
- * Failed to access the database
+ * https://some-gerrit-server:8080 srcName targetName \
+ * com.google.gwtorm.server.OrmException: Failed to access the database
*/
@Override
public String format(LoggingEvent event) {
@@ -70,7 +70,8 @@
buf.append(event.getMessage());
req(ImportLog.FROM, buf, event);
- req(ImportLog.PROJECT_NAME, buf, event);
+ req(ImportLog.SRC_PROJECT_NAME, buf, event);
+ req(ImportLog.TARGET_PROJECT_NAME, buf, event);
opt(ImportLog.ERROR, buf, event);
buf.append('\n');
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProject.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProject.java
index 59c43fe..b8ef8dd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProject.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
@@ -22,7 +22,6 @@
import com.google.gerrit.extensions.annotations.PluginData;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -42,7 +41,6 @@
import com.googlesource.gerrit.plugins.importer.ImportProject.Input;
-import org.apache.commons.validator.routines.UrlValidator;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.internal.storage.file.LockFile;
import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -61,13 +59,26 @@
class ImportProject implements RestModifyView<ConfigResource, Input> {
public static class Input {
public String from;
+ public String name;
public String user;
public String pass;
public String parent;
+
+ private void validate() throws BadRequestException {
+ if (Strings.isNullOrEmpty(from)) {
+ throw new BadRequestException("from is required");
+ }
+ if (Strings.isNullOrEmpty(user)) {
+ throw new BadRequestException("user is required");
+ }
+ if (Strings.isNullOrEmpty(pass)) {
+ throw new BadRequestException("pass is required");
+ }
+ }
}
interface Factory {
- ImportProject create(Project.NameKey project);
+ ImportProject create(Project.NameKey targetProject);
}
private static Logger log = LoggerFactory.getLogger(ImportProject.class);
@@ -83,10 +94,11 @@
private final ImportLog importLog;
private final File lockRoot;
- private final Project.NameKey project;
+ private final Project.NameKey targetProject;
+ private Project.NameKey srcProject;
private Project.NameKey parent;
- private Writer err = null;
+ private Writer err;
@Inject
ImportProject(
@@ -100,7 +112,7 @@
ImportJson importJson,
ImportLog importLog,
@PluginData File data,
- @Assisted Project.NameKey project) {
+ @Assisted Project.NameKey targetProject) {
this.projectCache = projectCache;
this.openRepoStep = openRepoStep;
this.configRepoStep = configRepoStep;
@@ -111,12 +123,13 @@
this.importJson = importJson;
this.importLog = importLog;
this.lockRoot = data;
- this.project = project;
+ this.targetProject = targetProject;
}
void setErr(Writer err) {
this.err = err;
}
+
@Override
public Response<String> apply(ConfigResource rsrc, Input input)
throws RestApiException, OrmException, IOException, ValidationException,
@@ -124,22 +137,10 @@
if (input == null) {
input = new Input();
}
- if (Strings.isNullOrEmpty(input.from)) {
- throw new BadRequestException("from is required");
- }
- if (!(new UrlValidator(UrlValidator.ALLOW_LOCAL_URLS)).isValid(input.from)) {
- throw new BadRequestException("from must be a valid URL");
- }
- if (Strings.isNullOrEmpty(input.user)) {
- throw new BadRequestException("user is required");
- }
- if (Strings.isNullOrEmpty(input.pass)) {
- throw new BadRequestException("pass is required");
- }
- LockFile lockFile = lockForImport(project);
+ LockFile lockFile = lockForImport();
try {
- return apply(lockFile, input, false);
+ return apply(lockFile, input, null);
} finally {
lockFile.unlock();
}
@@ -148,7 +149,7 @@
public Response<String> resume(String user, String pass, File importStatus)
throws RestApiException, OrmException, IOException, ValidationException,
GitAPIException, NoSuchChangeException, NoSuchAccountException {
- LockFile lockFile = lockForImport(project);
+ LockFile lockFile = lockForImport();
try {
ImportProjectInfo info = ImportJson.parse(importStatus);
@@ -156,53 +157,65 @@
input.user = user;
input.pass = pass;
input.from = info.from;
+ input.name = info.name;
input.parent = info.parent;
- return apply(lockFile, input, true);
+ return apply(lockFile, input, info);
} finally {
lockFile.unlock();
}
}
- private Response<String> apply(LockFile lockFile, Input input, boolean resume)
+ private Response<String> apply(LockFile lockFile, Input input, ImportProjectInfo info)
throws RestApiException, OrmException, IOException, ValidationException,
GitAPIException, NoSuchChangeException, NoSuchAccountException {
- if (resume) {
- throw new NotImplementedException();
- }
+ boolean resume = info != null;
+
+ input.validate();
ProgressMonitor pm = err != null ? new TextProgressMonitor(err) :
NullProgressMonitor.INSTANCE;
try {
+ srcProject = !Strings.isNullOrEmpty(input.name)
+ ? new Project.NameKey(input.name) : targetProject;
+ checkProjectInSource(input, pm);
setParentProjectName(input, pm);
checkPreconditions(pm);
- Repository repo = openRepoStep.open(project, pm);
+ Repository repo = openRepoStep.open(targetProject, resume, pm);
try {
- ImportJson.persist(lockFile, importJson.format(input), pm);
- configRepoStep.configure(repo, project, input.from, pm);
+ ImportJson.persist(lockFile, importJson.format(input, info), pm);
+ configRepoStep.configure(repo, srcProject, input.from, pm);
gitFetchStep.fetch(input.user, input.pass, repo, pm);
- configProjectStep.configure(project, parent, pm);
+ configProjectStep.configure(targetProject, parent, pm);
replayChangesFactory.create(input.from, input.user, input.pass, repo,
- project, pm)
- .replay();
+ srcProject, targetProject, resume, pm).replay();
} finally {
repo.close();
}
- importLog.onImport((IdentifiedUser) currentUser.get(), project,
- input.from);
+ importLog.onImport((IdentifiedUser) currentUser.get(), srcProject,
+ targetProject, input.from);
+ } catch (BadRequestException e) {
+ throw e;
} catch (Exception e) {
- importLog.onImport((IdentifiedUser) currentUser.get(), project,
- input.from, e);
+ importLog.onImport((IdentifiedUser) currentUser.get(), srcProject,
+ targetProject, input.from, e);
log.error(format("Unable to transfer project '%s' from"
+ " source gerrit host '%s'.",
- project.get(), input.from), e);
+ srcProject.get(), input.from), e);
throw e;
}
return Response.<String> ok("OK");
}
+ private void checkProjectInSource(Input input, ProgressMonitor pm)
+ throws IOException, BadRequestException {
+ pm.beginTask("Check source project", 1);
+ new RemoteApi(input.from, input.user, input.pass).getProject(srcProject.get());
+ updateAndEnd(pm);
+ }
+
private void setParentProjectName(Input input, ProgressMonitor pm)
throws IOException, BadRequestException {
pm.beginTask("Set parent project", 1);
@@ -212,25 +225,24 @@
} else {
parent = new Project.NameKey(
new RemoteApi(input.from, input.user, input.pass)
- .getProject(project.get()).parent);
+ .getProject(srcProject.get()).parent);
}
}
updateAndEnd(pm);
}
- private void checkPreconditions(ProgressMonitor pm) throws ValidationException {
+ private void checkPreconditions(ProgressMonitor pm) throws BadRequestException {
pm.beginTask("Check preconditions", 1);
ProjectState p = projectCache.get(parent);
if (p == null) {
- throw new ValidationException(format(
- "Parent project %s does not exist in target,", parent.get()));
+ throw new BadRequestException(format(
+ "Parent project '%s' does not exist in target.", parent.get()));
}
updateAndEnd(pm);
}
- private LockFile lockForImport(Project.NameKey project)
- throws ResourceConflictException {
- File importStatus = new File(lockRoot, project.get());
+ private LockFile lockForImport() throws ResourceConflictException {
+ File importStatus = new File(lockRoot, targetProject.get());
LockFile lockFile = new LockFile(importStatus, FS.DETECTED);
try {
if (lockFile.lock()) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProjectInfo.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProjectInfo.java
index 92364e4..f35ca7b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProjectInfo.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProjectInfo.java
@@ -18,6 +18,7 @@
public class ImportProjectInfo {
public String from;
+ public String name;
public String parent;
public List<ImportInfo> imports;
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/InsertLinkToOriginalChangeStep.java b/src/main/java/com/googlesource/gerrit/plugins/importer/InsertLinkToOriginalChangeStep.java
index 0205745..fe04784 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/InsertLinkToOriginalChangeStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/InsertLinkToOriginalChangeStep.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
@@ -44,10 +44,11 @@
private final String fromGerrit;
private final Change change;
private final ChangeInfo changeInfo;
+ private final boolean resume;
interface Factory {
InsertLinkToOriginalChangeStep create(String fromGerrit, Change change,
- ChangeInfo changeInfo);
+ ChangeInfo changeInfo, boolean resume);
}
@Inject
@@ -59,7 +60,8 @@
ChangeMessagesUtil cmUtil,
@Assisted String fromGerrit,
@Assisted Change change,
- @Assisted ChangeInfo changeInfo) {
+ @Assisted ChangeInfo changeInfo,
+ @Assisted boolean resume) {
this.currentUser = currentUser;
this.updateFactory = updateFactory;
this.genericUserFactory = genericUserFactory;
@@ -69,10 +71,12 @@
this.fromGerrit = fromGerrit;
this.change = change;
this.changeInfo = changeInfo;
+ this.resume = resume;
}
void insert() throws NoSuchChangeException, OrmException, IOException {
- insertMessage(change, "Imported from " + changeUrl(changeInfo));
+ insertMessage(change, (resume ? "Resumed import of " : "Imported from ")
+ + changeUrl(changeInfo));
}
private String changeUrl(ChangeInfo c) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ListImportedProjects.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ListImportedProjects.java
index 307fe8c..7ba24db 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ListImportedProjects.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ListImportedProjects.java
@@ -14,6 +14,7 @@
package com.googlesource.gerrit.plugins.importer;
+import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.gerrit.extensions.annotations.PluginData;
import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -22,6 +23,8 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import org.kohsuke.args4j.Option;
+
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
@@ -33,6 +36,14 @@
private final File lockRoot;
+ @Option(name = "--match", metaVar = "MATCH",
+ usage = "List only projects containing this substring, case insensitive")
+ public void setMatch(String match) {
+ this.match = match.toLowerCase();
+ }
+
+ private String match;
+
@Inject
ListImportedProjects(@PluginData File data) {
this.lockRoot = data;
@@ -49,10 +60,12 @@
}
private File[] listImportFiles() {
+ match = Strings.nullToEmpty(match);
+
return lockRoot.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
- return !name.endsWith(".lock");
+ return !name.endsWith(".lock") && name.toLowerCase().contains(match);
}
});
}
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 fb66805..c45d18a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/Module.java
@@ -15,6 +15,7 @@
package com.googlesource.gerrit.plugins.importer;
import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
+import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
import static com.googlesource.gerrit.plugins.importer.ImportProjectResource.IMPORT_PROJECT_KIND;
import com.google.gerrit.extensions.annotations.Exports;
@@ -31,6 +32,9 @@
bind(CapabilityDefinition.class)
.annotatedWith(Exports.named(ImportCapability.ID))
.to(ImportCapability.class);
+ bind(CapabilityDefinition.class)
+ .annotatedWith(Exports.named(CopyProjectCapability.ID))
+ .to(CopyProjectCapability.class);
install(new RestApiModule() {
@Override
protected void configure() {
@@ -38,6 +42,10 @@
child(CONFIG_KIND, "projects").to(ProjectsCollection.class);
get(IMPORT_PROJECT_KIND).to(GetImportedProject.class);
put(IMPORT_PROJECT_KIND, "resume").to(ResumeProjectImport.class);
+
+ put(PROJECT_KIND, "copy").to(CopyProject.class);
+ put(PROJECT_KIND, "copy.resume").to(ResumeCopyProject.class);
+ put(PROJECT_KIND, "import.resume").to(ResumeProjectImport.OnProjects.class);
}
});
bind(LifecycleListener.class)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/OpenRepositoryStep.java b/src/main/java/com/googlesource/gerrit/plugins/importer/OpenRepositoryStep.java
index 16eef2b..8b347a1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/OpenRepositoryStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/OpenRepositoryStep.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
@@ -50,15 +50,23 @@
this.projectCreationValidationListeners = projectCreationValidationListeners;
}
- Repository open(Project.NameKey name, ProgressMonitor pm)
+ Repository open(Project.NameKey name, boolean resume, ProgressMonitor pm)
throws ResourceConflictException, IOException {
pm.beginTask("Open repository", 1);
try {
- git.openRepository(name);
- throw new ResourceConflictException(format(
- "repository %s already exists", name.get()));
+ Repository repo = git.openRepository(name);
+ if (resume) {
+ return repo;
+ } else {
+ throw new ResourceConflictException(format(
+ "repository %s already exists", name.get()));
+ }
} catch (RepositoryNotFoundException e) {
// Project doesn't exist
+ if (resume) {
+ throw new ResourceConflictException(format(
+ "repository %s does not exist", name.get()));
+ }
}
beforeCreateProject(name);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ProgressMonitorUtil.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ProgressMonitorUtil.java
index 51b1a49..209d14e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ProgressMonitorUtil.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ProgressMonitorUtil.java
@@ -1,16 +1,17 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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 org.eclipse.jgit.lib.ProgressMonitor;
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 e5cfedb..92ae1d2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectCommand.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
@@ -23,7 +23,6 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.ConfigResource;
import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.validators.ValidationException;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
@@ -46,6 +45,11 @@
usage = "URL of the remote system from where the project should be imported")
private String url;
+ @Option(name = "--name", required = false, metaVar = "NAME",
+ usage = "name of project in source system (if not specified it is"
+ + " assumed to be the same name as in the target system)")
+ private String name;
+
@Option(name = "--user", aliases = {"-u"}, required = true, metaVar = "NAME",
usage = "user on remote system")
private String user;
@@ -56,10 +60,13 @@
@Option(name = "--parent", required = false, metaVar = "NAME",
usage = "name of parent project in target system")
- private ProjectControl parent;
+ private String parent;
+
+ @Option(name = "--quiet", usage = "suppress progress messages")
+ private boolean quiet;
@Argument(index = 0, required = true, metaVar = "NAME",
- usage = "name of the project to be imported")
+ usage = "name of the project in target system")
private String project;
@Inject
@@ -71,15 +78,18 @@
NoSuchAccountException {
ImportProject.Input input = new ImportProject.Input();
input.from = url;
+ input.name = name;
input.user = user;
input.pass = readPassword();
- if (parent != null) {
- input.parent = parent.getProject().getName();
+ if (!Strings.isNullOrEmpty(parent)) {
+ input.parent = parent;
}
try {
ImportProject importer = importProjectFactory.create(new Project.NameKey(project));
- importer.setErr(stderr);
+ if (!quiet) {
+ importer.setErr(stderr);
+ }
importer.apply(new ConfigResource(), input);
} catch (RestApiException e) {
throw die(e.getMessage());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/RemoteApi.java b/src/main/java/com/googlesource/gerrit/plugins/importer/RemoteApi.java
index 5d20f01..967069e 100755
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/RemoteApi.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/RemoteApi.java
@@ -42,9 +42,10 @@
public ProjectInfo getProject(String projectName) throws IOException,
BadRequestException {
String endPoint = "/projects/" + projectName;
- RestResponse r = checkedGet(endPoint);
- return newGson().fromJson(r.getReader(),
- new TypeToken<ProjectInfo>() {}.getType());
+ try (RestResponse r = checkedGet(endPoint)) {
+ return newGson().fromJson(r.getReader(),
+ new TypeToken<ProjectInfo>() {}.getType());
+ }
}
public List<ChangeInfo> queryChanges(String projectName) throws IOException,
@@ -59,10 +60,12 @@
ListChangesOption.CURRENT_REVISION,
ListChangesOption.ALL_REVISIONS,
ListChangesOption.ALL_COMMITS)));
- RestResponse r = checkedGet(endPoint);
- List<ChangeInfo> result =
- newGson().fromJson(r.getReader(),
+
+ List<ChangeInfo> result;
+ try (RestResponse r = checkedGet(endPoint)) {
+ result = newGson().fromJson(r.getReader(),
new TypeToken<List<ChangeInfo>>() {}.getType());
+ }
for (ChangeInfo c : result) {
for (Map.Entry<String, RevisionInfo> e : c.revisions.entrySet()) {
@@ -76,10 +79,11 @@
public Iterable<CommentInfo> getComments(int changeId, String rev)
throws IOException, BadRequestException {
String endPoint = "/changes/" + changeId + "/revisions/" + rev + "/comments";
- RestResponse r = checkedGet(endPoint);
- Map<String, List<CommentInfo>> result =
- newGson().fromJson(r.getReader(),
- new TypeToken<Map<String, List<CommentInfo>>>() {}.getType());
+ Map<String, List<CommentInfo>> result;
+ try (RestResponse r = checkedGet(endPoint)) {
+ result = newGson().fromJson(r.getReader(),
+ new TypeToken<Map<String, List<CommentInfo>>>() {}.getType());
+ }
for (Map.Entry<String, List<CommentInfo>> e : result.entrySet()) {
for (CommentInfo i : e.getValue()) {
i.path = e.getKey();
@@ -105,6 +109,12 @@
throw new BadRequestException(
"invalid credentials: accessing source system failed with 401 Unauthorized");
}
+ if (r.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
+ throw new BadRequestException(
+ "Project not found or missing permissions for accessing the project:"
+ + " accessing source system failed with 404 Not found");
+ }
+
if (r.getStatusCode() < 200 || 300 <= r.getStatusCode()) {
throw new IOException(String.format(
"Unexpected response code for %s on %s : %s", method.name(),
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayChangesStep.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayChangesStep.java
index e320d64..6fbd8a0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayChangesStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayChangesStep.java
@@ -1,20 +1,22 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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.common.collect.Iterators;
import com.google.gerrit.common.errors.NoSuchAccountException;
+import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Branch;
@@ -23,9 +25,12 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.validators.ValidationException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.lib.Constants;
@@ -45,7 +50,9 @@
@Assisted("user") String user,
@Assisted("password") String password,
Repository repo,
- Project.NameKey name,
+ @Assisted("srcProject") Project.NameKey srcProject,
+ @Assisted("targetProject") Project.NameKey targetProject,
+ boolean resume,
ProgressMonitor pm);
}
@@ -58,11 +65,13 @@
private final AccountUtil accountUtil;
private final ReviewDb db;
private final ChangeIndexer indexer;
+ private final Provider<InternalChangeQuery> queryProvider;
private final String fromGerrit;
private final RemoteApi api;
private final Repository repo;
- private final Project.NameKey name;
-
+ private final Project.NameKey srcProject;
+ private final Project.NameKey targetProject;
+ private final boolean resume;
private final ProgressMonitor pm;
@Inject
@@ -76,12 +85,15 @@
AccountUtil accountUtil,
ReviewDb db,
ChangeIndexer indexer,
- @Assisted ProgressMonitor pm,
+ Provider<InternalChangeQuery> queryProvider,
@Assisted("from") String fromGerrit,
@Assisted("user") String user,
@Assisted("password") String password,
@Assisted Repository repo,
- @Assisted Project.NameKey name) {
+ @Assisted("srcProject") Project.NameKey srcProject,
+ @Assisted("targetProject") Project.NameKey targetProject,
+ @Assisted boolean resume,
+ @Assisted ProgressMonitor pm) {
this.replayRevisionsFactory = replayRevisionsFactory;
this.replayInlineCommentsFactory = replayInlineCommentsFactory;
this.replayMessagesFactory = replayMessagesFactory;
@@ -91,17 +103,20 @@
this.accountUtil = accountUtil;
this.db = db;
this.indexer = indexer;
+ this.queryProvider = queryProvider;
this.fromGerrit = fromGerrit;
this.api = new RemoteApi(fromGerrit, user, password);
this.repo = repo;
- this.name = name;
+ this.srcProject = srcProject;
+ this.targetProject = targetProject;
+ this.resume = resume;
this.pm = pm;
}
void replay() throws IOException, OrmException,
NoSuchAccountException, NoSuchChangeException, RestApiException,
ValidationException {
- List<ChangeInfo> changes = api.queryChanges(name.get());
+ List<ChangeInfo> changes = api.queryChanges(srcProject.get());
pm.beginTask("Replay Changes", changes.size());
RevWalk rw = new RevWalk(repo);
@@ -119,34 +134,68 @@
private void replayChange(RevWalk rw, ChangeInfo c)
throws IOException, OrmException, NoSuchAccountException,
NoSuchChangeException, RestApiException, ValidationException {
- Change change = createChange(c);
+ if (c.status == ChangeStatus.DRAFT) {
+ // no import of draft changes
+ return;
+ }
+
+ Change change = resume ? findChange(c) : null;
+ boolean resumeChange;
+ if (change == null) {
+ resumeChange = false;
+ change = createChange(c);
+ } else {
+ resumeChange = true;
+ }
replayRevisionsFactory.create(repo, rw, change, c).replay();
- db.changes().insert(Collections.singleton(change));
+ upsertChange(resumeChange, change, c);
- replayInlineCommentsFactory.create(change, c, api).replay();
- replayMessagesFactory.create(change, c).replay();
- addApprovalsFactory.create(change, c).add();
- addHashtagsFactory.create(change, c).add();
+ replayInlineCommentsFactory.create(change, c, api, resumeChange).replay();
+ replayMessagesFactory.create(change, c, resumeChange).replay();
+ addApprovalsFactory.create(change, c, resume).add();
+ addHashtagsFactory.create(change, c, resumeChange).add();
- insertLinkToOriginalFactory.create(fromGerrit,change, c).insert();
+ insertLinkToOriginalFactory.create(fromGerrit,change, c, resumeChange).insert();
indexer.index(db, change);
}
+ private Change findChange(ChangeInfo c) throws OrmException {
+ List<Change> changes = ChangeData.asChanges(
+ queryProvider.get().byBranchKey(
+ new Branch.NameKey(targetProject, fullName(c.branch)),
+ new Change.Key(c.changeId)));
+ if (changes.isEmpty()) {
+ return null;
+ } else {
+ return db.changes().get(
+ Iterators.getOnlyElement(changes.iterator()).getId());
+ }
+ }
+
private Change createChange(ChangeInfo c) throws OrmException,
NoSuchAccountException {
Change.Id changeId = new Change.Id(db.nextChangeId());
Change change =
new Change(new Change.Key(c.changeId), changeId, accountUtil.resolveUser(c.owner),
- new Branch.NameKey(new Project.NameKey(c.project),
- fullName(c.branch)), c.created);
+ new Branch.NameKey(targetProject, fullName(c.branch)), c.created);
change.setStatus(Change.Status.forChangeStatus(c.status));
change.setTopic(c.topic);
change.setLastUpdatedOn(c.updated);
return change;
}
+ private void upsertChange(boolean resumeChange, Change change, ChangeInfo c)
+ throws OrmException {
+ if (resumeChange) {
+ change.setStatus(Change.Status.forChangeStatus(c.status));
+ change.setTopic(c.topic);
+ change.setLastUpdatedOn(c.updated);
+ }
+ db.changes().upsert(Collections.singleton(change));
+ }
+
private static String fullName(String branch) {
if (branch.startsWith(Constants.R_HEADS)) {
return branch;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayInlineCommentsStep.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayInlineCommentsStep.java
index 815460e..1e2fa12 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayInlineCommentsStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayInlineCommentsStep.java
@@ -1,84 +1,99 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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.common.collect.Ordering;
-import com.google.common.collect.Table;
-import com.google.common.collect.TreeBasedTable;
+import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.errors.NoSuchAccountException;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.CommentRange;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.PostReview;
-import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashMap;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Set;
class ReplayInlineCommentsStep {
interface Factory {
ReplayInlineCommentsStep create(Change change, ChangeInfo changeInfo,
- RemoteApi api);
+ RemoteApi api, boolean resume);
}
private final AccountUtil accountUtil;
private final ReviewDb db;
- private final Provider<PostReview> postReview;
private final IdentifiedUser.GenericFactory genericUserFactory;
private final ChangeControl.GenericFactory changeControlFactory;
+ private final ChangeUpdate.Factory updateFactory;
+ private final PatchLineCommentsUtil plcUtil;
+ private final PatchListCache patchListCache;
private final Change change;
private final ChangeInfo changeInfo;
private final RemoteApi api;
+ private final boolean resume;
@Inject
public ReplayInlineCommentsStep(AccountUtil accountUtil,
ReviewDb db,
- Provider<PostReview> postReview,
IdentifiedUser.GenericFactory genericUserFactory,
ChangeControl.GenericFactory changeControlFactory,
+ ChangeUpdate.Factory updateFactory,
+ PatchLineCommentsUtil plcUtil,
+ PatchListCache patchListCache,
@Assisted Change change,
@Assisted ChangeInfo changeInfo,
- @Assisted RemoteApi api) {
+ @Assisted RemoteApi api,
+ @Assisted boolean resume) {
this.accountUtil = accountUtil;
this.db = db;
- this.postReview = postReview;
this.genericUserFactory = genericUserFactory;
this.changeControlFactory = changeControlFactory;
+ this.updateFactory = updateFactory;
+ this.plcUtil = plcUtil;
+ this.patchListCache = patchListCache;
this.change = change;
this.changeInfo = changeInfo;
this.api = api;
+ this.resume = resume;
}
void replay() throws RestApiException, OrmException, IOException,
@@ -86,62 +101,95 @@
for (PatchSet ps : db.patchSets().byChange(change.getId())) {
Iterable<CommentInfo> comments = api.getComments(
changeInfo._number, ps.getRevision().get());
-
- Table<Timestamp, Account.Id, List<CommentInfo>> t = TreeBasedTable.create(
- Ordering.natural(), new Comparator<Account.Id>() {
- @Override
- public int compare(Account.Id a1, Account.Id a2) {
- return a1.get() - a2.get();
- }}
- );
-
- for (CommentInfo comment : comments) {
- Account.Id id = accountUtil.resolveUser(comment.author);
- List<CommentInfo> ci = t.get(comment.updated, id);
- if (ci == null) {
- ci = new ArrayList<>();
- t.put(comment.updated, id, ci);
- }
- ci.add(comment);
+ if (resume) {
+ comments = filterComments(ps, comments);
}
- for (Timestamp ts : t.rowKeySet()) {
- for (Map.Entry<Account.Id, List<CommentInfo>> e : t.row(ts).entrySet()) {
- postComments(change, ps, e.getValue(), e.getKey(), ts);
- }
+ Multimap<Account.Id, CommentInfo> commentsByAuthor = ArrayListMultimap.create();
+ for (CommentInfo comment : comments) {
+ Account.Id id = accountUtil.resolveUser(comment.author);
+ commentsByAuthor.put(id, comment);
+ }
+
+ for (Account.Id id : commentsByAuthor.keySet()) {
+ insertComments(ps, id, commentsByAuthor.get(id));
}
}
}
- private void postComments(Change change, PatchSet ps,
- List<CommentInfo> comments, Account.Id author, Timestamp ts)
- throws RestApiException, OrmException, IOException, NoSuchChangeException {
- ReviewInput input = new ReviewInput();
- input.notify = NotifyHandling.NONE;
- input.comments = new HashMap<>();
-
- for (CommentInfo comment : comments) {
- if (!input.comments.containsKey(comment.path)) {
- input.comments.put(comment.path,
- new ArrayList<ReviewInput.CommentInput>());
- }
-
- ReviewInput.CommentInput commentInput = new ReviewInput.CommentInput();
- commentInput.id = comment.id;
- commentInput.inReplyTo = comment.inReplyTo;
- commentInput.line = comment.line;
- commentInput.message = comment.message;
- commentInput.path = comment.path;
- commentInput.range = comment.range;
- commentInput.side = comment.side;
- commentInput.updated = comment.updated;
-
- input.comments.get(comment.path).add(commentInput);
+ private Iterable<CommentInfo> filterComments(PatchSet ps,
+ Iterable<CommentInfo> comments) throws OrmException {
+ Set<String> existingUuids = new HashSet<>();
+ for (PatchLineComment c : db.patchComments().byPatchSet(ps.getId())) {
+ existingUuids.add(c.getKey().get());
}
- postReview.get().apply(
- new RevisionResource(new ChangeResource(control(change, author)), ps),
- input, ts);
+ Iterator<CommentInfo> it = comments.iterator();
+ while (it.hasNext()) {
+ CommentInfo c = it.next();
+ if (existingUuids.contains(Url.decode(c.id))) {
+ it.remove();
+ }
+ }
+ return comments;
+ }
+
+ private void insertComments(PatchSet ps, Account.Id author,
+ Collection<CommentInfo> comments) throws OrmException, IOException,
+ NoSuchChangeException {
+ ChangeControl ctrl = control(change, author);
+
+ Map<String, PatchLineComment> drafts = scanDraftComments(ctrl, ps);
+
+ List<PatchLineComment> del = Lists.newArrayList();
+ List<PatchLineComment> ups = Lists.newArrayList();
+
+ for (CommentInfo c : comments) {
+ String parent = Url.decode(c.inReplyTo);
+ PatchLineComment e = drafts.remove(Url.decode(c.id));
+ if (e == null) {
+ e = new PatchLineComment(
+ new PatchLineComment.Key(
+ new Patch.Key(ps.getId(), c.path),
+ Url.decode(c.id)),
+ c.line != null ? c.line : 0,
+ author, parent, c.updated);
+ } else if (parent != null) {
+ e.setParentUuid(parent);
+ }
+ e.setStatus(PatchLineComment.Status.PUBLISHED);
+ e.setWrittenOn(c.updated);
+ e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
+ setCommentRevId(e, patchListCache, change, ps);
+ e.setMessage(c.message);
+ if (c.range != null) {
+ e.setRange(new CommentRange(
+ c.range.startLine,
+ c.range.startCharacter,
+ c.range.endLine,
+ c.range.endCharacter));
+ e.setLine(c.range.endLine);
+ }
+ ups.add(e);
+ }
+
+ del.addAll(drafts.values());
+ ChangeUpdate update = updateFactory.create(ctrl, TimeUtil.nowTs());
+ update.setPatchSetId(ps.getId());
+ plcUtil.deleteComments(db, update, del);
+ plcUtil.upsertComments(db, update, ups);
+ update.commit();
+ }
+
+ private Map<String, PatchLineComment> scanDraftComments(ChangeControl ctrl,
+ PatchSet ps) throws OrmException {
+ Map<String, PatchLineComment> drafts = Maps.newHashMap();
+ for (PatchLineComment c : plcUtil.draftByPatchSetAuthor(db, ps.getId(),
+ ((IdentifiedUser) ctrl.getCurrentUser()).getAccountId(),
+ ctrl.getNotes())) {
+ drafts.put(c.getKey().get(), c);
+ }
+ return drafts;
}
private ChangeControl control(Change change, Account.Id id)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayMessagesStep.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayMessagesStep.java
index 778e8c3..1b2d5e8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayMessagesStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayMessagesStep.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
@@ -38,7 +38,8 @@
class ReplayMessagesStep {
interface Factory {
- ReplayMessagesStep create(Change change, ChangeInfo changeInfo);
+ ReplayMessagesStep create(Change change, ChangeInfo changeInfo,
+ boolean resume);
}
private final AccountUtil accountUtil;
@@ -49,6 +50,7 @@
private final ChangeControl.GenericFactory changeControlFactory;
private final Change change;
private final ChangeInfo changeInfo;
+ private final boolean resume;
@Inject
public ReplayMessagesStep(AccountUtil accountUtil,
@@ -58,7 +60,8 @@
ChangeControl.GenericFactory changeControlFactory,
ReviewDb db,
@Assisted Change change,
- @Assisted ChangeInfo changeInfo) {
+ @Assisted ChangeInfo changeInfo,
+ @Assisted boolean resume) {
this.accountUtil = accountUtil;
this.updateFactory = updateFactory;
this.cmUtil = cmUtil;
@@ -67,18 +70,25 @@
this.changeControlFactory = changeControlFactory;
this.change = change;
this.changeInfo = changeInfo;
+ this.resume = resume;
}
void replay() throws NoSuchAccountException, NoSuchChangeException,
OrmException, IOException {
for (ChangeMessageInfo msg : changeInfo.messages) {
+ ChangeMessage.Key msgKey = new ChangeMessage.Key(change.getId(), msg.id);
+ if (resume && db.changeMessages().get(msgKey) != null) {
+ // already replayed
+ continue;
+ }
+
Timestamp ts = msg.date;
if (msg.author != null) {
Account.Id userId = accountUtil.resolveUser(msg.author);
ChangeUpdate update = updateFactory.create(control(change, userId), ts);
ChangeMessage cmsg =
- new ChangeMessage(new ChangeMessage.Key(change.getId(), msg.id),
- userId, ts, new PatchSet.Id(change.getId(), msg._revisionNumber));
+ new ChangeMessage(msgKey, userId, ts, new PatchSet.Id(
+ change.getId(), msg._revisionNumber));
cmsg.setMessage(msg.message);
cmUtil.addChangeMessage(db, update, cmsg);
update.commit();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayRevisionsStep.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayRevisionsStep.java
index e37dff8..8d5b152 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayRevisionsStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayRevisionsStep.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// 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
+// 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
+// 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.
+// 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;
@@ -81,15 +81,24 @@
db.changes().beginTransaction(change.getId());
try {
for (RevisionInfo r : revisions) {
+ if (r.draft != null && r.draft) {
+ // no import of draft patch sets
+ continue;
+ }
+ PatchSet ps = new PatchSet(new PatchSet.Id(change.getId(), r._number));
+ String newRef = ps.getId().toRefName();
+ if (repo.resolve(newRef) != null) {
+ // already replayed
+ continue;
+ }
+
String origRef = imported(r.ref);
ObjectId id = repo.resolve(origRef);
if (id == null) {
- // already replayed?
continue;
}
RevCommit commit = rw.parseCommit(id);
- PatchSet ps = new PatchSet(new PatchSet.Id(change.getId(), r._number));
patchSets.add(ps);
ps.setUploader(accountUtil.resolveUser(r.uploader));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/RestResponse.java b/src/main/java/com/googlesource/gerrit/plugins/importer/RestResponse.java
index 512db6e..24f6831 100755
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/RestResponse.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/RestResponse.java
@@ -16,13 +16,15 @@
import static com.google.gerrit.httpd.restapi.RestApiServlet.JSON_MAGIC;
+import org.apache.http.client.methods.CloseableHttpResponse;
+
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
public class RestResponse extends HttpResponse {
- RestResponse(org.apache.http.HttpResponse response) {
+ RestResponse(CloseableHttpResponse response) {
super(response);
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/RestSession.java b/src/main/java/com/googlesource/gerrit/plugins/importer/RestSession.java
index f02ecf7..d2ca5c7 100755
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/RestSession.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/RestSession.java
@@ -91,8 +91,7 @@
return new RestResponse(getClient().execute(delete));
}
-
- public static RawInput newRawInput(final String content) throws IOException {
+ public static RawInput newRawInput(final String content) {
Preconditions.checkNotNull(content);
Preconditions.checkArgument(!content.isEmpty());
return new RawInput() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ResumeCopyProject.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ResumeCopyProject.java
new file mode 100644
index 0000000..00e22e1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ResumeCopyProject.java
@@ -0,0 +1,124 @@
+// 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.common.errors.NoSuchAccountException;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import com.googlesource.gerrit.plugins.importer.CopyProject.Input;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+
+import java.io.IOException;
+
+@Singleton
+@RequiresCapability(CopyProjectCapability.ID)
+class ResumeCopyProject implements RestModifyView<ProjectResource, Input>,
+ UiAction<ProjectResource> {
+ private final ResumeProjectImport resumeProjectImport;
+ private final ProjectsCollection projectsCollection;
+ private final Provider<CurrentUser> currentUserProvider;
+ private final String pluginName;
+ private final String canonicalWebUrl;
+ private final ProjectCache projectCache;
+
+ @Inject
+ ResumeCopyProject(
+ ResumeProjectImport resumeProjectImport,
+ ProjectsCollection projectsCollection,
+ Provider<CurrentUser> currentUserProvider,
+ @PluginName String pluginName,
+ @CanonicalWebUrl String canonicalWebUrl,
+ ProjectCache projectCache) {
+ this.resumeProjectImport = resumeProjectImport;
+ this.projectsCollection = projectsCollection;
+ this.currentUserProvider = currentUserProvider;
+ this.pluginName = pluginName;
+ this.canonicalWebUrl = canonicalWebUrl;
+ this.projectCache = projectCache;
+ }
+
+ @Override
+ public Response<String> apply(ProjectResource rsrc, Input input)
+ throws RestApiException, IOException, OrmException, ValidationException,
+ GitAPIException, NoSuchChangeException, NoSuchAccountException {
+ AccountState s = ((IdentifiedUser) currentUserProvider.get()).state();
+
+ ResumeProjectImport.Input in = new ResumeProjectImport.Input();
+ in.user = s.getUserName();
+ in.pass = s.getPassword(s.getUserName());
+
+ ImportProjectResource projectResource =
+ projectsCollection.parse(new ConfigResource(),
+ IdString.fromDecoded(rsrc.getName()));
+ return resumeProjectImport.apply(projectResource, in);
+ }
+
+ @Override
+ public UiAction.Description getDescription(
+ ProjectResource rsrc) {
+ return new UiAction.Description()
+ .setLabel("Resume Copy...")
+ .setTitle(String.format("Resume copy for project %s", rsrc.getName()))
+ .setVisible(canResumeCopy(rsrc) && isCopied(rsrc));
+ }
+
+ private boolean canResumeCopy(ProjectResource rsrc) {
+ CapabilityControl ctl = currentUserProvider.get().getCapabilities();
+ return ctl.canAdministrateServer()
+ || (ctl.canPerform(pluginName + "-" + CopyProjectCapability.ID)
+ && rsrc.getControl().isOwner());
+ }
+
+ private boolean isCopied(ProjectResource rsrc) {
+ try {
+ ImportProjectResource projectResource =
+ projectsCollection.parse(new ConfigResource(),
+ IdString.fromDecoded(rsrc.getName()));
+ ImportProjectInfo info = projectResource.getInfo();
+ if (!canonicalWebUrl.equals(info.from)) {
+ // no copy, but an import from another system
+ return false;
+ }
+
+ // check that source project still exists
+ return projectCache.get(new Project.NameKey(info.name)) != null;
+ } catch (ResourceNotFoundException | IOException e) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ResumeProjectImport.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ResumeProjectImport.java
index 40abb1e..04d5df5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ResumeProjectImport.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ResumeProjectImport.java
@@ -18,10 +18,13 @@
import com.google.gerrit.common.errors.NoSuchAccountException;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.config.ConfigResource;
import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.validators.ValidationException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -61,4 +64,28 @@
return importProjectFactory.create(rsrc.getName())
.resume(input.user, input.pass, rsrc.getImportStatus());
}
+
+ public static class OnProjects implements RestModifyView<ProjectResource, Input> {
+ private final ProjectsCollection projectsCollection;
+ private final ResumeProjectImport resumeProjectImport;
+
+ @Inject
+ public OnProjects(
+ ProjectsCollection projectsCollection,
+ ResumeProjectImport resumeProjectImport) {
+ this.projectsCollection = projectsCollection;
+ this.resumeProjectImport = resumeProjectImport;
+ }
+
+ @Override
+ public Response<String> apply(ProjectResource rsrc, Input input)
+ throws RestApiException, IOException, OrmException,
+ ValidationException, GitAPIException, NoSuchChangeException,
+ NoSuchAccountException {
+ ImportProjectResource projectResource =
+ projectsCollection.parse(new ConfigResource(),
+ IdString.fromDecoded(rsrc.getName()));
+ return resumeProjectImport.apply(projectResource, input);
+ }
+ }
}
diff --git a/src/main/resources/Documentation/cmd-project.md b/src/main/resources/Documentation/cmd-project.md
index 099b232..87b1363 100644
--- a/src/main/resources/Documentation/cmd-project.md
+++ b/src/main/resources/Documentation/cmd-project.md
@@ -10,9 +10,11 @@
```
ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ project \
--from <URL> | -f <URL> \
+ [--name <NAME>] \
--user <USER> | -u <USER> \
--pass - | <PASS> \
[--parent <NAME>] \
+ [--quiet] \
<NAME>
```
@@ -36,6 +38,10 @@
`--from`
: URL of the remote system from where the project should be imported.
+`--name`
+: Name of the project in the source system.
+ If not specified it is assumed to be the same name as in the target system.
+
`--pass`
: Password of remote user.
@@ -44,7 +50,10 @@
`--parent`
: Name of the parent project in the target system.
- The imported projects will be created under this parent project.
+ The imported project will be created under this parent project.
+
+`--quiet`
+: Suppress progress messages.
EXAMPLES
--------
diff --git a/src/main/resources/Documentation/rest-api-config.md b/src/main/resources/Documentation/rest-api-config.md
index 86764ff..74b99de 100644
--- a/src/main/resources/Documentation/rest-api-config.md
+++ b/src/main/resources/Documentation/rest-api-config.md
@@ -8,7 +8,7 @@
[REST API](../../../Documentation/rest-api.html).
<a id="importer-endpoints"> Importer Endpoints
----------------------------------------------
+----------------------------------------------
### <a id="import-project"> Import Project
_PUT /config/server/@PLUGIN@~projects/[\{project-name\}](../../../Documentation/rest-api-projects.html#project-name)_
@@ -48,10 +48,14 @@
capability (provided by this plugin) or the 'Administrate Server'
capability.
+It is possible to filter the list of imported projects using the
+`match` option. The response will include only those projects whose
+name contains the given `match` substring, case insensitive.
+
#### Request
```
- GET /config/server/@PLUGIN@~projects/ HTTP/1.0
+ GET /config/server/@PLUGIN@~projects/?match=my HTTP/1.0
```
#### Response
@@ -65,6 +69,7 @@
{
"myProject": {
"from": "http://localhost:8081/",
+ "name": "myProject",
"imports": [
{
"timestamp": "2015-03-11 09:14:21.748000000",
@@ -78,8 +83,9 @@
}
]
},
- "myOtherProject": {
+ "myProject2": {
"from": "http://localhost:8081/",
+ "name": "projectToBeRenamed",
"imports": [
{
"timestamp": "2015-03-11 09:16:04.511000000",
@@ -124,6 +130,7 @@
)]}'
{
"from": "http://localhost:8081/",
+ "name": "myProject",
"imports": [
{
"timestamp": "2015-03-11 09:14:21.748000000",
@@ -184,6 +191,7 @@
* _from_: URL of the remote system from where the project should be
imported.
+* _name_: Name of the project in the source system.
* _parent_: (Optional) Name of the parent project in the target system.
* _imports_: List of past imports as [ImportInfo](#import-info)
entities.
@@ -195,6 +203,9 @@
* _from_: URL of the remote system from where the project should be
imported.
+* _name_: (Optional) Name of the project in the source system.
+If not specified it is assumed to be the same name as in the target
+system.
* _user_: User on remote system.
* _pass_: Password of remote user.
* _parent_: (Optional) Name of the parent project in the target system.
diff --git a/src/main/resources/Documentation/rest-api-projects.md b/src/main/resources/Documentation/rest-api-projects.md
new file mode 100644
index 0000000..ebc443f
--- /dev/null
+++ b/src/main/resources/Documentation/rest-api-projects.md
@@ -0,0 +1,95 @@
+@PLUGIN@ - /projects/ REST API
+==============================
+
+This page describes the REST endpoints that are added by the @PLUGIN@
+plugin.
+
+Please also take note of the general information on the
+[REST API](../../../Documentation/rest-api.html).
+
+<a id="importer-endpoints"> Importer Endpoints
+----------------------------------------------
+
+### <a id="copy-project"> Copy Project
+_PUT /projects/[\{project-name\}](../../../Documentation/rest-api-projects.html#project-name)/@PLUGIN@~copy_
+
+Copies a project.
+
+Information about the copy target must be provided in the request body
+as a [CopyProjectInput](#copy-project-input) entity.
+
+Caller must be a member of a group that is granted the 'CopyProject'
+capability (provided by this plugin) or the 'Administrate Server'
+capability.
+
+#### Request
+
+```
+ PUT /projects/myProject/@PLUGIN@~copy HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "name": "myProjectCopy"
+ }
+```
+
+### <a id="resume-copy-project"> Resume Copy Project
+_PUT /projects/[\{project-name\}](../../../Documentation/rest-api-projects.html#project-name)/@PLUGIN@~copy.resume_
+
+Resumes copying to a project from the original copy source.
+
+Caller must be a member of a group that is granted the 'CopyProject'
+capability (provided by this plugin) or the 'Administrate Server'
+capability.
+
+#### Request
+
+```
+ PUT /projects/myProjectCopy/@PLUGIN@~copy.resume HTTP/1.0
+```
+
+### <a id="resume-project-import"> Resume Project Import
+_PUT /projects/[\{project-name\}](../../../Documentation/rest-api-projects.html#project-name)/@PLUGIN@~import.resume_
+
+Resumes importing to a project from the original copy source.
+
+Information about the import resume must be provided in the request
+body as a [ImportResumeInput](rest-api-config.html#import-resume-input)
+entity.
+
+Caller must be a member of a group that is granted the 'ImportProject'
+capability (provided by this plugin) or the 'Administrate Server'
+capability.
+
+#### Request
+
+```
+ PUT /projects/myProjectCopy/@PLUGIN@~import.resume HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "user": "myUser",
+ "pass": "myPassword"
+ }
+```
+
+
+<a id="json-entities">JSON Entities
+-----------------------------------
+
+### <a id="copy-project-input"></a>CopyProjectInput
+
+The `CopyProjectInput` entity contains information about a the copy
+target.
+
+* _name_: The target project name.
+
+
+SEE ALSO
+--------
+
+* [Config related REST endpoints](../../../Documentation/rest-api-projects.html)
+
+GERRIT
+------
+Part of [Gerrit Code Review](../../../Documentation/index.html)
diff --git a/src/main/resources/static/copy-project.js b/src/main/resources/static/copy-project.js
new file mode 100644
index 0000000..a7bc91d
--- /dev/null
+++ b/src/main/resources/static/copy-project.js
@@ -0,0 +1,42 @@
+// 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.
+
+Gerrit.install(function(self) {
+ function onCopyProject(c) {
+ var t = c.textfield();
+ var b = c.button('Copy',
+ {onclick: function(){
+ c.call(
+ {name: t.value},
+ function(r) {
+ c.hide();
+ window.alert('The project: "'
+ + c.project
+ + '" was copied to "'
+ + t.value
+ + '".'),
+ Gerrit.go('/admin/projects/' + t.value);
+ });
+ }});
+ c.popup(c.div(
+ c.msg('Copy project "'
+ + c.project
+ + '" to '),
+ t,
+ c.br(),
+ c.br(),
+ b));
+ }
+ self.onAction('project', 'copy', onCopyProject);
+ });
diff --git a/src/main/resources/static/resume-copy-project.js b/src/main/resources/static/resume-copy-project.js
new file mode 100644
index 0000000..9d61631
--- /dev/null
+++ b/src/main/resources/static/resume-copy-project.js
@@ -0,0 +1,38 @@
+// 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.
+
+Gerrit.install(function(self) {
+ function onResumeCopyProject(c) {
+ var b = c.button('Resume',
+ {onclick: function(){
+ c.call(
+ {},
+ function(r) {
+ c.hide();
+ window.alert('Copy for project "'
+ + c.project
+ + '" was resumed."'),
+ Gerrit.go('/admin/projects/' + c.project);
+ });
+ }});
+ c.popup(c.div(
+ c.msg('Resume copy for project "'
+ + c.project
+ + '" ?'),
+ c.br(),
+ c.br(),
+ b));
+ }
+ self.onAction('project', 'copy.resume', onResumeCopyProject);
+ });