Merge "Return number of created/updated changes on import/resume import"
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/AddHashtagsStep.java b/src/main/java/com/googlesource/gerrit/plugins/importer/AddHashtagsStep.java
index f3bf629..f8aaef9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/AddHashtagsStep.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/AddHashtagsStep.java
@@ -14,11 +14,13 @@
 
 package com.googlesource.gerrit.plugins.importer;
 
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.change.HashtagsUtil;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -27,6 +29,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.HashSet;
 
@@ -36,9 +41,13 @@
     AddHashtagsStep create(Change change, ChangeInfo changeInfo, boolean resume);
   }
 
+  private static final Logger log = LoggerFactory
+      .getLogger(AddHashtagsStep.class);
+
   private final HashtagsUtil hashtagsUtil;
   private final CurrentUser currentUser;
   private final ChangeControl.GenericFactory changeControlFactory;
+  private final String pluginName;
   private final Change change;
   private final ChangeInfo changeInfo;
   private final boolean resume;
@@ -47,14 +56,16 @@
   AddHashtagsStep(HashtagsUtil hashtagsUtil,
       CurrentUser currentUser,
       ChangeControl.GenericFactory changeControlFactory,
+      @PluginName String pluginName,
       @Assisted Change change,
       @Assisted ChangeInfo changeInfo,
       @Assisted boolean resume) {
     this.hashtagsUtil = hashtagsUtil;
-    this.change = change;
-    this.changeInfo = changeInfo;
     this.currentUser = currentUser;
     this.changeControlFactory = changeControlFactory;
+    this.pluginName = pluginName;
+    this.change = change;
+    this.changeInfo = changeInfo;
     this.resume = resume;
   }
 
