Add rebase button to the change screen
This change adds a rebase button along with the rest of the action buttons in the change page. When pressing the button, the most recent patch set will be rebased onto the tip of the destination branch or the latest patchset of the change we depend upon. A new patch set containing the rebased commit will be produced and added to the change. Content-merging is always used, regardless of the project setting for use of content merges. This change started long ago as a draft by Magnus Bäck and the very nifty rebase trick (rebasing without the need for a working index) is his. Since then a lot has happen and I would like to discuss a bit around the design choices I made along the way: Design choices -------------- * Currently all users that can upload new patch sets to a change are also allowed to see the rebase-button. In the future, I plan to add a rebase capability to Gerrit, so the right to rebase other users' changes could be limited to include only certain powerusers (depending on project and refs). I prepared by adding the canRebase() to ChangeDetail and ChangeControl instead of just linking directly to canAddPatchSet(). * The emailing is subclassed from ReplacePatchSetSender which I think makes sense. * I lifted some code from ReceiveCommits, which allows me to move votes to new patch sets, to ApprovalsUtil.java. Some refactoring could be made in ReceiveCommits to make use of that code, but I would prefer to do it in a later and separate change due to the importance and complexity of ReceiveCommits. * As long as user has the correct access rights to execute a rebase within Gerrit, the button will be visible and enabled. I chose not to do any automagic merge-testing before showing the actual Rebase button. This approach was chosen since it contributes to cleaner code and better performance. We do not really want to execute merge tests just because the user is looking on the change-screen, especially since a rebase operation is not issued that often. * Of course, the rebase can fail for various reasons, and there are decent error messages explaining why the rebase was not allowed to be executed. * The new GerritWidgetCallback was introduced. Parts in gerrit-gwtui can and should be refactored to make use of this callback. Bug: issue 1035 Change-Id: I7eba96dfa769690eddce43fe9ea3c6cd39fe01a0
This commit is contained in:

committed by
Gustaf Lundh

parent
0ae1044fcb
commit
06e41266a2
@@ -29,6 +29,7 @@ public class ChangeDetail {
|
||||
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 @@ public class ChangeDetail {
|
||||
canPublish = a;
|
||||
}
|
||||
|
||||
public boolean canRebase() {
|
||||
return canRebase;
|
||||
}
|
||||
|
||||
public void setCanRebase(final boolean a) {
|
||||
canRebase = a;
|
||||
}
|
||||
|
||||
public boolean canRestore() {
|
||||
return canRestore;
|
||||
}
|
||||
|
@@ -44,4 +44,7 @@ public interface ChangeManageService extends RemoteJsonService {
|
||||
|
||||
@SignInRequired
|
||||
void deleteDraftChange(PatchSet.Id patchSetId, AsyncCallback<VoidResult> callback);
|
||||
|
||||
@SignInRequired
|
||||
void rebaseChange(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
|
||||
}
|
||||
|
@@ -111,6 +111,8 @@ public interface ChangeConstants extends Constants {
|
||||
String patchSetInfoParents();
|
||||
String initialCommit();
|
||||
|
||||
String buttonRebaseChange();
|
||||
|
||||
String buttonRevertChangeBegin();
|
||||
String buttonRevertChangeSend();
|
||||
String headingRevertMessage();
|
||||
|
@@ -96,6 +96,8 @@ oldVersionHistory = Old Version History:
|
||||
baseDiffItem = Base
|
||||
autoMerge = Auto Merge
|
||||
|
||||
buttonRebaseChange = Rebase Change
|
||||
|
||||
buttonRevertChangeBegin = Revert Change
|
||||
buttonRevertChangeSend = Revert Change
|
||||
headingRevertMessage = Revert Commit Message:
|
||||
|
@@ -19,6 +19,7 @@ import com.google.gerrit.common.data.ChangeDetail;
|
||||
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 @@ public class ChangeDetailCache extends ListenableValue<ChangeDetail> {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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) {
|
||||
|
@@ -321,6 +321,8 @@ public class ChangeScreen extends Screen
|
||||
}
|
||||
|
||||
dependenciesPanel.setOpen(depsOpen);
|
||||
|
||||
dependenciesPanel.getHeader().clear();
|
||||
if (outdated > 0) {
|
||||
dependenciesPanel.getHeader().add(new InlineLabel(
|
||||
Util.M.outdatedHeader(outdated)));
|
||||
|
@@ -229,7 +229,10 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
|
||||
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
|
||||
|
@@ -551,6 +551,19 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
|
||||
});
|
||||
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) {
|
||||
|
@@ -129,6 +129,8 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
|
||||
|
||||
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()) {
|
||||
|
@@ -24,6 +24,7 @@ import com.google.inject.Inject;
|
||||
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 @@ class ChangeManageServiceImpl implements ChangeManageService {
|
||||
@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 @@ class ChangeManageServiceImpl implements ChangeManageService {
|
||||
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);
|
||||
|
@@ -31,6 +31,7 @@ public class ChangeModule extends RpcServletModule {
|
||||
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);
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -432,6 +432,10 @@ public final class Change {
|
||||
lastUpdatedOn = new Timestamp(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public int getNumberOfPatchSets() {
|
||||
return nbrPatchSets;
|
||||
}
|
||||
|
||||
public String getSortKey() {
|
||||
return sortKey;
|
||||
}
|
||||
|
@@ -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 @@ public class ApprovalsUtil {
|
||||
}
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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.GitRepositoryManager;
|
||||
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.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;
|
||||
@@ -53,6 +66,8 @@ import org.eclipse.jgit.util.Base64;
|
||||
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 @@ public class ChangeUtil {
|
||||
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,
|
||||
|
@@ -44,6 +44,7 @@ import com.google.gerrit.server.mail.CommentSender;
|
||||
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 @@ public class GerritRequestModule extends FactoryModule {
|
||||
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);
|
||||
|
@@ -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"));
|
||||
}
|
||||
}
|
@@ -199,6 +199,11 @@ public class ChangeControl {
|
||||
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
|
||||
|
@@ -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
|
Reference in New Issue
Block a user