Merge "Add project UI action to complete a project import"
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/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/ImportProjectMenu.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProjectMenu.java
index 50f5529..f470720 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProjectMenu.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProjectMenu.java
@@ -40,6 +40,7 @@
     List<MenuItem> projectItems = Lists.newArrayListWithExpectedSize(2);
     if (canImport()) {
       projectItems.add(new MenuItem("Import Project", "#/x/" + pluginName + "/project", ""));
+      projectItems.add(new MenuItem("List Imports", "#/x/" + pluginName + "/list", ""));
     }
     if (!projectItems.isEmpty()) {
       menuEntries.add(new MenuEntry("Projects", projectItems));
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/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/AccountInfo.java b/src/main/java/com/googlesource/gerrit/plugins/importer/client/AccountInfo.java
new file mode 100644
index 0000000..18b44fa
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/client/AccountInfo.java
@@ -0,0 +1,27 @@
+// 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.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class AccountInfo extends JavaScriptObject {
+  public final native int _account_id() /*-{ return this._account_id || 0; }-*/;
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String username() /*-{ return this.username; }-*/;
+  public final native String email() /*-{ return this.email; }-*/;
+
+  protected AccountInfo() {
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/client/CompleteImportDialog.java b/src/main/java/com/googlesource/gerrit/plugins/importer/client/CompleteImportDialog.java
new file mode 100644
index 0000000..5874d8a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/client/CompleteImportDialog.java
@@ -0,0 +1,115 @@
+// 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.client;
+
+import com.google.gerrit.plugin.client.Plugin;
+import com.google.gerrit.plugin.client.rpc.NoContent;
+import com.google.gerrit.plugin.client.rpc.RestApi;
+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.Button;
+import com.google.gwt.user.client.ui.DialogBox;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.AutoCenterDialogBox;
+
+public class CompleteImportDialog extends AutoCenterDialogBox {
+
+  private final Button cancelButton;
+  private final Button completeButton;
+
+  public CompleteImportDialog(final String project) {
+    super(/* auto hide */false, /* modal */true);
+    setGlassEnabled(true);
+    setText("Complete Project Import");
+
+    FlowPanel buttons = new FlowPanel();
+
+    completeButton = new Button();
+    completeButton.setText("Complete");
+    completeButton.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        hide();
+
+        new RestApi("config").id("server")
+            .view(Plugin.get().getName(), "projects").id(project)
+            .delete(new AsyncCallback<NoContent>() {
+              @Override
+              public void onSuccess(NoContent result) {
+                Plugin.get().go("/x/" + Plugin.get().getName() + "/list");
+
+                final DialogBox successDialog = new DialogBox();
+                successDialog.setText("Project Import Completed");
+                successDialog.setAnimationEnabled(true);
+
+                Panel p = new VerticalPanel();
+                p.setStyleName("importer-message-panel");
+                p.add(new Label("The project import was completed."));
+                Button okButton = new Button("OK");
+                okButton.addClickHandler(new ClickHandler() {
+                  public void onClick(ClickEvent event) {
+                    successDialog.hide();
+                  }
+                });
+
+                p.add(okButton);
+                successDialog.add(p);
+
+                successDialog.center();
+                successDialog.show();
+              }
+
+              @Override
+              public void onFailure(Throwable caught) {
+              }
+            });
+      }
+    });
+    buttons.add(completeButton);
+
+    cancelButton = new Button();
+    cancelButton.addStyleName("importer-cancel-button");
+    cancelButton.setText("Cancel");
+    cancelButton.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        hide();
+      }
+    });
+    buttons.add(cancelButton);
+
+    FlowPanel center = new FlowPanel();
+    Label msg = new Label("Complete import of project '" + project + "'");
+    msg.addStyleName("importer-complete-message");
+    center.add(msg);
+
+    center.add(buttons);
+    add(center);
+
+    setWidget(center);
+  }
+
+  @Override
+  public void center() {
+    super.center();
+    GlobalKey.dialog(this);
+    cancelButton.setFocus(true);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportActionPanel.java b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportActionPanel.java
new file mode 100644
index 0000000..408dd89
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportActionPanel.java
@@ -0,0 +1,39 @@
+// 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.client;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.FlowPanel;
+
+public class ImportActionPanel extends FlowPanel {
+
+  ImportActionPanel(final String project) {
+    setStyleName("importer-action-panel");
+    add(new Button("Resume...", new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        (new ResumeImportDialog(project)).center();
+      }
+    }));
+    add(new Button("Complete...", new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        (new CompleteImportDialog(project)).center();
+      }
+    }));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportInfo.java b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportInfo.java
new file mode 100644
index 0000000..82978f0
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportInfo.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.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ImportInfo extends JavaScriptObject {
+  public final native String timestamp() /*-{ return this.timestamp; }-*/;
+  public final native AccountInfo user() /*-{ return this.user; }-*/;
+  public final native String remoteUser() /*-{ return this.remote_user; }-*/;
+
+  protected ImportInfo() {
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportProjectInfo.java b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportProjectInfo.java
new file mode 100644
index 0000000..e89f058
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportProjectInfo.java
@@ -0,0 +1,28 @@
+// 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.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+
+public class ImportProjectInfo extends JavaScriptObject {
+  public final native String from() /*-{ return this.from; }-*/;
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String parent() /*-{ return this.parent; }-*/;
+  public final native JsArray<ImportInfo> imports() /*-{ return this.imports; }-*/;
+
+  protected ImportProjectInfo() {
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportProjectListScreen.java b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportProjectListScreen.java
new file mode 100644
index 0000000..36be3dc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportProjectListScreen.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.client;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.Natives;
+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.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.InlineHyperlink;
+import com.google.gwt.user.client.ui.VerticalPanel;
+
+import java.util.List;
+
+public class ImportProjectListScreen extends VerticalPanel {
+  static class Factory implements Screen.EntryPoint {
+    @Override
+    public void onLoad(Screen screen) {
+      screen.setPageTitle("Imported Projects");
+      screen.show(new ImportProjectListScreen());
+    }
+  }
+
+  ImportProjectListScreen() {
+    setStyleName("importer-imports-panel");
+
+    new RestApi("config").id("server")
+        .view(Plugin.get().getPluginName(), "projects")
+        .get(new AsyncCallback<NativeMap<ImportProjectInfo>>() {
+          @Override
+          public void onSuccess(NativeMap<ImportProjectInfo> info) {
+            display(info);
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            // never invoked
+          }
+        });
+  }
+
+  private void display(NativeMap<ImportProjectInfo> map) {
+    int columns = 5;
+    FlexTable t = new FlexTable();
+    t.setStyleName("importer-importProjectTable");
+    FlexCellFormatter fmt = t.getFlexCellFormatter();
+    for (int c = 0; c < columns; c++) {
+      fmt.addStyleName(0, c, "dataHeader");
+      fmt.addStyleName(0, c, "topMostCell");
+    }
+    fmt.addStyleName(0, 0, "leftMostCell");
+
+    t.setText(0, 0, "Project Name");
+    t.setText(0, 1, "From");
+    t.setText(0, 2, "Last Import By");
+    t.setText(0, 3, "Last Import At");
+    t.setText(0, 4, "Actions");
+
+    int row = 1;
+    for (final String project : map.keySet()) {
+      ImportProjectInfo info = map.get(project);
+
+      for (int c = 0; c < columns; c++) {
+        fmt.addStyleName(row, c, "dataCell");
+        fmt.addStyleName(row, 0, "leftMostCell");
+      }
+
+      t.setWidget(row, 0, new InlineHyperlink(
+          project, "/x/" + Plugin.get().getName() + "/projects/" + project));
+
+      String srcProjectUrl = projectUrl(info, project);
+      t.setWidget(row, 1, new Anchor(srcProjectUrl, srcProjectUrl));
+
+      List<ImportInfo> importList = Natives.asList(info.imports());
+      if (!importList.isEmpty()) {
+        ImportInfo lastImportInfo = importList.get(importList.size() - 1);
+        t.setText(row, 2, lastImportInfo.user().username());
+        t.setText(row, 3, removeNs(lastImportInfo.timestamp()));
+      } else {
+        t.setText(row, 2, "N/A");
+        t.setText(row, 3, "N/A");
+      }
+
+      t.setWidget(row, 4, new ImportActionPanel(project));
+
+      row++;
+    }
+
+    add(t);
+  }
+
+  public static String removeNs(String timestamp) {
+    return timestamp.substring(0, timestamp.lastIndexOf('.'));
+  }
+
+  public static String projectUrl(ImportProjectInfo info, String project) {
+    return ensureSlash(info.from())
+        + "#/admin/projects/"
+        + (info.name() != null ? info.name() : project);
+  }
+
+  private static String ensureSlash(String in) {
+    if (in != null && !in.endsWith("/")) {
+      return in + "/";
+    }
+    return in;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportProjectScreen.java b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportProjectScreen.java
index c24c2c3..f65ae5c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportProjectScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImportProjectScreen.java
@@ -14,25 +14,22 @@
 
 package com.googlesource.gerrit.plugins.importer.client;
 
+import static com.googlesource.gerrit.plugins.importer.client.TextBoxUtil.addPasswordTextBox;
+import static com.googlesource.gerrit.plugins.importer.client.TextBoxUtil.addTextBox;
+import static com.googlesource.gerrit.plugins.importer.client.TextBoxUtil.getValue;
+
 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.core.client.JavaScriptObject;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.DialogBox;
 import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.PasswordTextBox;
 import com.google.gwt.user.client.ui.TextBox;
 import com.google.gwt.user.client.ui.VerticalPanel;
 
@@ -55,13 +52,13 @@
   ImportProjectScreen() {
     setStyleName("importer-import-panel");
 
-    fromTxt = addTextBox("From*", "URL of the remote system from where the project should be imported");
-    srcNameTxt = addTextBox("Project Name in Source*", "name of project in source system");
-    targetNameTxt = addTextBox("Target Project Name", "name of project in target system"
+    fromTxt = addTextBox(this, "From*", "URL of the remote system from where the project should be imported");
+    srcNameTxt = addTextBox(this, "Project Name in Source*", "name of project in source system");
+    targetNameTxt = addTextBox(this, "Target Project Name", "name of project in target system"
         + " (if not specified it is assumed to be the same name as in the source system)");
-    userTxt = addTextBox("Remote User*", "user on remote system");
-    passTxt = addPasswordTextBox("Password*", "password of remote user");
-    parentTxt = addTextBox("Parent", "name of parent project in target system"
+    userTxt = addTextBox(this, "Remote User*", "user on remote system");
+    passTxt = addPasswordTextBox(this, "Password*", "password of remote user");
+    parentTxt = addTextBox(this, "Parent", "name of parent project in target system"
         + "(if not specified it is assumed to be the same parent as in the source system)");
 
     HorizontalPanel buttons = new HorizontalPanel();
@@ -83,77 +80,6 @@
     importButton.setEnabled(false);
   }
 
-  private TextBox addTextBox(String label, String infoMsg) {
-    return addTextBox(label, infoMsg, false);
-  }
-
-  private TextBox addPasswordTextBox(String label, String infoMsg) {
-    return addTextBox(label, infoMsg, true);
-  }
-
-  private TextBox addTextBox(String label, String infoMsg, boolean isPassword) {
-    HorizontalPanel hp = new HorizontalPanel();
-    hp.add(new Label(label));
-    Image info = new Image(ImporterPlugin.RESOURCES.info());
-    info.setTitle(infoMsg);
-    hp.add(info);
-    hp.add(new Label(":"));
-
-    Panel p = new VerticalPanel();
-    p.add(hp);
-    TextBox tb = createTextBox(isPassword);
-    p.add(tb);
-    add(p);
-    return tb;
-  }
-
-  private static TextBox createTextBox(boolean isPassword) {
-    TextBox tb;
-    if (isPassword) {
-      tb = new PasswordTextBox() {
-        @Override
-        public void onBrowserEvent(Event event) {
-          super.onBrowserEvent(event);
-          handlePaste(this, event);
-        }
-      };
-    } else {
-      tb = new TextBox() {
-        @Override
-        public void onBrowserEvent(Event event) {
-          super.onBrowserEvent(event);
-          handlePaste(this, event);
-        }
-      };
-    }
-    tb.addKeyPressHandler(new KeyPressHandler() {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        event.stopPropagation();
-      }
-    });
-    tb.sinkEvents(Event.ONPASTE);
-    tb.setVisibleLength(40);
-    return tb;
-  }
-
-  private static void handlePaste(final TextBox tb, Event event) {
-    if (event.getTypeInt() == Event.ONPASTE) {
-      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
-        @Override
-        public void execute() {
-          if (getValue(tb).length() != 0) {
-            tb.setEnabled(true);
-          }
-        }
-      });
-    }
-  }
-
-  private static String getValue(TextBox tb) {
-    return tb.getValue().trim();
-  }
-
   private void doImport() {
     final String targetName = getValue(targetNameTxt).length() == 0
         ? getValue(srcNameTxt) : getValue(targetNameTxt);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImporterPlugin.java b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImporterPlugin.java
index 88473d9..c1e9973 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImporterPlugin.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ImporterPlugin.java
@@ -25,5 +25,7 @@
   @Override
   public void onPluginLoad() {
     Plugin.get().screen("project", new ImportProjectScreen.Factory());
+    Plugin.get().screen("list", new ImportProjectListScreen.Factory());
+    Plugin.get().screenRegex("projects/(.*)", new ProjectImportsScreen.Factory());
   }
 }
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
new file mode 100644
index 0000000..874bb35
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ProjectImportsScreen.java
@@ -0,0 +1,119 @@
+// 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.client;
+
+import static com.googlesource.gerrit.plugins.importer.client.ImportProjectListScreen.projectUrl;
+import static com.googlesource.gerrit.plugins.importer.client.ImportProjectListScreen.removeNs;
+
+import com.google.gerrit.client.rpc.Natives;
+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.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Anchor;
+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;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ProjectImportsScreen extends VerticalPanel {
+  static class Factory implements Screen.EntryPoint {
+    @Override
+    public void onLoad(Screen screen) {
+      screen.setPageTitle("Imports of  " + screen.getToken(1));
+      screen.show(new ProjectImportsScreen(screen.getToken(1)));
+    }
+  }
+
+  ProjectImportsScreen(final String project) {
+    setStyleName("importer-import-panel");
+
+    new RestApi("config").id("server").view(Plugin.get().getPluginName(), "projects")
+        .id(project).get(new AsyncCallback<ImportProjectInfo>() {
+            @Override
+            public void onSuccess(ImportProjectInfo importProjectInfo) {
+              display(project, importProjectInfo);
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+              // never invoked
+            }
+          });
+  }
+
+  private void display(final String project, ImportProjectInfo info) {
+    MyTable t = new MyTable();
+    t.setStyleName("importer-projectImportInfoTable");
+    t.addRow("Project Name", project);
+    String srcProjectUrl = projectUrl(info, project);
+    t.addRow("From", new Anchor(srcProjectUrl, srcProjectUrl));
+    t.addRow("Parent", info.parent());
+    t.addRow("Actions", new ImportActionPanel(project));
+    add(t);
+
+    add(new Label("Imports:"));
+    int columns = 3;
+    FlexTable importsTable = new FlexTable();
+    importsTable.setStyleName("importer-importProjectTable");
+    FlexCellFormatter fmt = importsTable.getFlexCellFormatter();
+    for (int c = 0; c < columns; c++) {
+      fmt.addStyleName(0, c, "dataHeader");
+      fmt.addStyleName(0, c, "topMostCell");
+    }
+    fmt.addStyleName(0, 0, "leftMostCell");
+
+    importsTable.setText(0, 0, "Timestamp");
+    importsTable.setText(0, 1, "User");
+    importsTable.setText(0, 2, "Remote User");
+    int row = 1;
+    List<ImportInfo> imports = Natives.asList(info.imports());
+    Collections.reverse(imports);
+    for (ImportInfo importInfo : imports) {
+      for (int c = 0; c < columns; c++) {
+        fmt.addStyleName(row, c, "dataCell");
+        fmt.addStyleName(row, 0, "leftMostCell");
+      }
+
+      importsTable.setText(row, 0, removeNs(importInfo.timestamp()));
+      importsTable.setText(row, 1, importInfo.user().username());
+      importsTable.setText(row, 2, importInfo.remoteUser());
+
+      row++;
+    }
+
+    add(importsTable);
+  }
+
+  private static class MyTable extends FlexTable {
+    private static int row = 0;
+
+    private void addRow(String label, String value) {
+      setWidget(row, 0, new Label(label + ":"));
+      setWidget(row, 1, new Label(value));
+      row++;
+    }
+
+    private void addRow(String label, Widget w) {
+      setWidget(row, 0, new Label(label + ":"));
+      setWidget(row, 1, w);
+      row++;
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/client/ResumeImportDialog.java b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ResumeImportDialog.java
new file mode 100644
index 0000000..73488f4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ResumeImportDialog.java
@@ -0,0 +1,129 @@
+// 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.client;
+
+import static com.googlesource.gerrit.plugins.importer.client.TextBoxUtil.addPasswordTextBox;
+import static com.googlesource.gerrit.plugins.importer.client.TextBoxUtil.addTextBox;
+import static com.googlesource.gerrit.plugins.importer.client.TextBoxUtil.getValue;
+
+import com.google.gerrit.plugin.client.Plugin;
+import com.google.gerrit.plugin.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+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.Button;
+import com.google.gwt.user.client.ui.DialogBox;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.AutoCenterDialogBox;
+
+public class ResumeImportDialog extends AutoCenterDialogBox {
+
+  private final Button cancelButton;
+  private final Button resumeButton;
+  private final TextBox userTxt;
+  private final TextBox passTxt;
+
+  public ResumeImportDialog(final String project) {
+    super(/* auto hide */false, /* modal */true);
+    setGlassEnabled(true);
+    setText("Resume Project Import");
+
+    FlowPanel buttons = new FlowPanel();
+
+    resumeButton = new Button();
+    resumeButton.setText("Resume");
+    resumeButton.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        hide();
+
+        ResumeImportProjectInput in = ResumeImportProjectInput.create();
+        in.user(getValue(userTxt));
+        in.pass(getValue(passTxt));
+
+        new RestApi("config").id("server")
+            .view(Plugin.get().getName(), "projects").id(project)
+            .view("resume").put(in, new AsyncCallback<JavaScriptObject>() {
+              @Override
+              public void onSuccess(JavaScriptObject result) {
+                Plugin.get().go("/admin/projects/" + project);
+
+                final DialogBox successDialog = new DialogBox();
+                successDialog.setText("Resume Project Import");
+                successDialog.setAnimationEnabled(true);
+
+                Panel p = new VerticalPanel();
+                p.setStyleName("importer-message-panel");
+                p.add(new Label("The project import was resumed."));
+                Button okButton = new Button("OK");
+                okButton.addClickHandler(new ClickHandler() {
+                  public void onClick(ClickEvent event) {
+                    successDialog.hide();
+                  }
+                });
+
+                p.add(okButton);
+                successDialog.add(p);
+
+                successDialog.center();
+                successDialog.show();
+              }
+
+              @Override
+              public void onFailure(Throwable caught) {
+              }
+            });
+      }
+    });
+    buttons.add(resumeButton);
+
+    cancelButton = new Button();
+    cancelButton.addStyleName("importer-cancel-button");
+    cancelButton.setText("Cancel");
+    cancelButton.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        hide();
+      }
+    });
+    buttons.add(cancelButton);
+
+    FlowPanel center = new FlowPanel();
+    Label msg = new Label("Resume import of project '" + project + "'");
+    msg.addStyleName("importer-resume-message");
+    center.add(msg);
+
+    userTxt = addTextBox(center, "Remote User*", "user on remote system");
+    passTxt = addPasswordTextBox(center, "Password*", "password of remote user");
+
+    center.add(buttons);
+    add(center);
+
+    setWidget(center);
+  }
+
+  @Override
+  public void center() {
+    super.center();
+    GlobalKey.dialog(this);
+    cancelButton.setFocus(true);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/client/ResumeImportProjectInput.java b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ResumeImportProjectInput.java
new file mode 100644
index 0000000..1635882
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/client/ResumeImportProjectInput.java
@@ -0,0 +1,29 @@
+// 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.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ResumeImportProjectInput extends JavaScriptObject {
+  final native void user(String u) /*-{ this.user = u; }-*/;
+  final native void pass(String p) /*-{ this.pass = p; }-*/;
+
+  static ResumeImportProjectInput create() {
+    return (ResumeImportProjectInput) createObject();
+  }
+
+  protected ResumeImportProjectInput() {
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/client/TextBoxUtil.java b/src/main/java/com/googlesource/gerrit/plugins/importer/client/TextBoxUtil.java
new file mode 100644
index 0000000..81d56c8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/client/TextBoxUtil.java
@@ -0,0 +1,101 @@
+// 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.client;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.PasswordTextBox;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.VerticalPanel;
+
+public class TextBoxUtil {
+  public static TextBox addTextBox(Panel p, String label, String infoMsg) {
+    return addTextBox(p, label, infoMsg, false);
+  }
+
+  public static TextBox addPasswordTextBox(Panel p, String label, String infoMsg) {
+    return addTextBox(p, label, infoMsg, true);
+  }
+
+  public static TextBox addTextBox(Panel p, String label, String infoMsg, boolean isPassword) {
+    HorizontalPanel hp = new HorizontalPanel();
+    hp.add(new Label(label));
+    Image info = new Image(ImporterPlugin.RESOURCES.info());
+    info.setTitle(infoMsg);
+    hp.add(info);
+    hp.add(new Label(":"));
+
+    Panel vp = new VerticalPanel();
+    vp.add(hp);
+    TextBox tb = createTextBox(isPassword);
+    vp.add(tb);
+    p.add(vp);
+    return tb;
+  }
+
+  private static TextBox createTextBox(boolean isPassword) {
+    TextBox tb;
+    if (isPassword) {
+      tb = new PasswordTextBox() {
+        @Override
+        public void onBrowserEvent(Event event) {
+          super.onBrowserEvent(event);
+          handlePaste(this, event);
+        }
+      };
+    } else {
+      tb = new TextBox() {
+        @Override
+        public void onBrowserEvent(Event event) {
+          super.onBrowserEvent(event);
+          handlePaste(this, event);
+        }
+      };
+    }
+    tb.addKeyPressHandler(new KeyPressHandler() {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        event.stopPropagation();
+      }
+    });
+    tb.sinkEvents(Event.ONPASTE);
+    tb.setVisibleLength(40);
+    return tb;
+  }
+
+  private static void handlePaste(final TextBox tb, Event event) {
+    if (event.getTypeInt() == Event.ONPASTE) {
+      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+        @Override
+        public void execute() {
+          if (getValue(tb).length() != 0) {
+            tb.setEnabled(true);
+          }
+        }
+      });
+    }
+  }
+
+  public static String getValue(TextBox tb) {
+    return tb.getValue().trim();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/public/importer.css b/src/main/java/com/googlesource/gerrit/plugins/importer/public/importer.css
index d3e79ea..5eef977 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/public/importer.css
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/public/importer.css
@@ -13,10 +13,60 @@
  * limitations under the License.
  */
 
-.importer-import-panel, .importer-message-panel {
+.importer-import-panel,
+.importer-message-panel,
+.importer-imports-panel {
   border-spacing: 0px 5px;
 }
 
 .importer-importButton {
   margin-left: 10px !important;
 }
+
+.importer-importProjectTable {
+  border-collapse: separate;
+  border-spacing: 0;
+}
+
+.importer-importProjectTable .leftMostCell {
+  border-left: 1px solid #EEE;
+}
+
+.importer-importProjectTable .topMostCell {
+  border-top: 1px solid #EEE;
+}
+
+.importer-importProjectTable .dataHeader {
+  border: 1px solid #FFF;
+  padding: 2px 6px 1px;
+  background-color: #EEE;
+  font-style: italic;
+  white-space: nowrap;
+  color: textColor;
+}
+
+.importer-importProjectTable .dataCell {
+  padding-left: 5px;
+  padding-right: 5px;
+  border-right: 1px solid #EEE;
+  border-bottom: 1px solid #EEE;
+  vertical-align: middle;
+  height: 20px;
+}
+
+.importer-cancel-button {
+  margin-left: 150px;
+}
+
+.importer-resume-message,
+.importer-complete-message {
+  margin-bottom: 10px;
+}
+
+.importer-action-panel .gwt-Button {
+  margin: 2px;
+}
+
+.importer-projectImportInfoTable {
+  margin-bottom: 10px;
+}
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/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));
     }