@@ -62,14 +73,23 @@
       ValidationException, OrmException, NoSuchChangeException {
     ChangeControl ctrl = changeControlFactory.controlFor(change, currentUser);
 
-    if (resume) {
-      HashtagsInput input = new HashtagsInput();
-      input.remove = ctrl.getNotes().load().getHashtags();
-      hashtagsUtil.setHashtags(ctrl, input, false, false);
-    }
+    try {
+      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(ctrl, input, false, false);
+      HashtagsInput input = new HashtagsInput();
+      input.add = new HashSet<>(changeInfo.hashtags);
+      hashtagsUtil.setHashtags(ctrl, input, false, false);
+    } catch (AuthException e) {
+      log.warn(String.format(
+          "[%s] Hashtags cannot be set on change %s because the importing"
+              + " user %s doesn't have permissions to edit hashtags"
+              + " (e.g. assign the 'Edit Hashtags' global capability"
+              + " and resume the import with the force option).",
+          pluginName, ChangeTriplet.format(change), currentUser.getUserName()));
+    }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/GroupCommand.java b/src/main/java/com/googlesource/gerrit/plugins/importer/GroupCommand.java
index 54f65bb..a9b2d58 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/GroupCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/GroupCommand.java
@@ -19,17 +19,14 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.config.ConfigResource;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.validators.ValidationException;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -60,18 +57,19 @@
   private ImportGroup.Factory importGroupFactory;
 
   @Override
-  protected void run() throws OrmException, IOException, UnloggedFailure,
-      ValidationException, GitAPIException, NoSuchChangeException,
+  protected void run() throws UnloggedFailure, OrmException, IOException,
       NoSuchAccountException {
     ImportGroup.Input input = new ImportGroup.Input();
     input.from = url;
     input.user = user;
     input.pass = getPassword();
 
-    Response<String> response = importGroupFactory.create(new AccountGroup.NameKey(group)).apply(
-        new ConfigResource(), input);
-    stdout.println(response);
-
+    try {
+      importGroupFactory.create(new AccountGroup.NameKey(group)).apply(
+          new ConfigResource(), input);
+    } catch (RestApiException e){
+      throw die(e.getMessage());
+    }
   }
 
   private String getPassword() throws IOException, UnloggedFailure {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportGroup.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportGroup.java
index 8b79900..7e3a278 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportGroup.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportGroup.java
@@ -14,17 +14,46 @@
 
 package com.googlesource.gerrit.plugins.importer;
 
+import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.PreconditionFailedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.CreateGroupArgs;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gerrit.server.validators.GroupCreationValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import com.googlesource.gerrit.plugins.importer.ImportGroup.Input;
 
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
 @RequiresCapability(ImportCapability.ID)
 class ImportGroup implements RestModifyView<ConfigResource, Input> {
   public static class Input {
@@ -39,16 +68,165 @@
 
   private final GroupCache groupCache;
   private final AccountGroup.NameKey group;
+  private final ReviewDb db;
+  private final AccountUtil accountUtil;
+  private final AccountCache accountCache;
+  private final GroupIncludeCache groupIncludeCache;
+  private final Config cfg;
+  private final DynamicSet<GroupCreationValidationListener> groupCreationValidationListeners;
+  private RemoteApi api;
 
   @Inject
-  ImportGroup(GroupCache groupCache, @Assisted AccountGroup.NameKey group) {
+  ImportGroup(AccountUtil accountUtil, GroupCache groupCache,
+      AccountCache accountCache, GroupIncludeCache groupIncludeCache,
+      ReviewDb db, @Assisted AccountGroup.NameKey group,
+      @GerritServerConfig Config cfg,
+      DynamicSet<GroupCreationValidationListener> groupCreationValidationListeners) {
+    this.db = db;
+    this.accountUtil = accountUtil;
     this.groupCache = groupCache;
+    this.accountCache = accountCache;
+    this.groupIncludeCache = groupIncludeCache;
+    this.cfg = cfg;
+    this.groupCreationValidationListeners = groupCreationValidationListeners;
     this.group = group;
   }
 
   @Override
-  public Response<String> apply(ConfigResource rsrc, Input input) {
-    return Response.<String> ok("TODO");
+  public Response<String> apply(ConfigResource rsrc, Input input)
+      throws ResourceConflictException, PreconditionFailedException,
+      BadRequestException, NoSuchAccountException, OrmException, IOException {
+    GroupInfo groupInfo;
+    this.api = new RemoteApi(input.from, input.user, input.pass);
+    groupInfo = api.getGroup(group.get());
+    validate(groupInfo);
+    createGroup(groupInfo);
+
+    return Response.<String> ok("OK");
+  }
+
+  private void validate(GroupInfo groupInfo) throws ResourceConflictException,
+      PreconditionFailedException, BadRequestException, IOException,
+      OrmException, NoSuchAccountException {
+    if (groupCache.get(new AccountGroup.NameKey(groupInfo.name)) != null) {
+      throw new ResourceConflictException(String.format(
+          "Group with name %s already exists", groupInfo.name));
+    }
+    if (groupCache.get(new AccountGroup.UUID(groupInfo.id)) != null) {
+      throw new ResourceConflictException(String.format(
+          "Group with UUID %s already exists", groupInfo.id));
+    }
+    if (!groupInfo.id.equals(groupInfo.ownerId))
+      if (groupCache.get(new AccountGroup.UUID(groupInfo.ownerId)) == null) {
+        throw new PreconditionFailedException(String.format(
+            "Owner group with UUID %s does not exist", groupInfo.ownerId));
+      }
+    for (AccountInfo member : groupInfo.members) {
+      try {
+        accountUtil.resolveUser(api, member);
+      } catch (NoSuchAccountException e) {
+        throw new PreconditionFailedException(e.getMessage());
+      }
+    }
+    for (GroupInfo include : groupInfo.includes) {
+      if (groupCache.get(new AccountGroup.UUID(include.id)) == null) {
+        throw new PreconditionFailedException(String.format(
+            "Included group with UUID %s does not exist", include.id));
+      }
+    }
+
+    for (GroupCreationValidationListener l : groupCreationValidationListeners) {
+      try {
+        l.validateNewGroup(toCreateGroupArgs(groupInfo));
+      } catch (ValidationException e) {
+        throw new ResourceConflictException(e.getMessage(), e);
+      }
+    }
+  }
+
+  private CreateGroupArgs toCreateGroupArgs(GroupInfo groupInfo)
+      throws IOException, OrmException, BadRequestException,
+      NoSuchAccountException {
+    CreateGroupArgs args = new CreateGroupArgs();
+    args.setGroupName(groupInfo.name);
+    args.groupDescription = groupInfo.description;
+    args.visibleToAll = cfg.getBoolean("groups", "newGroupsVisibleToAll", false);
+    if (!groupInfo.ownerId.equals(groupInfo.id)) {
+      args.ownerGroupId =
+          groupCache.get(new AccountGroup.UUID(groupInfo.ownerId)).getId();
+    }
+    Set<Account.Id> initialMembers = new HashSet<>();
+    for (AccountInfo member : groupInfo.members) {
+      initialMembers.add(accountUtil.resolveUser(api, member));
+    }
+    args.initialMembers = initialMembers;
+    Set<AccountGroup.UUID> initialGroups = new HashSet<>();
+    for (GroupInfo member : groupInfo.includes) {
+      initialGroups.add(new AccountGroup.UUID(member.id));
+    }
+    args.initialGroups = initialGroups;
+    return args;
+  }
+
+  private AccountGroup createGroup(GroupInfo info) throws OrmException,
+      ResourceConflictException, NoSuchAccountException, BadRequestException,
+      IOException {
+    AccountGroup.Id groupId = new AccountGroup.Id(db.nextAccountGroupId());
+    AccountGroup.UUID uuid = new AccountGroup.UUID(info.id);
+    AccountGroup group =
+        new AccountGroup(new AccountGroup.NameKey(info.name), groupId, uuid);
+    group.setVisibleToAll(cfg.getBoolean("groups", "newGroupsVisibleToAll",
+        false));
+    group.setDescription(info.description);
+    AccountGroupName gn = new AccountGroupName(group);
+    // first insert the group name to validate that the group name hasn't
+    // already been used to create another group
+    try {
+      db.accountGroupNames().insert(Collections.singleton(gn));
+    } catch (OrmDuplicateKeyException e) {
+      throw new ResourceConflictException(info.name);
+    }
+    db.accountGroups().insert(Collections.singleton(group));
+
+    addMembers(groupId, info.members);
+    addGroups(groupId, info.includes);
+
+    groupCache.evict(group);
+
+    return group;
+  }
+
+  private void addMembers(AccountGroup.Id groupId, List<AccountInfo> members)
+      throws OrmException, NoSuchAccountException, BadRequestException,
+      IOException {
+    List<AccountGroupMember> memberships = new ArrayList<>();
+    for (AccountInfo member : members) {
+      Account.Id userId = accountUtil.resolveUser(api, member);
+      AccountGroupMember membership =
+          new AccountGroupMember(new AccountGroupMember.Key(userId, groupId));
+      memberships.add(membership);
+    }
+    db.accountGroupMembers().insert(memberships);
+
+    for (AccountInfo member : members) {
+      accountCache.evict(accountUtil.resolveUser(api, member));
+    }
+  }
+
+  private void addGroups(AccountGroup.Id groupId, List<GroupInfo> includedGroups)
+      throws OrmException {
+    List<AccountGroupById> includeList = new ArrayList<>();
+    for (GroupInfo includedGroup : includedGroups) {
+      AccountGroup.UUID memberUUID = new AccountGroup.UUID(includedGroup.id);
+      AccountGroupById groupInclude =
+          new AccountGroupById(new AccountGroupById.Key(groupId, memberUUID));
+      includeList.add(groupInclude);
+    }
+    db.accountGroupById().insert(includeList);
+
+    for (GroupInfo member : includedGroups) {
+      groupIncludeCache.evictParentGroupsOf(new AccountGroup.UUID(member.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 abc04da..935d588 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProject.java
@@ -19,7 +19,6 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.errors.NoSuchAccountException;
-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.ResourceConflictException;
@@ -91,7 +90,7 @@
   private final Provider<CurrentUser> currentUser;
   private final ImportJson importJson;
   private final ImportLog importLog;
-  private final File lockRoot;
+  private final ProjectsCollection projects;
 
   private final Project.NameKey targetProject;
   private Project.NameKey srcProject;
@@ -111,7 +110,7 @@
       Provider<CurrentUser> currentUser,
       ImportJson importJson,
       ImportLog importLog,
-      @PluginData File data,
+      ProjectsCollection projects,
       @Assisted Project.NameKey targetProject) {
     this.projectCache = projectCache;
     this.openRepoStep = openRepoStep;
@@ -122,7 +121,8 @@
     this.currentUser = currentUser;
     this.importJson = importJson;
     this.importLog = importLog;
-    this.lockRoot = data;
+    this.projects = projects;
+
     this.targetProject = targetProject;
   }
 
@@ -247,7 +247,7 @@
   }
 
   private LockFile lockForImport() throws ResourceConflictException {
-    File importStatus = new File(lockRoot, targetProject.get());
+    File importStatus = projects.FS_LAYOUT.getImportStatusFile(targetProject.get());
     LockFile lockFile = new LockFile(importStatus, FS.DETECTED);
     try {
       if (lockFile.lock()) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectsCollection.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectsCollection.java
index 80594ab..81dfc31 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectsCollection.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ProjectsCollection.java
@@ -37,6 +37,14 @@
     ChildCollection<ConfigResource, ImportProjectResource>,
     AcceptsCreate<ConfigResource> {
 
+  class FileSystemLayout {
+    File getImportStatusFile(String id) {
+      return new File(lockRoot, id);
+    }
+  }
+
+  public final FileSystemLayout FS_LAYOUT = new FileSystemLayout();
+
   private final DynamicMap<RestView<ImportProjectResource>> views;
   private final ImportProject.Factory importProjectFactory;
   private final Provider<ListImportedProjects> list;
@@ -67,7 +75,7 @@
 
   public ImportProjectResource parse(String id)
       throws ResourceNotFoundException {
-    File f = new File(lockRoot, id);
+    File f = FS_LAYOUT.getImportStatusFile(id);
     if (!f.exists()) {
       throw new ResourceNotFoundException(id);
     }
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..b056bf7
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,273 @@
+The @PLUGIN@ plugin allows to import projects and groups from a remote
+Gerrit server into the Gerrit server where the plugin is installed.
+
+The imports are done online while both, source and target Gerrit
+server, are running.
+
+The user that does the import must be a member of a group that is
+granted the 'Import' capability (provided by this plugin) or the
+'Administrate Server' capability.
+
+Importing also requires a user/password for the source Gerrit server.
+This user must be able to see the entities that should imported.
+
+The data for the import is retrieved by accessing the Gerrit REST API
+on the source Gerrit server. Additionally project imports fetch from
+the repositories on the source Gerrit server. The git operations are
+done over the HTTP protocol.
+
+Imports are done in several steps:
+
+* do initial import
+* [optional] resume import as many times as you want
+* complete the import
+
+Until an import is completed, the imported entities should not be
+modified in the target Gerrit server, otherwise resuming the import may
+fail or even override modifications done in the target Gerrit server
+(e.g. change approvals will be overridden). But depending on what was
+modified, it may also just work.
+
+Imports are logged in 'review\_site/logs/import\_log' so that
+administrators can see who imported when which project. Imports do also
+send audit events.
+
+<a id="project-import">
+### Project Import
+
+The import project functionality allows to import (move) a project from
+one Gerrit server to another Gerrit server.
+
+<a id="project-import-process">
+#### Process
+
+A project import would be done in several steps:
+
+* do the initial import of the project
+* test on the target Gerrit that everything is okay
+* inform the project team about the project move and disallow further
+  modifications of the project in source Gerrit server (e.g. by
+  permissions or by setting the project state to read-only)
+* resume the project import to get all modifications which have been
+  done after the initial import
+* complete the import and if needed make project in the target Gerrit
+  server writable
+* inform the project team that they can now work on the project in the
+  target Gerrit server
+* reconfigure any third-party tools (such as Jenkins) to work against
+  the project in the target Gerrit server
+* [optionally] delete the project in the source Gerrit server using the
+  [delete-project](https://gerrit.googlesource.com/plugins/delete-project/+doc/master/src/main/resources/Documentation/about.md)
+  plugin
+
+Doing an initial import first and resuming the import later has the
+advantage that the downtime for the project team can be kept minimal.
+The initial project import may take some time, but the resume should be
+fast since it only needs to transfer the delta since the initial (last)
+import.
+
+<a id="project-import-preconditions">
+#### Preconditions
+
+Preconditions for a project import:
+
+* The parent project of the imported project must already exist in the
+  target Gerrit server.
+* User accounts must exist in the target Gerrit server unless auth type
+  is 'LDAP', 'HTTP\_LDAP' or 'CLIENT\_SSL\_CERT\_LDAP'.
+
+For auth type 'LDAP', 'HTTP\_LDAP' or 'CLIENT\_SSL\_CERT\_LDAP' missing
+user accounts are automatically created. The public SSH keys of a user
+are automatically retrieved from the source Gerrit server and added to
+the new account in the target Gerrit server.
+
+Gerrit internal users (e.g. service users) are never automatically
+created but must be created in the target Gerrit server before the
+import.
+
+<a id="project-import-commands">
+#### Commands
+
+Importing a project can be done via
+
+* [REST](rest-api-config.html#import-project)
+* [SSH](cmd-project.html) and
+* UI from menu 'Projects' > 'Import Project'
+
+Resuming a project import can be done via
+
+* [REST](rest-api-config.html#resume-project-import)
+* [SSH](cmd-resume-project.html)
+* UI from the list imports screen (menu 'Projects' > 'List Imports') and
+* UI from the project info screen ('Resume Import...' action)
+
+Completing the import can be done via
+
+* [REST](rest-api-config.html#complete-project-import)
+* [SSH](cmd-complete-project.html)
+* UI from the list imports screen (menu 'Projects' > 'List Imports') and
+* UI from the project info screen ('Complete Import' action)
+
+When doing a project import the project in the target Gerrit server can
+be created with a new name or under another parent project.
+
+<a id="project-import-steps">
+#### How the project import works
+
+The project import is implemented in such a way that it replays the
+actions that have been done in the source Gerrit server with preserving
+the original timestamps.
+
+A project import consists of the following steps:
+
+* creation of the Git repository and project in the target Gerrit server
+* fetch all refs from the source Gerrit server
+* [optional] reparent project in the target Gerrit server
+* replay all changes (changes in the target Gerrit server get new
+  numeric ID's)
+* import of groups for access rights on this project if they are
+  missing in the target Gerrit server
+
+Replaying a change is done by:
+
+* replay all revisions (create the change refs and insert the patch sets)
+* replay inline comments
+* replay change messages
+* add approvals (approvals for unknown labels are ignored)
+* add hashtags (hashtags are only applied if the importing user has
+  permissions to edit hashtags, e.g. if the
+  [Edit Hashtags](../../../Documentation/access-control.html#category_edit_hashtags)
+  global capability is assigned)
+* add link to original change as a new change message
+
+<a id="import-file">
+#### Import File
+
+At a point in time a project can be imported only by a single process.
+To protect a running import from other processes the import creates an
+import file 'review\_site/data/@PLUGIN@/\<target-project-name\>' and
+locks this file. In the import file an
+[ImportProjectInfo](rest-api-config.html#import-project-info) entity is
+persisted that stores the input parameters and records the past
+imports. The import file is kept after the import is done so that the
+input parameters do not need to be specified again when the import is
+resumed.
+
+<a id="resume-project-import">
+#### Resume Project Import
+
+Once a project was imported, the project import can be resumed to
+import modifications that happened in the source Gerrit server after
+the initial/last import to the target Gerrit server has been done.
+
+The resume of an import is only guaranteed to work if none of the
+imported entities has been modified in the target Gerrit server,
+otherwise resuming the import may fail or even override modifications
+done in the target Gerrit server (e.g. change approvals will be
+overridden). But depending on what was modified, it may also just work.
+
+On resume changes that have the same last modified timestamp in the
+source and target Gerrit server are skipped, unless the force option is
+set.
+
+The force option is useful if an import finished with warnings (in the
+error log) and the import should be resumed after fixing the issues,
+e.g.:
+
+* approvals for unknown labels have been skipped and the labels have
+  now been configured on the target Gerrit server
+* hashtags couldn't be set due to missing permissions, but the
+  permissions have been granted now
+
+On resume approvals, hashtags and change topic are always reapplied.
+This means that any modification of these properties in the target
+Gerrit server is overridden if the import of a change is resumed.
+
+<a id="complete-project-import">
+#### Complete Project Import
+
+Completing the project import deletes the [import file](#import-file)
+for that project. Afterwards it's not possible to resume the project
+import anymore. Also the project doesn't appear in the list of imported
+projects anymore.
+
+<a id="project-copy">
+### Project Copy
+
+Project copy is a special case of project import, where a project from
+the same Gerrit server is imported under a new name.
+
+To be able to copy a project the user must be a member of a group that
+is granted the 'Copy Project' capability (provided by this plugin) or
+the 'Administrate Server' capability.
+
+Please note, due the implementation doing a project import internally
+the user doing the copy must have an HTTP password generated.
+
+<a id="project-copy-commands">
+#### Commands
+
+Copying a project can be done via
+
+* [REST](rest-api-projects.html#copy-project)
+* [SSH](cmd-copy-project.html) and
+* UI from the project info screen ('Copy...' action)
+
+Resuming a project copy can be done via
+
+* [REST](rest-api-projects.html#resume-copy-import)
+* [SSH](cmd-resume-project.html)
+* UI from the list imports screen (menu 'Projects' > 'List Imports') and
+* UI from the project info screen ('Resume Copy...' action)
+
+Completing the copy can be done via
+
+* [REST](rest-api-config.html#complete-project-import)
+* [SSH](cmd-complete-project.html)
+* UI from the list imports screen (menu 'Projects' > 'List Imports') and
+* UI from the project info screen ('Complete Copy' action)
+
+When doing a project copy the project *cannot* be put under another
+parent project. But you can reparent the project copy after the copy is
+done.
+
+<a id="project-rename">
+### Project Rename
+
+By doing a [project copy](#project-copy) and then using the
+[delete-project](https://gerrit.googlesource.com/plugins/delete-project/+doc/master/src/main/resources/Documentation/about.md)
+plugin to delete the source project, a project can be renamed.
+
+<a id="group-import">
+### Group Import
+
+The import group functionality allows to import a Gerrit group from one
+Gerrit server to another Gerrit server.
+
+<a id="group-import-preconditions">
+#### Preconditions
+
+Preconditions for a group import:
+
+* If the group is not self-owned, the owner group must already exist in
+  the target Gerrit server.
+* Included groups must already exist in the target Gerrit server.
+* Member accounts must exist in the target Gerrit server unless auth
+  type is 'LDAP', 'HTTP\_LDAP' or 'CLIENT\_SSL\_CERT\_LDAP'.
+
+For auth type 'LDAP', 'HTTP\_LDAP' or 'CLIENT\_SSL\_CERT\_LDAP' missing
+member accounts are automatically created. The public SSH keys of a
+member are automatically retrieved from the source Gerrit server and
+added to the new account in the target Gerrit server.
+
+Gerrit internal users (e.g. service users) are never automatically
+created but must be created in the target Gerrit server before the
+import.
+
+<a id="group-import-commands">
+#### Commands
+
+Importing a group can be done via
+
+* [REST](rest-api-config.html#import-group) and
+* [SSH](cmd-group.html)