Merge "Handle labels not existing in the target system"
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 b996657..f23cef1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/AccountUtil.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/AccountUtil.java
@@ -68,7 +68,7 @@
         case HTTP_LDAP:
         case CLIENT_SSL_CERT_LDAP:
         case LDAP:
-          return createAccountByLdapAndAddSshKeys(api, acc, a);
+          return createAccountByLdapAndAddSshKeys(api, acc);
         default:
           throw new NoSuchAccountException(String.format("User %s not found",
               acc.username));
@@ -84,7 +84,7 @@
   }
 
   private Account.Id createAccountByLdapAndAddSshKeys(RemoteApi api,
-      AccountInfo acc, AccountState a)
+      AccountInfo acc)
       throws NoSuchAccountException, BadRequestException, IOException,
       OrmException {
     if (!acc.username.matches(Account.USER_NAME_PATTERN)) {
@@ -96,7 +96,7 @@
       AuthRequest req = AuthRequest.forUser(acc.username);
       req.setSkipAuthentication(true);
       Account.Id id = accountManager.authenticate(req).getAccountId();
-      addSshKeys(api, acc, a);
+      addSshKeys(api, acc);
       return id;
     } catch (AccountException e) {
       throw new NoSuchAccountException(
@@ -104,9 +104,10 @@
     }
   }
 
-  private void addSshKeys(RemoteApi api, AccountInfo acc, AccountState a) throws
+  private void addSshKeys(RemoteApi api, AccountInfo acc) throws
   BadRequestException, IOException, OrmException {
     List<SshKeyInfo> sshKeys = api.getSshKeys(acc.username);
+    AccountState a = accountCache.getByUsername(acc.username);
     db.get().accountSshKeys().upsert(toAccountSshKey(a, sshKeys));
   }
 
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 e40fc5e..f3bf629 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/AddHashtagsStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/AddHashtagsStep.java
@@ -15,12 +15,10 @@
 package com.googlesource.gerrit.plugins.importer;
 
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
-import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.HashtagsUtil;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -39,7 +37,7 @@
   }
 
   private final HashtagsUtil hashtagsUtil;
-  private final IdentifiedUser.GenericFactory genericUserFactory;
+  private final CurrentUser currentUser;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final Change change;
   private final ChangeInfo changeInfo;
@@ -47,7 +45,7 @@
 
   @Inject
   AddHashtagsStep(HashtagsUtil hashtagsUtil,
-      IdentifiedUser.GenericFactory genericUserFactory,
+      CurrentUser currentUser,
       ChangeControl.GenericFactory changeControlFactory,
       @Assisted Change change,
       @Assisted ChangeInfo changeInfo,
@@ -55,14 +53,14 @@
     this.hashtagsUtil = hashtagsUtil;
     this.change = change;
     this.changeInfo = changeInfo;
-    this.genericUserFactory = genericUserFactory;
+    this.currentUser = currentUser;
     this.changeControlFactory = changeControlFactory;
     this.resume = resume;
   }
 
   void add() throws IllegalArgumentException, AuthException, IOException,
       ValidationException, OrmException, NoSuchChangeException {
-    ChangeControl ctrl = control(change, changeInfo.owner);
+    ChangeControl ctrl = changeControlFactory.controlFor(change, currentUser);
 
     if (resume) {
       HashtagsInput input = new HashtagsInput();
@@ -74,15 +72,4 @@
     input.add = new HashSet<>(changeInfo.hashtags);
     hashtagsUtil.setHashtags(ctrl, input, false, false);
   }
-
-  private ChangeControl control(Change change, AccountInfo acc)
-      throws NoSuchChangeException {
-    return control(change, new Account.Id(acc._accountId));
-  }
-
-  private ChangeControl control(Change change, Account.Id id)
-      throws NoSuchChangeException {
-    return changeControlFactory.controlFor(change,
-        genericUserFactory.create(id));
-  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/CompleteProjectImport.java b/src/main/java/com/googlesource/gerrit/plugins/importer/CompleteProjectImport.java
index 7cb35ac..81363e7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/CompleteProjectImport.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/CompleteProjectImport.java
@@ -15,12 +15,21 @@
 package com.googlesource.gerrit.plugins.importer;
 
 import com.google.gerrit.extensions.annotations.PluginData;
+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.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 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.account.CapabilityControl;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import com.googlesource.gerrit.plugins.importer.CompleteProjectImport.Input;
 
@@ -67,4 +76,57 @@
       throw new ResourceConflictException("failed to lock project for delete");
     }
   }
