Merge "Add rebase button to the change screen"
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
index 3a6bca8..74d1962 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
@@ -29,6 +29,7 @@
protected boolean allowsAnonymous;
protected boolean canAbandon;
protected boolean canPublish;
+ protected boolean canRebase;
protected boolean canRestore;
protected boolean canRevert;
protected boolean canDeleteDraft;
@@ -80,6 +81,14 @@
canPublish = a;
}
+ public boolean canRebase() {
+ return canRebase;
+ }
+
+ public void setCanRebase(final boolean a) {
+ canRebase = a;
+ }
+
public boolean canRestore() {
return canRestore;
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java
index a43ac94..18453ca 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java
@@ -44,4 +44,7 @@
@SignInRequired
void deleteDraftChange(PatchSet.Id patchSetId, AsyncCallback<VoidResult> callback);
+
+ @SignInRequired
+ void rebaseChange(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 0af35b6..3372096 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -111,6 +111,8 @@
String patchSetInfoParents();
String initialCommit();
+ String buttonRebaseChange();
+
String buttonRevertChangeBegin();
String buttonRevertChangeSend();
String headingRevertMessage();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 6735b63..ad70674 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -96,6 +96,8 @@
baseDiffItem = Base
autoMerge = Auto Merge
+buttonRebaseChange = Rebase Change
+
buttonRevertChangeBegin = Revert Change
buttonRevertChangeSend = Revert Change
headingRevertMessage = Revert Commit Message:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
index 7245db5..8f48ebd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
@@ -19,6 +19,7 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.FocusWidget;
public class ChangeDetailCache extends ListenableValue<ChangeDetail> {
public static class GerritCallback extends
@@ -29,6 +30,27 @@
}
}
+ /*
+ * GerritCallback which will re-enable a FocusWidget
+ * {@link com.google.gwt.user.client.ui.FocusWidget} if we are returning
+ * with a failed result.
+ *
+ * It is up to the caller to handle the original disabling of the Widget.
+ */
+ public static class GerritWidgetCallback extends GerritCallback {
+ private FocusWidget widget;
+
+ public GerritWidgetCallback(FocusWidget widget) {
+ this.widget = widget;
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ widget.setEnabled(true);
+ super.onFailure(caught);
+ }
+ }
+
public static class IgnoreErrorCallback implements AsyncCallback<ChangeDetail> {
@Override
public void onSuccess(ChangeDetail detail) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index 867abdd..8d5e105 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
@@ -321,6 +321,8 @@
}
dependenciesPanel.setOpen(depsOpen);
+
+ dependenciesPanel.getHeader().clear();
if (outdated > 0) {
dependenciesPanel.getHeader().add(new InlineLabel(
Util.M.outdatedHeader(outdated)));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 943e967..7771bcf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -229,7 +229,10 @@
if (! c.isLatest()) {
s += " [OUTDATED]";
table.getRowFormatter().addStyleName(row, Gerrit.RESOURCES.css().outdated());
+ } else {
+ table.getRowFormatter().removeStyleName(row, Gerrit.RESOURCES.css().outdated());
}
+
table.setWidget(row, C_SUBJECT, new TableChangeLink(s, c));
table.setWidget(row, C_OWNER, link(c.getOwner()));
table.setWidget(row, C_PROJECT, new ProjectLink(c.getProject().getKey(), c
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index 199dc9d..50a8a6a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
@@ -551,6 +551,19 @@
});
actionsPanel.add(b);
}
+
+ if (changeDetail.canRebase()) {
+ final Button b = new Button(Util.C.buttonRebaseChange());
+ b.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ b.setEnabled(false);
+ Util.MANAGE_SVC.rebaseChange(patchSet.getId(),
+ new ChangeDetailCache.GerritWidgetCallback(b));
+ }
+ });
+ actionsPanel.add(b);
+ }
}
private void populateDiffAllActions(final PatchSetDetail detail) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index 23d5865..47a9395 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
@@ -129,6 +129,8 @@
detail.setCanRevert(change.getStatus() == Change.Status.MERGED && control.canAddPatchSet());
+ detail.setCanRebase(detail.getChange().getStatus().isOpen() && control.canRebase());
+
detail.setCanEdit(control.getRefControl().canWrite());
if (detail.getChange().getStatus().isOpen()) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java
index e53f0bf..4178437 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java
@@ -24,6 +24,7 @@
class ChangeManageServiceImpl implements ChangeManageService {
private final SubmitAction.Factory submitAction;
private final AbandonChangeHandler.Factory abandonChangeHandlerFactory;
+ private final RebaseChange.Factory rebaseChangeFactory;
private final RestoreChangeHandler.Factory restoreChangeHandlerFactory;
private final RevertChange.Factory revertChangeFactory;
private final PublishAction.Factory publishAction;
@@ -32,12 +33,14 @@
@Inject
ChangeManageServiceImpl(final SubmitAction.Factory patchSetAction,
final AbandonChangeHandler.Factory abandonChangeHandlerFactory,
+ final RebaseChange.Factory rebaseChangeFactory,
final RestoreChangeHandler.Factory restoreChangeHandlerFactory,
final RevertChange.Factory revertChangeFactory,
final PublishAction.Factory publishAction,
final DeleteDraftChange.Factory deleteDraftChangeFactory) {
this.submitAction = patchSetAction;
this.abandonChangeHandlerFactory = abandonChangeHandlerFactory;
+ this.rebaseChangeFactory = rebaseChangeFactory;
this.restoreChangeHandlerFactory = restoreChangeHandlerFactory;
this.revertChangeFactory = revertChangeFactory;
this.publishAction = publishAction;
@@ -54,6 +57,11 @@
abandonChangeHandlerFactory.create(patchSetId, message).to(callback);
}
+ public void rebaseChange(final PatchSet.Id patchSetId,
+ final AsyncCallback<ChangeDetail> callback) {
+ rebaseChangeFactory.create(patchSetId).to(callback);
+ }
+
public void revertChange(final PatchSet.Id patchSetId, final String message,
final AsyncCallback<ChangeDetail> callback) {
revertChangeFactory.create(patchSetId, message).to(callback);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
index 4224293..b672a439 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
@@ -31,6 +31,7 @@
factory(AbandonChangeHandler.Factory.class);
factory(RestoreChangeHandler.Factory.class);
factory(RevertChange.Factory.class);
+ factory(RebaseChange.Factory.class);
factory(ChangeDetailFactory.Factory.class);
factory(IncludedInDetailFactory.Factory.class);
factory(PatchSetDetailFactory.Factory.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
new file mode 100644
index 0000000..3c29074
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2012 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.google.gerrit.httpd.rpc.changedetail;
+
+import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+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.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.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.PersonIdent;
+
+import java.io.IOException;
+
+class RebaseChange extends Handler<ChangeDetail> {
+ interface Factory {
+ RebaseChange create(PatchSet.Id patchSetId);
+ }
+
+ private final ChangeControl.Factory changeControlFactory;
+ private final ReviewDb db;
+ private final IdentifiedUser currentUser;
+ private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory;
+
+ private final ChangeDetailFactory.Factory changeDetailFactory;
+ private final ReplicationQueue replication;
+
+ private final PatchSet.Id patchSetId;
+
+ private final ChangeHookRunner hooks;
+
+ private final GitRepositoryManager gitManager;
+ private final PatchSetInfoFactory patchSetInfoFactory;
+
+ private final PersonIdent myIdent;
+
+ private final ApprovalTypes approvalTypes;
+
+ @Inject
+ RebaseChange(final ChangeControl.Factory changeControlFactory,
+ final ReviewDb db, final IdentifiedUser currentUser,
+ final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
+ final ChangeDetailFactory.Factory changeDetailFactory,
+ @Assisted final PatchSet.Id patchSetId, final ChangeHookRunner hooks,
+ final GitRepositoryManager gitManager,
+ final PatchSetInfoFactory patchSetInfoFactory,
+ final ReplicationQueue replication,
+ @GerritPersonIdent final PersonIdent myIdent,
+ final ApprovalTypes approvalTypes) {
+ this.changeControlFactory = changeControlFactory;
+ this.db = db;
+ this.currentUser = currentUser;
+ this.rebasedPatchSetSenderFactory = rebasedPatchSetSenderFactory;
+ this.changeDetailFactory = changeDetailFactory;
+
+ this.patchSetId = patchSetId;
+ this.hooks = hooks;
+ this.gitManager = gitManager;
+
+ this.patchSetInfoFactory = patchSetInfoFactory;
+ this.replication = replication;
+ this.myIdent = myIdent;
+
+ this.approvalTypes = approvalTypes;
+ }
+
+ @Override
+ public ChangeDetail call() throws NoSuchChangeException, OrmException,
+ EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException,
+ MissingObjectException, IncorrectObjectTypeException, IOException,
+ InvalidChangeOperationException {
+
+ ChangeUtil.rebaseChange(patchSetId, currentUser, db,
+ rebasedPatchSetSenderFactory, hooks, gitManager, patchSetInfoFactory,
+ replication, myIdent, changeControlFactory, approvalTypes);
+
+ return changeDetailFactory.create(patchSetId.getParentKey()).call();
+ }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 42e822f..61e6a97 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -432,6 +432,10 @@
lastUpdatedOn = new Timestamp(System.currentTimeMillis());
}
+ public int getNumberOfPatchSets() {
+ return nbrPatchSets;
+ }
+
public String getSortKey() {
return sortKey;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index 19e3e00..3417111 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -14,11 +14,17 @@
package com.google.gerrit.server;
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import java.util.Collections;
import java.util.List;
public class ApprovalsUtil {
@@ -33,4 +39,36 @@
}
db.patchSetApprovals().update(approvals);
}
+
+ /**
+ * Moves the PatchSetApprovals to the last PatchSet on the change while
+ * keeping the vetos.
+ *
+ * @param db The review database
+ * @param change Change to update
+ * @param approvalTypes The approval types
+ * @throws OrmException
+ * @throws IOException
+ */
+ public static void copyVetosToLatestPatchSet(final ReviewDb db, Change change,
+ ApprovalTypes approvalTypes) throws OrmException, IOException {
+ PatchSet.Id source;
+ if (change.getNumberOfPatchSets() > 1) {
+ source = new PatchSet.Id(change.getId(), change.getNumberOfPatchSets() - 1);
+ } else {
+ throw new IOException("Previous patch set could not be found");
+ }
+
+ PatchSet.Id dest = change.currPatchSetId();
+ for (PatchSetApproval a : db.patchSetApprovals().byPatchSet(source)) {
+ // ApprovalCategory.SUBMIT is still in db but not relevant in git-store
+ if (!ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
+ final ApprovalType type = approvalTypes.byId(a.getCategoryId());
+ if (type.getCategory().isCopyMinScore() && type.isMaxNegative(a)) {
+ db.patchSetApprovals().insert(
+ Collections.singleton(new PatchSetApproval(dest, a)));
+ }
+ }
+ }
+ }
}
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 bbcb14b..9dc572d 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,10 +14,16 @@
package com.google.gerrit.server;
+import com.google.gerrit.common.ChangeHookRunner;
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;
@@ -28,12 +34,16 @@
import com.google.gerrit.server.git.MergeOp;
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.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmConcurrencyException;
import com.google.gwtorm.server.OrmException;
@@ -44,8 +54,11 @@
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;
@@ -53,6 +66,8 @@
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;
@@ -159,6 +174,252 @@
opFactory.create(change.getDest()).verifyMergeability(change);
}
+ public static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src)
+ throws OrmException {
+ 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);
+ }
+
+ /**
+ * 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 {
+
+ 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.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 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 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 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 {
+ change = updatedChange;
+ }
+
+ 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());
+ }
+
+ replication.scheduleUpdate(change.getProject(), ru.getName());
+
+ 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());
+ }
+ }
+
+ 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 Change.Id revert(final PatchSet.Id patchSetId,
final IdentifiedUser user, final String message, final ReviewDb db,
final RevertedSender.Factory revertedSenderFactory,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index bf741a4..00562b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -44,6 +44,7 @@
import com.google.gerrit.server.mail.CreateChangeSender;
import com.google.gerrit.server.mail.MergeFailSender;
import com.google.gerrit.server.mail.MergedSender;
+import com.google.gerrit.server.mail.RebasedPatchSetSender;
import com.google.gerrit.server.mail.ReplacePatchSetSender;
import com.google.gerrit.server.mail.RestoredSender;
import com.google.gerrit.server.mail.RevertedSender;
@@ -95,6 +96,7 @@
factory(PublishComments.Factory.class);
factory(PublishDraft.Factory.class);
factory(ReplacePatchSetSender.Factory.class);
+ factory(RebasedPatchSetSender.Factory.class);
factory(AbandonedSender.Factory.class);
factory(RemoveReviewer.Factory.class);
factory(RestoreChange.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java
new file mode 100644
index 0000000..8fc8238
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2012 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.google.gerrit.server.mail;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/** Send notice to reviewers that a change has been rebased. */
+public class RebasedPatchSetSender extends ReplacePatchSetSender {
+ public static interface Factory {
+ RebasedPatchSetSender create(Change change);
+ }
+
+ @Inject
+ public RebasedPatchSetSender(EmailArguments ea,
+ @AnonymousCowardName String anonymousCowardName, SshInfo si,
+ @Assisted Change c) {
+ super(ea, anonymousCowardName, si, c);
+ }
+
+ @Override
+ protected void formatChange() throws EmailException {
+ appendText(velocifyFile("RebasedPatchSet.vm"));
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 3e65a11..1a50030 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -199,6 +199,11 @@
return isOwner() && isVisible(db);
}
+ /** Can this user rebase this change? */
+ public boolean canRebase() {
+ return canAddPatchSet();
+ }
+
/** Can this user restore this change? */
public boolean canRestore() {
return canAbandon(); // Anyone who can abandon the change can restore it back
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RebasedPatchSet.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RebasedPatchSet.vm
new file mode 100644
index 0000000..e761627
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RebasedPatchSet.vm
@@ -0,0 +1,54 @@
+## Copyright (C) 2012 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.
+##
+##
+## Template Type:
+## -------------
+## This is a velocity mail template, see: http://velocity.apache.org and the
+## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates.
+##
+## Template File Names and extensions:
+## ----------------------------------
+## Gerrit will use templates ending in ".vm" but will ignore templates ending
+## in ".vm.example". If a .vm template does not exist, the default internal
+## gerrit template which is the same as the .vm.example will be used. If you
+## want to override the default template, copy the .vm.example file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The RebasedPatchSet.vm template will determine the contents of the email
+## related to a user rebasing a patchset for a change through the Gerrit UI.
+## It is a ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm.
+##
+#if($email.reviewerNames)
+Hello $email.joinStrings($email.reviewerNames, ', '),
+
+I'd like you to reexamine a rebased change.#if($email.changeUrl) Please visit
+
+ $email.changeUrl
+
+to look at the new rebased patch set (#$patchSet.patchSetId).
+#end
+#else
+$fromName has created a new patch set by issuing a rebase in Gerrit (#$patchSet.patchSetId).
+#end
+
+Change subject: $change.subject
+......................................................................
+
+$email.changeDetail
+#if($email.sshHost)
+ git pull ssh://$email.sshHost/$projectName $patchSet.refName
+#end