Merge "Do not fail import if hashtags cannot be set due to missing permissions"
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 eb7d2e0..eef151a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/AddApprovalsStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/AddApprovalsStep.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.errors.NoSuchAccountException;
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -35,6 +36,9 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -46,6 +50,9 @@
     AddApprovalsStep create(Change change, ChangeInfo changeInfo, boolean resume);
   }
 
+  private static final Logger log = LoggerFactory
+      .getLogger(ReplayInlineCommentsStep.class);
+
   private final AccountUtil accountUtil;
   private final ChangeUpdate.Factory updateFactory;
   private final ReviewDb db;
@@ -54,6 +61,7 @@
   private final Change change;
   private final ChangeInfo changeInfo;
   private final boolean resume;
+  private final String pluginName;
 
   @Inject
   public AddApprovalsStep(AccountUtil accountUtil,
@@ -61,6 +69,7 @@
       ReviewDb db,
       IdentifiedUser.GenericFactory genericUserFactory,
       ChangeControl.GenericFactory changeControlFactory,
+      @PluginName String pluginName,
       @Assisted Change change,
       @Assisted ChangeInfo changeInfo,
       @Assisted boolean resume) {
@@ -72,6 +81,7 @@
     this.change = change;
     this.changeInfo = changeInfo;
     this.resume = resume;
+    this.pluginName = pluginName;
   }
 
   void add(RemoteApi api) throws OrmException, NoSuchChangeException, IOException,
@@ -90,6 +100,15 @@
           Account.Id user = accountUtil.resolveUser(api, a);
           ChangeControl ctrl = control(change, a);
           LabelType labelType = ctrl.getLabelTypes().byLabel(labelName);
+          if(labelType == null) {
+            log.warn(String.format("[%s] Label '%s' not found in target system."
+                + " This label was referenced by an approval provided from '%s'"
+                + " for change '%s'."
+                + " This approval will be skipped. In order to import this"
+                + " approval configure the missing label and resume the import."
+                , pluginName, labelName, a.username, changeInfo.id));
+            continue;
+          }
           approvals.add(new PatchSetApproval(
               new PatchSetApproval.Key(change.currentPatchSetId(), user,
                   labelType.getLabelId()), a.value.shortValue(),
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/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/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);
+  });