+
+  public static class OnProjects implements
+      RestModifyView<ProjectResource, Input>, UiAction<ProjectResource> {
+    private final ProjectsCollection projectsCollection;
+    private final CompleteProjectImport completeProjectImport;
+    private final Provider<CurrentUser> currentUserProvider;
+    private final String pluginName;
+
+    @Inject
+    public OnProjects(ProjectsCollection projectsCollection,
+        CompleteProjectImport completeProjectImport,
+        Provider<CurrentUser> currentUserProvider,
+        @PluginName String pluginName) {
+      this.projectsCollection = projectsCollection;
+      this.completeProjectImport = completeProjectImport;
+      this.currentUserProvider = currentUserProvider;
+      this.pluginName = pluginName;
+    }
+
+    @Override
+    public Response<?> apply(ProjectResource rsrc, Input input)
+        throws ResourceNotFoundException, ResourceConflictException {
+      ImportProjectResource projectResource =
+          projectsCollection.parse(new ConfigResource(),
+              IdString.fromDecoded(rsrc.getName()));
+      return completeProjectImport.apply(projectResource, input);
+    }
+
+    @Override
+    public UiAction.Description getDescription(ProjectResource rsrc) {
+      UiAction.Description desc = new UiAction.Description()
+          .setLabel("Complete Import...")
+          .setTitle("Complete the project import."
+              + " After completion, resume is not possible anymore.");
+
+      try {
+        projectsCollection.parse(new ConfigResource(),
+            IdString.fromDecoded(rsrc.getName()));
+        desc.setVisible(canCompleteImport(rsrc));
+      } catch (ResourceNotFoundException e) {
+        desc.setVisible(false);
+      }
+
+      return desc;
+    }
+
+    private boolean canCompleteImport(ProjectResource rsrc) {
+      CapabilityControl ctl = currentUserProvider.get().getCapabilities();
+      return ctl.canAdministrateServer()
+          || (ctl.canPerform(pluginName + "-" + ImportCapability.ID)
+              && rsrc.getControl().isOwner());
+    }
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/importer/HttpModule.java
index 7a5bdfc..7b6edf0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/HttpModule.java
@@ -29,6 +29,8 @@
         .toInstance(new JavaScriptPlugin("resume-copy-project.js"));
     DynamicSet.bind(binder(), WebUiPlugin.class)
         .toInstance(new JavaScriptPlugin("resume-project-import.js"));
+    DynamicSet.bind(binder(), WebUiPlugin.class)
+        .toInstance(new JavaScriptPlugin("complete-project-import.js"));
 
     DynamicSet.bind(binder(), WebUiPlugin.class)
         .toInstance(new GwtPlugin("importer"));
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 76fefdd..74b8eb0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProject.java
@@ -97,6 +97,7 @@
   private final Project.NameKey targetProject;
   private Project.NameKey srcProject;
   private Project.NameKey parent;
+  private boolean force;
 
   private Writer err;
 
@@ -146,9 +147,10 @@
     }
   }
 
