Extract replay changes logic into own import step
As a side effect this also extracts the LDAP utility methods into the
AccountUtil class.
Change-Id: I462932bb205aaa9696e31ac1d8927593ed6b1dbd
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/AccountUtil.java b/src/main/java/com/googlesource/gerrit/plugins/importer/AccountUtil.java
new file mode 100644
index 0000000..c5c5f42
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/AccountUtil.java
@@ -0,0 +1,83 @@
+//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;
+
+import com.google.gerrit.common.errors.NoSuchAccountException;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.Objects;
+
+@Singleton
+class AccountUtil {
+
+ private final AccountCache accountCache;
+ private final AccountManager accountManager;
+ private final AuthType authType;
+
+ @Inject
+ public AccountUtil(AccountCache accountCache,
+ AccountManager accountManager,
+ AuthConfig authConfig) {
+ this.accountCache = accountCache;
+ this.accountManager = accountManager;
+ this.authType = authConfig.getAuthType();
+ }
+
+ Account.Id resolveUser(AccountInfo acc) throws NoSuchAccountException {
+ AccountState a = accountCache.getByUsername(acc.username);
+ if (a == null) {
+ switch (authType) {
+ case HTTP_LDAP:
+ case CLIENT_SSL_CERT_LDAP:
+ case LDAP:
+ return createAccountByLdap(acc.username);
+ default:
+ throw new NoSuchAccountException(String.format("User %s not found",
+ acc.username));
+ }
+ }
+ if (!Objects.equals(a.getAccount().getPreferredEmail(), acc.email)) {
+ throw new NoSuchAccountException(String.format(
+ "User %s not found: Email mismatch, expected %s but found %s",
+ acc.username, acc.email, a.getAccount().getPreferredEmail()));
+ }
+ return a.getAccount().getId();
+ }
+
+ private Account.Id createAccountByLdap(String user)
+ throws NoSuchAccountException {
+ if (!user.matches(Account.USER_NAME_PATTERN)) {
+ throw new NoSuchAccountException(String.format("User %s not found", user));
+ }
+
+ try {
+ AuthRequest req = AuthRequest.forUser(user);
+ req.setSkipAuthentication(true);
+ return accountManager.authenticate(req).getAccountId();
+ } catch (AccountException e) {
+ throw new NoSuchAccountException(String.format("User %s not found", user));
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProjectTask.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProjectTask.java
index 7dba0f3..6965bac 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProjectTask.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportProjectTask.java
@@ -16,85 +16,25 @@
import static java.lang.String.format;
-import com.google.common.collect.Ordering;
-import com.google.common.collect.Table;
-import com.google.common.collect.TreeBasedTable;
-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.PluginData;
-import com.google.gerrit.extensions.api.changes.HashtagsInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.common.ApprovalInfo;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.extensions.common.CommentInfo;
-import com.google.gerrit.extensions.common.LabelInfo;
-import com.google.gerrit.extensions.common.RevisionInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AuthType;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountException;
-import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.AuthRequest;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.HashtagsUtil;
-import com.google.gerrit.server.change.PostReview;
-import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.config.AuthConfig;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.validators.ValidationException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.internal.storage.file.LockFile;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Objects;
class ImportProjectTask implements Runnable {
@@ -112,48 +52,21 @@
private final OpenRepositoryStep openRepoStep;
private final ConfigureRepositoryStep configRepoStep;
private final GitFetchStep gitFetchStep;
+ private final ReplayChangesStep.Factory replayChangesFactory;
private final File lockRoot;
- private final ReviewDb db;
- private final AccountCache accountCache;
- private final AccountManager accountManager;
- private final AuthType authType;
- private final CurrentUser currentUser;
- private final IdentifiedUser.GenericFactory genericUserFactory;
- private final ChangeControl.GenericFactory changeControlFactory;
- private final PatchSetInfoFactory patchSetInfoFactory;
- private final Provider<PostReview> postReview;
- private final ChangeUpdate.Factory updateFactory;
- private final ChangeMessagesUtil cmUtil;
- private final HashtagsUtil hashtagsUtil;
- private final ChangeIndexer indexer;
private final String fromGerrit;
private final Project.NameKey name;
private final String user;
private final String password;
- private final RemoteApi api;
private final StringBuffer messages;
- private Repository repo;
-
@Inject
ImportProjectTask(OpenRepositoryStep openRepoStep,
ConfigureRepositoryStep configRepoStep,
GitFetchStep gitFetchStep,
+ ReplayChangesStep.Factory replayChangesFactory,
@PluginData File data,
- ReviewDb db,
- AccountCache accountCache,
- AccountManager accountManager,
- AuthConfig authConfig,
- CurrentUser currentUser,
- IdentifiedUser.GenericFactory genericUserFactory,
- ChangeControl.GenericFactory changeControlFactory,
- PatchSetInfoFactory patchSetInfoFactory,
- Provider<PostReview> postReview,
- ChangeUpdate.Factory updateFactory,
- ChangeMessagesUtil cmUtil,
- HashtagsUtil hashtagsUtil,
- ChangeIndexer indexer,
@Assisted("from") String fromGerrit,
@Assisted Project.NameKey name,
@Assisted("user") String user,
@@ -162,27 +75,14 @@
this.openRepoStep = openRepoStep;
this.configRepoStep = configRepoStep;
this.gitFetchStep = gitFetchStep;
+ this.replayChangesFactory = replayChangesFactory;
this.lockRoot = data;
- this.db = db;
- this.accountCache = accountCache;
- this.accountManager = accountManager;
- this.authType = authConfig.getAuthType();
- this.currentUser = currentUser;
- this.genericUserFactory = genericUserFactory;
- this.changeControlFactory = changeControlFactory;
- this.patchSetInfoFactory = patchSetInfoFactory;
- this.postReview = postReview;
- this.updateFactory = updateFactory;
- this.cmUtil = cmUtil;
- this.hashtagsUtil = hashtagsUtil;
- this.indexer = indexer;
this.fromGerrit = fromGerrit;
this.name = name;
this.user = user;
this.password = password;
this.messages = messages;
- this.api = new RemoteApi(fromGerrit, user, password);
}
@Override
@@ -193,7 +93,7 @@
}
try {
- repo = openRepoStep.open(name, messages);
+ Repository repo = openRepoStep.open(name, messages);
if (repo == null) {
return;
}
@@ -201,7 +101,8 @@
try {
configRepoStep.configure(repo, name, fromGerrit);
gitFetchStep.fetch(user, password, repo, name, messages);
- replayChanges();
+ replayChangesFactory.create(fromGerrit, user, password, repo, name)
+ .replay();
} catch (IOException | GitAPIException | OrmException
| NoSuchAccountException | NoSuchChangeException | RestApiException
| ValidationException e) {
@@ -237,343 +138,4 @@
return null;
}
}
-
- private void replayChanges() throws IOException, OrmException,
- NoSuchAccountException, NoSuchChangeException, RestApiException,
- ValidationException {
- List<ChangeInfo> changes = api.queryChanges(name.get());
- RevWalk rw = new RevWalk(repo);
- try {
- for (ChangeInfo c : changes) {
- replayChange(rw, c);
- }
- } finally {
- rw.release();
- }
- }
-
- private void replayChange(RevWalk rw, ChangeInfo c)
- throws IOException, OrmException, NoSuchAccountException,
- NoSuchChangeException, RestApiException, ValidationException {
- Change change = createChange(c);
- replayRevisions(rw, change, c);
- db.changes().insert(Collections.singleton(change));
-
- replayInlineComments(change, c);
- replayMessages(change, c);
- addApprovals(change, c);
- addHashtags(change, c);
-
- insertLinkToOriginalChange(change, c);
-
- indexer.index(db, change);
- }
-
- private Change createChange(ChangeInfo c) throws OrmException,
- NoSuchAccountException {
- Change.Id changeId = new Change.Id(db.nextChangeId());
-
- Change change =
- new Change(new Change.Key(c.changeId), changeId, resolveUser(c.owner),
- new Branch.NameKey(new Project.NameKey(c.project),
- fullName(c.branch)), c.created);
- change.setStatus(Change.Status.forChangeStatus(c.status));
- change.setTopic(c.topic);
- return change;
- }
-
-
- private static String fullName(String branch) {
- if (branch.startsWith(Constants.R_HEADS)) {
- return branch;
- } else {
- return Constants.R_HEADS + branch;
- }
- }
-
- /**
- * @return the current patch set for the given change
- */
- private void replayRevisions(RevWalk rw, Change change,
- ChangeInfo c) throws IOException, OrmException, NoSuchAccountException {
- List<RevisionInfo> revisions = new ArrayList<>(c.revisions.values());
- sortRevisionInfoByNumber(revisions);
- List<PatchSet> patchSets = new ArrayList<>();
-
- db.changes().beginTransaction(change.getId());
- try {
- for (RevisionInfo r : revisions) {
- String origRef = r.ref;
- ObjectId id = repo.resolve(origRef);
- if (id == null) {
- // already replayed?
- continue;
- }
- RevCommit commit = rw.parseCommit(id);
-
- PatchSet ps = new PatchSet(new PatchSet.Id(change.getId(), r._number));
- patchSets.add(ps);
-
- ps.setUploader(resolveUser(r.uploader));
- ps.setCreatedOn(r.created);
- ps.setRevision(new RevId(commit.name()));
- ps.setDraft(r.draft != null && r.draft);
-
- PatchSetInfo info = patchSetInfoFactory.get(commit, ps.getId());
- if (c.currentRevision.equals(info.getRevId())) {
- change.setCurrentPatchSet(info);
- }
-
- ChangeUtil.insertAncestors(db, ps.getId(), commit);
-
- renameRef(repo, origRef, ps);
- }
-
- db.patchSets().insert(patchSets);
- db.commit();
- } finally {
- db.rollback();
- }
- }
-
- private static void sortRevisionInfoByNumber(List<RevisionInfo> list) {
- Collections.sort(list, new Comparator<RevisionInfo>() {
- @Override
- public int compare(RevisionInfo a, RevisionInfo b) {
- return a._number - b._number;
- }
- });
- }
-
- private void replayInlineComments(Change change, ChangeInfo c) throws OrmException,
- RestApiException, IOException, NoSuchChangeException,
- NoSuchAccountException {
- for (PatchSet ps : db.patchSets().byChange(change.getId())) {
- Iterable<CommentInfo> comments = api.getComments(
- c._number, ps.getRevision().get());
-
- Table<Timestamp, Account.Id, List<CommentInfo>> t = TreeBasedTable.create(
- Ordering.natural(), new Comparator<Account.Id>() {
- @Override
- public int compare(Account.Id a1, Account.Id a2) {
- return a1.get() - a2.get();
- }}
- );
-
- for (CommentInfo comment : comments) {
- Account.Id id = resolveUser(comment.author);
- List<CommentInfo> ci = t.get(comment.updated, id);
- if (ci == null) {
- ci = new ArrayList<>();
- t.put(comment.updated, id, ci);
- }
- ci.add(comment);
- }
-
- for (Timestamp ts : t.rowKeySet()) {
- for (Map.Entry<Account.Id, List<CommentInfo>> e : t.row(ts).entrySet()) {
- postComments(change, ps, e.getValue(), e.getKey(), ts);
- }
- }
- }
- }
-
- private void postComments(Change change, PatchSet ps,
- List<CommentInfo> comments, Account.Id author, Timestamp ts)
- throws RestApiException, OrmException, IOException, NoSuchChangeException {
- ReviewInput input = new ReviewInput();
- input.notify = NotifyHandling.NONE;
- input.comments = new HashMap<>();
-
- for (CommentInfo comment : comments) {
- if (!input.comments.containsKey(comment.path)) {
- input.comments.put(comment.path,
- new ArrayList<ReviewInput.CommentInput>());
- }
-
- ReviewInput.CommentInput commentInput = new ReviewInput.CommentInput();
- commentInput.id = comment.id;
- commentInput.inReplyTo = comment.inReplyTo;
- commentInput.line = comment.line;
- commentInput.message = comment.message;
- commentInput.path = comment.path;
- commentInput.range = comment.range;
- commentInput.side = comment.side;
- commentInput.updated = comment.updated;
-
- input.comments.get(comment.path).add(commentInput);
- }
-
- postReview.get().apply(
- new RevisionResource(new ChangeResource(control(change, author)), ps),
- input, ts);
- }
-
- private void renameRef(Repository repo, String origRef, PatchSet ps)
- throws IOException {
- String ref = ps.getId().toRefName();
- if (ref.equals(origRef)) {
- return;
- }
-
- createRef(repo, ps);
- deleteRef(repo, ps, origRef);
- }
-
- private void createRef(Repository repo, PatchSet ps) throws IOException {
- String ref = ps.getId().toRefName();
- RefUpdate ru = repo.updateRef(ref);
- ru.setForceUpdate(true);
- ru.setExpectedOldObjectId(ObjectId.zeroId());
- ru.setNewObjectId(ObjectId.fromString(ps.getRevision().get()));
- RefUpdate.Result result = ru.update();
- switch (result) {
- case NEW:
- case FORCED:
- case FAST_FORWARD:
- return;
- default:
- throw new IOException(String.format("Failed to create ref %s", ref));
- }
- }
-
- private void deleteRef(Repository repo, PatchSet ps, String ref)
- throws IOException {
- RefUpdate ru = repo.updateRef(ref);
- ru.setForceUpdate(true);
- ru.setExpectedOldObjectId(ObjectId.fromString(ps.getRevision().get()));
- ru.setNewObjectId(ObjectId.zeroId());
- RefUpdate.Result result = ru.update();
- switch (result) {
- case FORCED:
- return;
- default:
- throw new IOException(String.format("Failed to delete ref %s", ref));
- }
- }
-
- private Account.Id resolveUser(AccountInfo acc) throws NoSuchAccountException {
- AccountState a = accountCache.getByUsername(acc.username);
- if (a == null) {
- switch (authType) {
- case HTTP_LDAP:
- case CLIENT_SSL_CERT_LDAP:
- case LDAP:
- return createAccountByLdap(acc.username);
- default:
- throw new NoSuchAccountException(String.format("User %s not found",
- acc.username));
- }
- }
- if (!Objects.equals(a.getAccount().getPreferredEmail(), acc.email)) {
- throw new NoSuchAccountException(String.format(
- "User %s not found: Email mismatch, expected %s but found %s",
- acc.username, acc.email, a.getAccount().getPreferredEmail()));
- }
- return a.getAccount().getId();
- }
-
- private Account.Id createAccountByLdap(String user)
- throws NoSuchAccountException {
- if (!user.matches(Account.USER_NAME_PATTERN)) {
- throw new NoSuchAccountException(String.format("User %s not found", user));
- }
-
- try {
- AuthRequest req = AuthRequest.forUser(user);
- req.setSkipAuthentication(true);
- return accountManager.authenticate(req).getAccountId();
- } catch (AccountException e) {
- throw new NoSuchAccountException(String.format("User %s not found", user));
- }
- }
-
- private void replayMessages(Change change, ChangeInfo c)
- throws IOException, NoSuchChangeException, OrmException,
- NoSuchAccountException {
- for (ChangeMessageInfo msg : c.messages) {
- Account.Id userId = resolveUser(msg.author);
- Timestamp ts = msg.date;
- ChangeUpdate update = updateFactory.create(control(change, userId), ts);
- ChangeMessage cmsg =
- new ChangeMessage(new ChangeMessage.Key(change.getId(), msg.id),
- userId, ts, new PatchSet.Id(change.getId(), msg._revisionNumber));
- cmsg.setMessage(msg.message);
- cmUtil.addChangeMessage(db, update, cmsg);
- update.commit();
- }
- }
-
- private void addApprovals(Change change, ChangeInfo c)
- throws OrmException, NoSuchChangeException, IOException,
- NoSuchAccountException {
- List<PatchSetApproval> approvals = new ArrayList<>();
- for (Entry<String, LabelInfo> e : c.labels.entrySet()) {
- String labelName = e.getKey();
- LabelInfo label = e.getValue();
- if (label.all != null) {
- for (ApprovalInfo a : label.all) {
- Account.Id user = resolveUser(a);
- ChangeControl ctrl = control(change, a);
- LabelType labelType = ctrl.getLabelTypes().byLabel(labelName);
- approvals.add(new PatchSetApproval(
- new PatchSetApproval.Key(change.currentPatchSetId(), user,
- labelType.getLabelId()), a.value.shortValue(), a.date));
- ChangeUpdate update = updateFactory.create(ctrl);
- update.putApproval(labelName, a.value.shortValue());
- update.commit();
- }
- }
- }
- db.patchSetApprovals().upsert(approvals);
- }
-
- private void addHashtags(Change change, ChangeInfo c) throws AuthException,
- IOException, ValidationException, OrmException, NoSuchChangeException {
- HashtagsInput input = new HashtagsInput();
- input.add = new HashSet<>(c.hashtags);
- hashtagsUtil.setHashtags(control(change, c.owner), input, false, false);
- }
-
- private void insertLinkToOriginalChange(Change change,
- ChangeInfo c) throws NoSuchChangeException, OrmException, IOException {
- insertMessage(change, "Imported from " + changeUrl(c));
- }
-
- private String changeUrl(ChangeInfo c) {
- StringBuilder url = new StringBuilder();
- url.append(ensureSlash(fromGerrit)).append(c._number);
- return url.toString();
- }
-
- private void insertMessage(Change change, String message)
- throws NoSuchChangeException, OrmException, IOException {
- Account.Id userId = ((IdentifiedUser) currentUser).getAccountId();
- ChangeUpdate update = updateFactory.create(control(change, userId));
- ChangeMessage cmsg =
- new ChangeMessage(new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(db)), userId, TimeUtil.nowTs(),
- change.currentPatchSetId());
- cmsg.setMessage(message);
- cmUtil.addChangeMessage(db, update, cmsg);
- update.commit();
- }
-
- 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));
- }
-
- 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/Module.java b/src/main/java/com/googlesource/gerrit/plugins/importer/Module.java
index 04f7bf8..4ffeabe 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/Module.java
@@ -43,5 +43,7 @@
bind(OpenRepositoryStep.class);
bind(ConfigureRepositoryStep.class);
bind(GitFetchStep.class);
+ bind(AccountUtil.class);
+ install(new FactoryModuleBuilder().build(ReplayChangesStep.Factory.class));
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayChangesStep.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayChangesStep.java
new file mode 100644
index 0000000..bd7c099
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ReplayChangesStep.java
@@ -0,0 +1,442 @@
+//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;
+
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Table;
+import com.google.common.collect.TreeBasedTable;
+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.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.HashtagsUtil;
+import com.google.gerrit.server.change.PostReview;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+class ReplayChangesStep {
+
+ interface Factory {
+ ReplayChangesStep create(
+ @Assisted("from") String fromGerrit,
+ @Assisted("user") String user,
+ @Assisted("password") String password,
+ Repository repo,
+ Project.NameKey name);
+ }
+
+ private final AccountUtil accountUtil;
+ private final ReviewDb db;
+ private final ChangeIndexer indexer;
+ private final PatchSetInfoFactory patchSetInfoFactory;
+ private final Provider<PostReview> postReview;
+ private final ChangeUpdate.Factory updateFactory;
+ private final ChangeMessagesUtil cmUtil;
+ private final HashtagsUtil hashtagsUtil;
+ private final CurrentUser currentUser;
+ private final IdentifiedUser.GenericFactory genericUserFactory;
+ private final ChangeControl.GenericFactory changeControlFactory;
+ private final String fromGerrit;
+ private final RemoteApi api;
+ private final Repository repo;
+ private final Project.NameKey name;
+
+ @Inject
+ ReplayChangesStep(
+ AccountUtil accountUtil,
+ ReviewDb db,
+ ChangeIndexer indexer,
+ PatchSetInfoFactory patchSetInfoFactory,
+ Provider<PostReview> postReview,
+ ChangeUpdate.Factory updateFactory,
+ ChangeMessagesUtil cmUtil,
+ HashtagsUtil hashtagsUtil,
+ CurrentUser currentUser,
+ IdentifiedUser.GenericFactory genericUserFactory,
+ ChangeControl.GenericFactory changeControlFactory,
+ @Assisted("from") String fromGerrit,
+ @Assisted("user") String user,
+ @Assisted("password") String password,
+ @Assisted Repository repo,
+ @Assisted Project.NameKey name) {
+ this.accountUtil = accountUtil;
+ this.db = db;
+ this.indexer = indexer;
+ this.patchSetInfoFactory = patchSetInfoFactory;
+ this.postReview = postReview;
+ this.updateFactory = updateFactory;
+ this.cmUtil = cmUtil;
+ this.hashtagsUtil = hashtagsUtil;
+ this.currentUser = currentUser;
+ this.genericUserFactory = genericUserFactory;
+ this.changeControlFactory = changeControlFactory;
+ this.fromGerrit = fromGerrit;
+ this.api = new RemoteApi(fromGerrit, user, password);
+ this.repo = repo;
+ this.name = name;
+ }
+
+ void replay() throws IOException, OrmException,
+ NoSuchAccountException, NoSuchChangeException, RestApiException,
+ ValidationException {
+ List<ChangeInfo> changes = api.queryChanges(name.get());
+ RevWalk rw = new RevWalk(repo);
+ try {
+ for (ChangeInfo c : changes) {
+ replayChange(rw, c);
+ }
+ } finally {
+ rw.release();
+ }
+ }
+
+ private void replayChange(RevWalk rw, ChangeInfo c)
+ throws IOException, OrmException, NoSuchAccountException,
+ NoSuchChangeException, RestApiException, ValidationException {
+ Change change = createChange(c);
+ replayRevisions(rw, change, c);
+ db.changes().insert(Collections.singleton(change));
+
+ replayInlineComments(change, c);
+ replayMessages(change, c);
+ addApprovals(change, c);
+ addHashtags(change, c);
+
+ insertLinkToOriginalChange(change, c);
+
+ indexer.index(db, change);
+ }
+
+ private Change createChange(ChangeInfo c) throws OrmException,
+ NoSuchAccountException {
+ Change.Id changeId = new Change.Id(db.nextChangeId());
+
+ Change change =
+ new Change(new Change.Key(c.changeId), changeId, accountUtil.resolveUser(c.owner),
+ new Branch.NameKey(new Project.NameKey(c.project),
+ fullName(c.branch)), c.created);
+ change.setStatus(Change.Status.forChangeStatus(c.status));
+ change.setTopic(c.topic);
+ return change;
+ }
+
+ private static String fullName(String branch) {
+ if (branch.startsWith(Constants.R_HEADS)) {
+ return branch;
+ } else {
+ return Constants.R_HEADS + branch;
+ }
+ }
+
+ private void replayRevisions(RevWalk rw, Change change,
+ ChangeInfo c) throws IOException, OrmException, NoSuchAccountException {
+ List<RevisionInfo> revisions = new ArrayList<>(c.revisions.values());
+ sortRevisionInfoByNumber(revisions);
+ List<PatchSet> patchSets = new ArrayList<>();
+
+ db.changes().beginTransaction(change.getId());
+ try {
+ for (RevisionInfo r : revisions) {
+ String origRef = r.ref;
+ ObjectId id = repo.resolve(origRef);
+ if (id == null) {
+ // already replayed?
+ continue;
+ }
+ RevCommit commit = rw.parseCommit(id);
+
+ PatchSet ps = new PatchSet(new PatchSet.Id(change.getId(), r._number));
+ patchSets.add(ps);
+
+ ps.setUploader(accountUtil.resolveUser(r.uploader));
+ ps.setCreatedOn(r.created);
+ ps.setRevision(new RevId(commit.name()));
+ ps.setDraft(r.draft != null && r.draft);
+
+ PatchSetInfo info = patchSetInfoFactory.get(commit, ps.getId());
+ if (c.currentRevision.equals(info.getRevId())) {
+ change.setCurrentPatchSet(info);
+ }
+
+ ChangeUtil.insertAncestors(db, ps.getId(), commit);
+
+ renameRef(repo, origRef, ps);
+ }
+
+ db.patchSets().insert(patchSets);
+ db.commit();
+ } finally {
+ db.rollback();
+ }
+ }
+
+ private static void sortRevisionInfoByNumber(List<RevisionInfo> list) {
+ Collections.sort(list, new Comparator<RevisionInfo>() {
+ @Override
+ public int compare(RevisionInfo a, RevisionInfo b) {
+ return a._number - b._number;
+ }
+ });
+ }
+
+ private void replayInlineComments(Change change, ChangeInfo c) throws OrmException,
+ RestApiException, IOException, NoSuchChangeException,
+ NoSuchAccountException {
+ for (PatchSet ps : db.patchSets().byChange(change.getId())) {
+ Iterable<CommentInfo> comments = api.getComments(
+ c._number, ps.getRevision().get());
+
+ Table<Timestamp, Account.Id, List<CommentInfo>> t = TreeBasedTable.create(
+ Ordering.natural(), new Comparator<Account.Id>() {
+ @Override
+ public int compare(Account.Id a1, Account.Id a2) {
+ return a1.get() - a2.get();
+ }}
+ );
+
+ for (CommentInfo comment : comments) {
+ Account.Id id = accountUtil.resolveUser(comment.author);
+ List<CommentInfo> ci = t.get(comment.updated, id);
+ if (ci == null) {
+ ci = new ArrayList<>();
+ t.put(comment.updated, id, ci);
+ }
+ ci.add(comment);
+ }
+
+ for (Timestamp ts : t.rowKeySet()) {
+ for (Map.Entry<Account.Id, List<CommentInfo>> e : t.row(ts).entrySet()) {
+ postComments(change, ps, e.getValue(), e.getKey(), ts);
+ }
+ }
+ }
+ }
+
+ private void postComments(Change change, PatchSet ps,
+ List<CommentInfo> comments, Account.Id author, Timestamp ts)
+ throws RestApiException, OrmException, IOException, NoSuchChangeException {
+ ReviewInput input = new ReviewInput();
+ input.notify = NotifyHandling.NONE;
+ input.comments = new HashMap<>();
+
+ for (CommentInfo comment : comments) {
+ if (!input.comments.containsKey(comment.path)) {
+ input.comments.put(comment.path,
+ new ArrayList<ReviewInput.CommentInput>());
+ }
+
+ ReviewInput.CommentInput commentInput = new ReviewInput.CommentInput();
+ commentInput.id = comment.id;
+ commentInput.inReplyTo = comment.inReplyTo;
+ commentInput.line = comment.line;
+ commentInput.message = comment.message;
+ commentInput.path = comment.path;
+ commentInput.range = comment.range;
+ commentInput.side = comment.side;
+ commentInput.updated = comment.updated;
+
+ input.comments.get(comment.path).add(commentInput);
+ }
+
+ postReview.get().apply(
+ new RevisionResource(new ChangeResource(control(change, author)), ps),
+ input, ts);
+ }
+
+ private void renameRef(Repository repo, String origRef, PatchSet ps)
+ throws IOException {
+ String ref = ps.getId().toRefName();
+ if (ref.equals(origRef)) {
+ return;
+ }
+
+ createRef(repo, ps);
+ deleteRef(repo, ps, origRef);
+ }
+
+ private void createRef(Repository repo, PatchSet ps) throws IOException {
+ String ref = ps.getId().toRefName();
+ RefUpdate ru = repo.updateRef(ref);
+ ru.setForceUpdate(true);
+ ru.setExpectedOldObjectId(ObjectId.zeroId());
+ ru.setNewObjectId(ObjectId.fromString(ps.getRevision().get()));
+ RefUpdate.Result result = ru.update();
+ switch (result) {
+ case NEW:
+ case FORCED:
+ case FAST_FORWARD:
+ return;
+ default:
+ throw new IOException(String.format("Failed to create ref %s", ref));
+ }
+ }
+
+ private void deleteRef(Repository repo, PatchSet ps, String ref)
+ throws IOException {
+ RefUpdate ru = repo.updateRef(ref);
+ ru.setForceUpdate(true);
+ ru.setExpectedOldObjectId(ObjectId.fromString(ps.getRevision().get()));
+ ru.setNewObjectId(ObjectId.zeroId());
+ RefUpdate.Result result = ru.update();
+ switch (result) {
+ case FORCED:
+ return;
+ default:
+ throw new IOException(String.format("Failed to delete ref %s", ref));
+ }
+ }
+
+ private void replayMessages(Change change, ChangeInfo c)
+ throws IOException, NoSuchChangeException, OrmException,
+ NoSuchAccountException {
+ for (ChangeMessageInfo msg : c.messages) {
+ Account.Id userId = accountUtil.resolveUser(msg.author);
+ Timestamp ts = msg.date;
+ ChangeUpdate update = updateFactory.create(control(change, userId), ts);
+ ChangeMessage cmsg =
+ new ChangeMessage(new ChangeMessage.Key(change.getId(), msg.id),
+ userId, ts, new PatchSet.Id(change.getId(), msg._revisionNumber));
+ cmsg.setMessage(msg.message);
+ cmUtil.addChangeMessage(db, update, cmsg);
+ update.commit();
+ }
+ }
+
+ private void addApprovals(Change change, ChangeInfo c)
+ throws OrmException, NoSuchChangeException, IOException,
+ NoSuchAccountException {
+ List<PatchSetApproval> approvals = new ArrayList<>();
+ for (Entry<String, LabelInfo> e : c.labels.entrySet()) {
+ String labelName = e.getKey();
+ LabelInfo label = e.getValue();
+ if (label.all != null) {
+ for (ApprovalInfo a : label.all) {
+ Account.Id user = accountUtil.resolveUser(a);
+ ChangeControl ctrl = control(change, a);
+ LabelType labelType = ctrl.getLabelTypes().byLabel(labelName);
+ approvals.add(new PatchSetApproval(
+ new PatchSetApproval.Key(change.currentPatchSetId(), user,
+ labelType.getLabelId()), a.value.shortValue(), a.date));
+ ChangeUpdate update = updateFactory.create(ctrl);
+ update.putApproval(labelName, a.value.shortValue());
+ update.commit();
+ }
+ }
+ }
+ db.patchSetApprovals().upsert(approvals);
+ }
+
+ private void addHashtags(Change change, ChangeInfo c) throws AuthException,
+ IOException, ValidationException, OrmException, NoSuchChangeException {
+ HashtagsInput input = new HashtagsInput();
+ input.add = new HashSet<>(c.hashtags);
+ hashtagsUtil.setHashtags(control(change, c.owner), input, false, false);
+ }
+
+ private void insertLinkToOriginalChange(Change change,
+ ChangeInfo c) throws NoSuchChangeException, OrmException, IOException {
+ insertMessage(change, "Imported from " + changeUrl(c));
+ }
+
+ private String changeUrl(ChangeInfo c) {
+ StringBuilder url = new StringBuilder();
+ url.append(ensureSlash(fromGerrit)).append(c._number);
+ return url.toString();
+ }
+
+ private void insertMessage(Change change, String message)
+ throws NoSuchChangeException, OrmException, IOException {
+ Account.Id userId = ((IdentifiedUser) currentUser).getAccountId();
+ ChangeUpdate update = updateFactory.create(control(change, userId));
+ ChangeMessage cmsg =
+ new ChangeMessage(new ChangeMessage.Key(change.getId(),
+ ChangeUtil.messageUUID(db)), userId, TimeUtil.nowTs(),
+ change.currentPatchSetId());
+ cmsg.setMessage(message);
+ cmUtil.addChangeMessage(db, update, cmsg);
+ update.commit();
+ }
+
+ 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));
+ }
+
+ private static String ensureSlash(String in) {
+ if (in != null && !in.endsWith("/")) {
+ return in + "/";
+ }
+ return in;
+ }
+}