Refactor code for rebasing changes to make it reusable

A new submit strategy 'Rebase If Necessary' should be introduced that
automatically rebases the changes on submit if needed. Refactor the
existing code for rebasing changes from the WebUI to be reusable from
this new submit strategy.

Change-Id: I0ac803deea68f9e1294b3fa5bf814277e899f075
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin
2012-09-12 09:17:16 +02:00
committed by Gerrit Code Review
parent a085d501c0
commit a54984665d
6 changed files with 504 additions and 393 deletions

View File

@@ -14,17 +14,11 @@
package com.google.gerrit.server;
import com.google.common.collect.Sets;
import com.google.gerrit.common.ChangeHookRunner;
import com.google.gerrit.common.ChangeHooks;
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;
@@ -34,15 +28,10 @@ import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeOp;
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.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmConcurrencyException;
import com.google.gwtorm.server.OrmException;
@@ -53,11 +42,8 @@ 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;
@@ -68,9 +54,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -193,270 +177,6 @@ public class ChangeUtil {
db.patchSetAncestors().insert(toInsert);
}
/**
* Rebases a commit
*
* @param git Repository to find commits in
* @param inserter inserter to handle new trees and blobs.
* @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,
final ObjectInserter inserter, RevCommit original, RevCommit base,
PersonIdent committerIdent) throws IOException {
if (original.getParentCount() == 0) {
throw new IOException(
"Commits with no parents cannot be rebased (is this the initial commit?).");
}
if (original.getParentCount() > 1) {
throw new IOException(
"Patch sets with multiple parents cannot be rebased (merge commits)."
+ " Parents: " + Arrays.toString(original.getParents()));
}
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.setObjectInserter(new ObjectInserter.Filter() {
@Override
protected ObjectInserter delegate() {
return inserter;
}
@Override
public void flush() {
}
@Override
public void release() {
}
});
merger.setBase(parentCommit);
merger.merge(original, base);
if (merger.getResultTreeId() == null) {
throw new IOException(
"The rebase failed since conflicts occured during the merge.");
}
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 rebaseChange(final PatchSet.Id patchSetId,
final IdentifiedUser user, final ReviewDb db,
RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
final ChangeHookRunner hooks, GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
final GitReferenceUpdated replication, PersonIdent myIdent,
final ChangeControl.Factory changeControlFactory,
final ApprovalsUtil approvalsUtil) throws NoSuchChangeException,
EmailException, OrmException, MissingObjectException,
IncorrectObjectTypeException, IOException,
InvalidChangeOperationException {
final Change.Id changeId = patchSetId.getParentKey();
final ChangeControl changeControl =
changeControlFactory.validateFor(changeId);
if (!changeControl.canRebase()) {
throw new InvalidChangeOperationException(
"Cannot rebase: New patch sets are not allowed to be added to change: "
+ changeId.toString());
}
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 rebasedCommit;
final ObjectInserter oi = git.newObjectInserter();
try {
ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get());
ObjectId newId = oi.insert(rebaseCommit(
git, oi, revWalk.parseCommit(oldId), branchTipCommit, myIdent));
oi.flush();
rebasedCommit = revWalk.parseCommit(newId);
} finally {
oi.release();
}
change.nextPatchSetId();
final PatchSet newPatchSet = new PatchSet(change.currPatchSetId());
newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
newPatchSet.setUploader(user.getAccountId());
newPatchSet.setRevision(new RevId(rebasedCommit.name()));
newPatchSet.setDraft(originalPatchSet.isDraft());
final PatchSetInfo info =
patchSetInfoFactory.get(rebasedCommit, newPatchSet.getId());
RefUpdate ru = git.updateRef(newPatchSet.getRefName());
ru.setExpectedOldObjectId(ObjectId.zeroId());
ru.setNewObjectId(rebasedCommit);
ru.disableRefLog();
if (ru.update(revWalk) != RefUpdate.Result.NEW) {
throw new IOException(String.format(
"Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
change.getDest().getParentKey().get(), ru.getResult()));
}
replication.fire(change.getProject(), ru.getName());
final Set<Account.Id> oldReviewers = Sets.newHashSet();
final Set<Account.Id> oldCC = Sets.newHashSet();
db.changes().beginTransaction(change.getId());
try {
Change updatedChange;
updatedChange = db.changes().atomicUpdate(changeId,
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
if (change.getStatus().isOpen()) {
change.updateNumberOfPatchSets(newPatchSet.getPatchSetId());
return change;
} else {
return null;
}
}
});
if (updatedChange != null) {
change = updatedChange;
} else {
throw new InvalidChangeOperationException(
String.format("Change %s is closed", change.getId()));
}
insertAncestors(db, newPatchSet.getId(), rebasedCommit);
db.patchSets().insert(Collections.singleton(newPatchSet));
updatedChange = db.changes().atomicUpdate(changeId,
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
if (change.getStatus().isClosed()) {
return null;
}
if (!change.currentPatchSetId().equals(patchSetId)) {
return null;
}
if (change.getStatus() != Change.Status.DRAFT) {
change.setStatus(Change.Status.NEW);
}
change.setLastSha1MergeTested(null);
change.setCurrentPatchSet(info);
ChangeUtil.updated(change);
return change;
}
});
if (updatedChange != null) {
change = updatedChange;
} else {
throw new InvalidChangeOperationException(
String.format("Change %s was modified", change.getId()));
}
for (PatchSetApproval a : approvalsUtil.copyVetosToLatestPatchSet(change)) {
if (a.getValue() != 0) {
oldReviewers.add(a.getAccountId());
} else {
oldCC.add(a.getAccountId());
}
}
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));
db.commit();
} finally {
db.rollback();
}
final ReplacePatchSetSender cm =
rebasedPatchSetSenderFactory.create(change);
cm.setFrom(user.getAccountId());
cm.setPatchSet(newPatchSet);
cm.addReviewers(oldReviewers);
cm.addExtraCC(oldCC);
cm.send();
hooks.doPatchsetCreatedHook(change, newPatchSet, db);
} finally {
revWalk.release();
}
} finally {
git.close();
}
}
public static Change.Id revert(final PatchSet.Id patchSetId,
final IdentifiedUser user, final String message, final ReviewDb db,
final RevertedSender.Factory revertedSenderFactory,