-  public Response<String> resume(String user, String pass, File importStatus)
-      throws RestApiException, OrmException, IOException, ValidationException,
-      GitAPIException, NoSuchChangeException, NoSuchAccountException {
+  public Response<String> resume(String user, String pass, boolean force,
+      File importStatus) throws RestApiException, OrmException, IOException,
+      ValidationException, GitAPIException, NoSuchChangeException,
+      NoSuchAccountException {
     LockFile lockFile = lockForImport();
     try {
       ImportProjectInfo info = ImportJson.parse(importStatus);
@@ -160,6 +162,8 @@
       input.name = info.name;
       input.parent = info.parent;
 
+      this.force = force;
+
       return apply(lockFile, input, info);
     } finally {
       lockFile.unlock();
@@ -189,7 +193,7 @@
         gitFetchStep.fetch(input.user, input.pass, repo, pm);
         configProjectStep.configure(targetProject, parent, pm);
         replayChangesFactory.create(input.from, input.user, input.pass, repo,
-            srcProject, targetProject, resume, pm).replay();
+            srcProject, targetProject, force, resume, pm).replay();
       } finally {
         repo.close();
       }
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 7ba24db..1ac50be 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ListImportedProjects.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ListImportedProjects.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Maps;
+import com.google.common.io.Files;
 import com.google.gerrit.extensions.annotations.PluginData;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -26,8 +27,10 @@
 import org.kohsuke.args4j.Option;
 
 import java.io.File;
-import java.io.FilenameFilter;
 import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Locale;
 import java.util.Map;
 
 @Singleton
@@ -59,14 +62,16 @@
     return importedProjects;
   }
 
-  private File[] listImportFiles() {
-    match = Strings.nullToEmpty(match);
-
-    return lockRoot.listFiles(new FilenameFilter() {
-      @Override
-      public boolean accept(File dir, String name) {
-        return !name.endsWith(".lock") && name.toLowerCase().contains(match);
+  private Collection<File> listImportFiles() {
+    match = Strings.nullToEmpty(match).toLowerCase(Locale.ENGLISH);
+    Collection<File> importFiles = new HashSet<>();
+    for (File f : Files.fileTreeTraverser().preOrderTraversal(lockRoot)) {
+      if (f.isFile()
+          && !f.getName().endsWith(".lock")
+          && f.getName().toLowerCase(Locale.ENGLISH).contains(match)) {
+        importFiles.add(f);
       }
-    });
+    }
+    return importFiles;
   }
 }
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 d703aa8..0a121be 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/Module.java
@@ -52,6 +52,7 @@
         put(PROJECT_KIND, "copy").to(CopyProject.class);
         put(PROJECT_KIND, "copy.resume").to(ResumeCopyProject.class);
         put(PROJECT_KIND, "import.resume").to(ResumeProjectImport.OnProjects.class);
+        post(PROJECT_KIND, "delete").to(CompleteProjectImport.OnProjects.class);
 
         child(CONFIG_KIND, "groups").to(GroupsCollection.class);
       }
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 3322095..55de14d 100755
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/RemoteApi.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/RemoteApi.java
@@ -14,6 +14,8 @@
 
 package com.googlesource.gerrit.plugins.importer;
 
