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)