diff options
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.java | 479 |
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){ |