summaryrefslogtreecommitdiffstats
path: root/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
diff options
context:
space:
mode:
Diffstat (limited to 'gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java')
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java479
1 files changed, 329 insertions, 150 deletions
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 6886d2c85d..9dc572dfbd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -14,34 +14,38 @@
package com.google.gerrit.server;
-import static com.google.gerrit.reviewdb.ApprovalCategory.SUBMIT;
-
import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ChangeMessage;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.PatchSetInfo;
-import com.google.gerrit.reviewdb.RevId;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.TrackingId;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.client.TrackingId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.TrackingFooter;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeOp;
-import com.google.gerrit.server.git.MergeQueue;
import com.google.gerrit.server.git.ReplicationQueue;
+import com.google.gerrit.server.mail.EmailException;
+import com.google.gerrit.server.mail.RebasedPatchSetSender;
+import com.google.gerrit.server.mail.ReplacePatchSetSender;
+import com.google.gerrit.server.mail.ReplyToChangeSender;
+import com.google.gerrit.server.mail.RevertedSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.mail.AbandonedSender;
-import com.google.gerrit.server.mail.EmailException;
-import com.google.gerrit.server.mail.RevertedSender;
-import com.google.gwtorm.client.AtomicUpdate;
-import com.google.gwtorm.client.OrmConcurrencyException;
-import com.google.gwtorm.client.OrmException;
-
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmConcurrencyException;
+import com.google.gwtorm.server.OrmException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
@@ -50,8 +54,11 @@ import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.ThreeWayMerger;
import org.eclipse.jgit.revwalk.FooterLine;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -60,6 +67,7 @@ import org.eclipse.jgit.util.NB;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -162,113 +170,260 @@ public class ChangeUtil {
db.trackingIds().delete(toDelete);
}
- public static void submit(final PatchSet.Id patchSetId,
- final IdentifiedUser user, final ReviewDb db,
- final MergeOp.Factory opFactory, final MergeQueue merger)
+ public static void testMerge(MergeOp.Factory opFactory, Change change) {
+ opFactory.create(change.getDest()).verifyMergeability(change);
+ }
+
+ public static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src)
throws OrmException {
- final Change.Id changeId = patchSetId.getParentKey();
- final PatchSetApproval approval = createSubmitApproval(patchSetId, user, db);
+ final int cnt = src.getParentCount();
+ List<PatchSetAncestor> toInsert = new ArrayList<PatchSetAncestor>(cnt);
+ for (int p = 0; p < cnt; p++) {
+ PatchSetAncestor a =
+ new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1));
+ a.setAncestorRevision(new RevId(src.getParent(p).getId().getName()));
+ toInsert.add(a);
+ }
+ db.patchSetAncestors().insert(toInsert);
+ }
- db.patchSetApprovals().upsert(Collections.singleton(approval));
+ /**
+ * Rebases a commit
+ *
+ * @param git Repository to find commits in
+ * @param original The commit to rebase
+ * @param base Base to rebase against
+ * @return CommitBuilder the newly rebased commit
+ * @throws IOException Merged failed
+ */
+ public static CommitBuilder rebaseCommit(Repository git, RevCommit original,
+ RevCommit base, PersonIdent committerIdent) throws IOException {
- final Change updatedChange = db.changes().atomicUpdate(changeId,
- new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- if (change.getStatus() == Change.Status.NEW) {
- change.setStatus(Change.Status.SUBMITTED);
- ChangeUtil.updated(change);
- }
- return change;
- }
- });
+ if (original.getParentCount() == 0) {
+ throw new IOException(
+ "Commits with no parents cannot be rebased (is this the initial commit?).");
+ }
- if (updatedChange.getStatus() == Change.Status.SUBMITTED) {
- merger.merge(opFactory, updatedChange.getDest());
+ if (original.getParentCount() > 1) {
+ throw new IOException(
+ "Patch sets with multiple parents cannot be rebased (merge commits)."
+ + " Parents: " + Arrays.toString(original.getParents()));
}
- }
- public static PatchSetApproval createSubmitApproval(
- final PatchSet.Id patchSetId, final IdentifiedUser user, final ReviewDb db
- ) throws OrmException {
- final List<PatchSetApproval> allApprovals =
- new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
- patchSetId).toList());
-
- final PatchSetApproval.Key akey =
- new PatchSetApproval.Key(patchSetId, user.getAccountId(), SUBMIT);
-
- for (final PatchSetApproval approval : allApprovals) {
- if (akey.equals(approval.getKey())) {
- approval.setValue((short) 1);
- approval.setGranted();
- return approval;
- }
+ final RevCommit parentCommit = original.getParent(0);
+
+ if (base.equals(parentCommit)) {
+ throw new IOException("Change is already up to date.");
+ }
+
+ final ThreeWayMerger merger = MergeStrategy.RESOLVE.newMerger(git, true);
+ merger.setBase(parentCommit);
+ merger.merge(original, base);
+
+ if (merger.getResultTreeId() == null) {
+ throw new IOException(
+ "The rebase failed since conflicts occured during the merge.");
}
- return new PatchSetApproval(akey, (short) 1);
+
+ final CommitBuilder rebasedCommitBuilder = new CommitBuilder();
+
+ rebasedCommitBuilder.setTreeId(merger.getResultTreeId());
+ rebasedCommitBuilder.setParentId(base);
+ rebasedCommitBuilder.setAuthor(original.getAuthorIdent());
+ rebasedCommitBuilder.setMessage(original.getFullMessage());
+ rebasedCommitBuilder.setCommitter(committerIdent);
+
+ return rebasedCommitBuilder;
}
- public static void abandon(final PatchSet.Id patchSetId,
- final IdentifiedUser user, final String message, final ReviewDb db,
- final AbandonedSender.Factory abandonedSenderFactory,
- final ChangeHookRunner hooks) throws NoSuchChangeException,
- EmailException, OrmException {
+ public static void rebaseChange(final PatchSet.Id patchSetId,
+ final IdentifiedUser user, final ReviewDb db,
+ RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
+ final ChangeHookRunner hooks, GitRepositoryManager gitManager,
+ final PatchSetInfoFactory patchSetInfoFactory,
+ final ReplicationQueue replication, PersonIdent myIdent,
+ final ChangeControl.Factory changeControlFactory,
+ final ApprovalTypes approvalTypes) throws NoSuchChangeException,
+ EmailException, OrmException, MissingObjectException,
+ IncorrectObjectTypeException, IOException,
+ PatchSetInfoNotAvailableException, InvalidChangeOperationException {
+
final Change.Id changeId = patchSetId.getParentKey();
- final PatchSet patch = db.patchSets().get(patchSetId);
- if (patch == null) {
- throw new NoSuchChangeException(changeId);
- }
+ final ChangeControl changeControl =
+ changeControlFactory.validateFor(changeId);
- final ChangeMessage cmsg =
- new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil
- .messageUUID(db)), user.getAccountId());
- final StringBuilder msgBuf =
- new StringBuilder("Patch Set " + patchSetId.get() + ": Abandoned");
- if (message != null && message.length() > 0) {
- msgBuf.append("\n\n");
- msgBuf.append(message);
+ if (!changeControl.canRebase()) {
+ throw new InvalidChangeOperationException(
+ "Cannot rebase: New patch sets are not allowed to be added to change: "
+ + changeId.toString());
}
- cmsg.setMessage(msgBuf.toString());
-
- final Change updatedChange = db.changes().atomicUpdate(changeId,
- new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- if (change.getStatus().isOpen()
- && change.currentPatchSetId().equals(patchSetId)) {
- change.setStatus(Change.Status.ABANDONED);
- ChangeUtil.updated(change);
- return change;
+
+ Change change = changeControl.getChange();
+ final Repository git = gitManager.openRepository(change.getProject());
+ try {
+ final RevWalk revWalk = new RevWalk(git);
+ try {
+ final PatchSet originalPatchSet = db.patchSets().get(patchSetId);
+ RevCommit branchTipCommit = null;
+
+ List<PatchSetAncestor> patchSetAncestors =
+ db.patchSetAncestors().ancestorsOf(patchSetId).toList();
+ if (patchSetAncestors.size() > 1) {
+ throw new IOException(
+ "The patch set you are trying to rebase is dependent on several other patch sets: "
+ + patchSetAncestors.toString());
+ }
+ if (patchSetAncestors.size() == 1) {
+ List<PatchSet> depPatchSetList = db.patchSets()
+ .byRevision(patchSetAncestors.get(0).getAncestorRevision())
+ .toList();
+ if (!depPatchSetList.isEmpty()) {
+ PatchSet depPatchSet = depPatchSetList.get(0);
+
+ Change.Id depChangeId = depPatchSet.getId().getParentKey();
+ Change depChange = db.changes().get(depChangeId);
+
+ if (depChange.getStatus() == Status.ABANDONED) {
+ throw new IOException("Cannot rebase against an abandoned change: "
+ + depChange.getKey().toString());
+ }
+ if (depChange.getStatus().isOpen()) {
+ PatchSet latestDepPatchSet =
+ db.patchSets().get(depChange.currentPatchSetId());
+ if (!depPatchSet.getId().equals(depChange.currentPatchSetId())) {
+ branchTipCommit =
+ revWalk.parseCommit(ObjectId
+ .fromString(latestDepPatchSet.getRevision().get()));
+ } else {
+ throw new IOException(
+ "Change is already based on the latest patch set of the dependent change.");
+ }
+ }
+ }
+ }
+
+ if (branchTipCommit == null) {
+ // We are dependent on a merged PatchSet or have no PatchSet
+ // dependencies at all.
+ Ref destRef = git.getRef(change.getDest().get());
+ if (destRef == null) {
+ throw new IOException(
+ "The destination branch does not exist: "
+ + change.getDest().get());
+ }
+ branchTipCommit = revWalk.parseCommit(destRef.getObjectId());
+ }
+
+ final RevCommit originalCommit =
+ revWalk.parseCommit(ObjectId.fromString(originalPatchSet
+ .getRevision().get()));
+
+ CommitBuilder rebasedCommitBuilder =
+ rebaseCommit(git, originalCommit, branchTipCommit, myIdent);
+
+ final ObjectInserter oi = git.newObjectInserter();
+ final ObjectId rebasedCommitId;
+ try {
+ rebasedCommitId = oi.insert(rebasedCommitBuilder);
+ oi.flush();
+ } finally {
+ oi.release();
+ }
+
+ Change updatedChange =
+ db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus().isOpen()) {
+ change.nextPatchSetId();
+ return change;
+ } else {
+ return null;
+ }
+ }
+ });
+
+ if (updatedChange == null) {
+ throw new InvalidChangeOperationException("Change is closed: "
+ + change.toString());
} else {
- return null;
+ change = updatedChange;
}
- }
- });
- if (updatedChange != null) {
- db.changeMessages().insert(Collections.singleton(cmsg));
+ final PatchSet rebasedPatchSet = new PatchSet(change.currPatchSetId());
+ rebasedPatchSet.setCreatedOn(change.getCreatedOn());
+ rebasedPatchSet.setUploader(user.getAccountId());
+ rebasedPatchSet.setRevision(new RevId(rebasedCommitId.getName()));
+
+ insertAncestors(db, rebasedPatchSet.getId(),
+ revWalk.parseCommit(rebasedCommitId));
+
+ db.patchSets().insert(Collections.singleton(rebasedPatchSet));
+ final PatchSetInfo info =
+ patchSetInfoFactory.get(db, rebasedPatchSet.getId());
+
+ change =
+ db.changes().atomicUpdate(change.getId(),
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ change.setCurrentPatchSet(info);
+ ChangeUtil.updated(change);
+ return change;
+ }
+ });
+
+ final RefUpdate ru = git.updateRef(rebasedPatchSet.getRefName());
+ ru.setNewObjectId(rebasedCommitId);
+ ru.disableRefLog();
+ if (ru.update(revWalk) != RefUpdate.Result.NEW) {
+ throw new IOException("Failed to create ref "
+ + rebasedPatchSet.getRefName() + " in " + git.getDirectory()
+ + ": " + ru.getResult());
+ }
- final List<PatchSetApproval> approvals =
- db.patchSetApprovals().byChange(changeId).toList();
- for (PatchSetApproval a : approvals) {
- a.cache(updatedChange);
- }
- db.patchSetApprovals().update(approvals);
+ replication.scheduleUpdate(change.getProject(), ru.getName());
- // Email the reviewers
- final AbandonedSender cm = abandonedSenderFactory.create(updatedChange);
- cm.setFrom(user.getAccountId());
- cm.setChangeMessage(cmsg);
- cm.send();
- }
+ ApprovalsUtil.copyVetosToLatestPatchSet(db, change, approvalTypes);
+
+ final ChangeMessage cmsg =
+ new ChangeMessage(new ChangeMessage.Key(changeId,
+ ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
+ cmsg.setMessage("Patch Set " + patchSetId.get() + ": Rebased");
+ db.changeMessages().insert(Collections.singleton(cmsg));
+
+ final Set<Account.Id> oldReviewers = new HashSet<Account.Id>();
+ final Set<Account.Id> oldCC = new HashSet<Account.Id>();
+
+ for (PatchSetApproval a : db.patchSetApprovals().byChange(change.getId())) {
+ if (a.getValue() != 0) {
+ oldReviewers.add(a.getAccountId());
+ } else {
+ oldCC.add(a.getAccountId());
+ }
+ }
- hooks.doChangeAbandonedHook(updatedChange, user.getAccount(), message);
+ final ReplacePatchSetSender cm =
+ rebasedPatchSetSenderFactory.create(change);
+ cm.setFrom(user.getAccountId());
+ cm.setPatchSet(rebasedPatchSet);
+ cm.addReviewers(oldReviewers);
+ cm.addExtraCC(oldCC);
+ cm.send();
+
+ hooks.doPatchsetCreatedHook(change, rebasedPatchSet, db);
+ } finally {
+ revWalk.release();
+ }
+ } finally {
+ git.close();
+ }
}
- public static void revert(final PatchSet.Id patchSetId,
+ public static Change.Id revert(final PatchSet.Id patchSetId,
final IdentifiedUser user, final String message, final ReviewDb db,
final RevertedSender.Factory revertedSenderFactory,
- final ChangeHookRunner hooks, GitRepositoryManager gitManager,
+ final ChangeHooks hooks, GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
final ReplicationQueue replication, PersonIdent myIdent)
throws NoSuchChangeException, EmailException, OrmException,
@@ -346,7 +501,7 @@ public class ChangeUtil {
final ChangeMessage cmsg =
new ChangeMessage(new ChangeMessage.Key(changeId,
- ChangeUtil.messageUUID(db)), user.getAccountId());
+ ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
final StringBuilder msgBuf =
new StringBuilder("Patch Set " + patchSetId.get() + ": Reverted");
msgBuf.append("\n\n");
@@ -360,68 +515,92 @@ public class ChangeUtil {
cm.setChangeMessage(cmsg);
cm.send();
- hooks.doPatchsetCreatedHook(change, ps);
+ hooks.doPatchsetCreatedHook(change, ps, db);
+
+ return change.getId();
} finally {
revWalk.release();
git.close();
}
}
- public static void restore(final PatchSet.Id patchSetId,
- final IdentifiedUser user, final String message, final ReviewDb db,
- final AbandonedSender.Factory abandonedSenderFactory,
- final ChangeHookRunner hooks) throws NoSuchChangeException,
- EmailException, OrmException {
+ public static void deleteDraftChange(final PatchSet.Id patchSetId,
+ GitRepositoryManager gitManager,
+ final ReplicationQueue replication, final ReviewDb db)
+ throws NoSuchChangeException, OrmException, IOException {
final Change.Id changeId = patchSetId.getParentKey();
- final PatchSet patch = db.patchSets().get(patchSetId);
- if (patch == null) {
+ final Change change = db.changes().get(changeId);
+ if (change == null || change.getStatus() != Change.Status.DRAFT) {
throw new NoSuchChangeException(changeId);
}
- final ChangeMessage cmsg =
- new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil
- .messageUUID(db)), user.getAccountId());
- final StringBuilder msgBuf =
- new StringBuilder("Patch Set " + patchSetId.get() + ": Restored");
- if (message != null && message.length() > 0) {
- msgBuf.append("\n\n");
- msgBuf.append(message);
+ for (PatchSet ps : db.patchSets().byChange(changeId)) {
+ // These should all be draft patch sets.
+ deleteOnlyDraftPatchSet(ps, change, gitManager, replication, db);
}
- cmsg.setMessage(msgBuf.toString());
-
- final Change updatedChange = db.changes().atomicUpdate(changeId,
- new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- if (change.getStatus() == Change.Status.ABANDONED
- && change.currentPatchSetId().equals(patchSetId)) {
- change.setStatus(Change.Status.NEW);
- ChangeUtil.updated(change);
- return change;
- } else {
- return null;
- }
- }
- });
- if (updatedChange != null) {
- db.changeMessages().insert(Collections.singleton(cmsg));
+ db.changeMessages().delete(db.changeMessages().byChange(changeId));
+ db.starredChanges().delete(db.starredChanges().byChange(changeId));
+ db.trackingIds().delete(db.trackingIds().byChange(changeId));
+ db.changes().delete(Collections.singleton(change));
+ }
- final List<PatchSetApproval> approvals =
- db.patchSetApprovals().byChange(changeId).toList();
- for (PatchSetApproval a : approvals) {
- a.cache(updatedChange);
+ public static void deleteOnlyDraftPatchSet(final PatchSet patch,
+ final Change change, GitRepositoryManager gitManager,
+ final ReplicationQueue replication, final ReviewDb db)
+ throws NoSuchChangeException, OrmException, IOException {
+ final PatchSet.Id patchSetId = patch.getId();
+ if (patch == null || !patch.isDraft()) {
+ throw new NoSuchChangeException(patchSetId.getParentKey());
+ }
+
+ Repository repo = gitManager.openRepository(change.getProject());
+ try {
+ RefUpdate update = repo.updateRef(patch.getRefName());
+ update.setForceUpdate(true);
+ update.disableRefLog();
+ switch (update.delete()) {
+ case NEW:
+ case FAST_FORWARD:
+ case FORCED:
+ case NO_CHANGE:
+ // Successful deletion.
+ break;
+ default:
+ throw new IOException("Failed to delete ref " + patch.getRefName() +
+ " in " + repo.getDirectory() + ": " + update.getResult());
}
- db.patchSetApprovals().update(approvals);
+ replication.scheduleUpdate(change.getProject(), update.getName());
+ } finally {
+ repo.close();
+ }
- // Email the reviewers
- final AbandonedSender cm = abandonedSenderFactory.create(updatedChange);
- cm.setFrom(user.getAccountId());
- cm.setChangeMessage(cmsg);
- cm.send();
+ db.accountPatchReviews().delete(db.accountPatchReviews().byPatchSet(patchSetId));
+ db.changeMessages().delete(db.changeMessages().byPatchSet(patchSetId));
+ db.patchComments().delete(db.patchComments().byPatchSet(patchSetId));
+ db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(patchSetId));
+ db.patchSetAncestors().delete(db.patchSetAncestors().byPatchSet(patchSetId));
+
+ db.patchSets().delete(Collections.singleton(patch));
+ }
+
+ public static <T extends ReplyToChangeSender> void updatedChange(
+ final ReviewDb db, final IdentifiedUser user, final Change change,
+ final ChangeMessage cmsg, ReplyToChangeSender.Factory<T> senderFactory,
+ final String err) throws NoSuchChangeException,
+ InvalidChangeOperationException, EmailException, OrmException {
+ if (change == null) {
+ throw new InvalidChangeOperationException(err);
}
+ db.changeMessages().insert(Collections.singleton(cmsg));
+
+ ApprovalsUtil.syncChangeStatus(db, change);
- hooks.doChangeRestoreHook(updatedChange, user.getAccount(), message);
+ // Email the reviewers
+ final ReplyToChangeSender cm = senderFactory.create(change);
+ cm.setFrom(user.getAccountId());
+ cm.setChangeMessage(cmsg);
+ cm.send();
}
public static String sortKey(long lastUpdated, int id){