+import static com.google.gerrit.extensions.restapi.Url.encode;
+
 import com.google.common.collect.Iterables;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -44,6 +46,7 @@
 
   public ProjectInfo getProject(String projectName) throws IOException,
       BadRequestException {
+    projectName = encode(projectName);
     String endPoint = "/projects/" + projectName;
     try (RestResponse r = checkedGet(endPoint)) {
       return newGson().fromJson(r.getReader(),
@@ -81,6 +84,7 @@
 
   public GroupInfo getGroup(String groupName) throws IOException,
       BadRequestException {
+    groupName = encode(groupName);
     String endPoint = "/groups/" + groupName + "/detail";
     try (RestResponse r = checkedGet(endPoint)) {
       return newGson().fromJson(r.getReader(),
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 e32d1ec..bffae8c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayChangesStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayChangesStep.java
@@ -53,7 +53,8 @@
         Repository repo,
         @Assisted("srcProject") Project.NameKey srcProject,
         @Assisted("targetProject") Project.NameKey targetProject,
-        boolean resume,
+        @Assisted("force") boolean force,
+        @Assisted("resume") boolean resume,
         ProgressMonitor pm);
   }
 
@@ -72,6 +73,7 @@
   private final Repository repo;
   private final Project.NameKey srcProject;
   private final Project.NameKey targetProject;
+  private final boolean force;
   private final boolean resume;
   private final ProgressMonitor pm;
 
@@ -93,7 +95,8 @@
       @Assisted Repository repo,
       @Assisted("srcProject") Project.NameKey srcProject,
       @Assisted("targetProject") Project.NameKey targetProject,
-      @Assisted boolean resume,
+      @Assisted("force") boolean force,
+      @Assisted("resume") boolean resume,
       @Assisted ProgressMonitor pm) {
     this.replayRevisionsFactory = replayRevisionsFactory;
     this.replayInlineCommentsFactory = replayInlineCommentsFactory;
@@ -110,6 +113,7 @@
     this.repo = repo;
     this.srcProject = srcProject;
     this.targetProject = targetProject;
+    this.force = force;
     this.resume = resume;
     this.pm = pm;
   }
@@ -147,7 +151,7 @@
       change = createChange(c);
     } else {
       resumeChange = true;
-      if (change.getLastUpdatedOn().equals(c.updated)) {
+      if (!force && change.getLastUpdatedOn().equals(c.updated)) {
         // change was not modified since last import
         return;
       }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ResumeProjectCommand.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ResumeProjectCommand.java
index 4aa3d05..5dfb115 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ResumeProjectCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ResumeProjectCommand.java
@@ -35,6 +35,9 @@
       usage = "password of remote user")
   private String pass;
 
+  @Option(name = "--force", usage = "Whether the resume should be done forcefully.")
+  private boolean force;
+
   @Option(name = "--quiet", usage = "suppress progress messages")
   private boolean quiet;
 
@@ -58,6 +61,7 @@
       ResumeProjectImport.Input input = new ResumeProjectImport.Input();
       input.user = user;
       input.pass = pass;
+      input.force = force;
       resume.apply(rsrc, input);
     } catch (RestApiException e) {
       throw die(e.getMessage());
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 521be04..ad172d4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ResumeProjectImport.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ResumeProjectImport.java
@@ -50,6 +50,7 @@
   public static class Input {
     public String user;
     public String pass;
+    public boolean force;
   }
 
   private final ImportProject.Factory importProjectFactory;
@@ -74,7 +75,8 @@
 
     ImportProject importer = importProjectFactory.create(rsrc.getName());
     importer.setErr(err);
-    return importer.resume(input.user, input.pass, rsrc.getImportStatus());
+    return importer.resume(input.user, input.pass, input.force,
+        rsrc.getImportStatus());
   }
 
   public static class OnProjects implements
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/client/ProjectImportsScreen.java b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ProjectImportsScreen.java
index d031708..874bb35 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/client/ProjectImportsScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ProjectImportsScreen.java
@@ -21,11 +21,8 @@
 import com.google.gerrit.plugin.client.Plugin;
 import com.google.gerrit.plugin.client.rpc.RestApi;
 import com.google.gerrit.plugin.client.screen.Screen;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlexTable;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.Label;
diff --git a/src/main/resources/Documentation/cmd-resume-project.md b/src/main/resources/Documentation/cmd-resume-project.md
index 8221c5f..f5c7ebd 100644
--- a/src/main/resources/Documentation/cmd-resume-project.md
+++ b/src/main/resources/Documentation/cmd-resume-project.md
@@ -11,6 +11,7 @@
 ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ resume-project \
   --user <USER> | -u <USER> \
   --pass - | <PASS> \
+  [--force] \
   [--quiet] \
   <NAME>
 ```
@@ -38,6 +39,11 @@
 `--user`
 :	User on remote system.
 
+`--force`
+:	Whether the resume should be done forcefully. On resume with force
+	changes that have the same last modified timestamp in the source
+	and target system are resumed, otherwise they will be skipped.
+
 `--quiet`
 :	Suppress progress messages.
 
diff --git a/src/main/resources/Documentation/rest-api-config.md b/src/main/resources/Documentation/rest-api-config.md
index 380e584..5655414 100644
--- a/src/main/resources/Documentation/rest-api-config.md
+++ b/src/main/resources/Documentation/rest-api-config.md
@@ -41,9 +41,6 @@
 
 Lists the imported projects.
 
-As result a map is returned that maps the project name to
-[ImportProjectInfo](#import-project-info) entity.
-
 Caller must be a member of a group that is granted the 'Import'
 capability (provided by this plugin) or the 'Administrate Server'
 capability.
@@ -58,6 +55,9 @@
   GET /config/server/@PLUGIN@~projects/?match=my HTTP/1.0
 ```
 
+As result a map is returned that maps the project name to
+[ImportProjectInfo](#import-project-info) entity.
+
 #### Response
 
 ```
@@ -196,7 +196,7 @@
   }
 ```
 
-### <a id="complete-project-import"> Resume Project Import
+### <a id="complete-project-import"> Complete Project Import
 _DELETE /config/server/@PLUGIN@~projects/[\{project-name\}](../../../Documentation/rest-api-projects.html#project-name)_
 
 Mark a project import as completed.
@@ -272,6 +272,9 @@
 
 * _user_: User on remote system.
 * _pass_: Password of remote user.
+* _force_: Whether the resume should be done forcefully. On resume with
+force changes that have the same last modified timestamp in the source
+and target system are resumed, otherwise they will be skipped.
 
 
 SEE ALSO
diff --git a/src/main/resources/Documentation/rest-api-projects.md b/src/main/resources/Documentation/rest-api-projects.md
index ebc443f..252b33d 100644
--- a/src/main/resources/Documentation/rest-api-projects.md
+++ b/src/main/resources/Documentation/rest-api-projects.md
@@ -73,6 +73,25 @@
   }
 ```
 
+### <a id="complete-project-import"> Complete Project Import
+_POST /projects/[\{project-name\}](../../../Documentation/rest-api-projects.html#project-name)/@PLUGIN@~delete)_
+
+Mark a project import as completed.
+
+Once a project import is completed it cannot be resumed any more.
+
+#### Request
+
+```
+  POST /projects/myProject/@PLUGIN@~delete HTTP/1.0
+```
+
+#### Response
+
+```
+  HTTP/1.1 204 No Content
+```
+
 
 <a id="json-entities">JSON Entities
 -----------------------------------
diff --git a/src/main/resources/static/complete-project-import.js b/src/main/resources/static/complete-project-import.js
new file mode 100644
index 0000000..059dc46
--- /dev/null
+++ b/src/main/resources/static/complete-project-import.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 onCompleteProjectImport(c) {
+      var b = c.button('Complete',
+        {onclick: function(){
+          c.call(
+            {},
+            function(r) {
+              c.hide();
+              window.alert('Import for project "'
+                + c.project
+                + '" was completed."'),
+              Gerrit.go('/admin/projects/' + c.project);
+            });
+        }});
+      c.popup(c.div(
+        c.msg('Complete import for project "'
+          + c.project
+          + '" ?'),
+        c.br(),
+        c.br(),
+        b));
+    }
+    self.onAction('project', 'delete', onCompleteProjectImport);
+  });
diff --git a/src/main/resources/static/resume-project-import.js b/src/main/resources/static/resume-project-import.js
index eaf752d..fdca63c 100644
--- a/src/main/resources/static/resume-project-import.js
+++ b/src/main/resources/static/resume-project-import.js
@@ -17,10 +17,11 @@
       var u = c.textfield();
       var p = c.textfield();
       p.type = 'password';
+      var f = c.checkbox();
       var b = c.button('Resume',
         {onclick: function(){
           c.call(
-            {user: u.value, pass: p.value},
+            {user: u.value, pass: p.value, force: f.checked},
             function(r) {
               c.hide();
               window.alert('Import for project "'
@@ -38,6 +39,8 @@
         c.br(),
         c.prependLabel('Password:', p),
         c.br(),
+        c.label(f, 'Force Resume'),
+        c.br(),
         c.br(),
         b));
     }