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 3a6bca8ed1..74d19622a5 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 @@ 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; } 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 a43ac94578..18453ca7b6 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 @@ public interface ChangeManageService extends RemoteJsonService { @SignInRequired void deleteDraftChange(PatchSet.Id patchSetId, AsyncCallback callback); + + @SignInRequired + void rebaseChange(PatchSet.Id patchSetId, AsyncCallback 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 0af35b6b95..3372096a5c 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 @@ public interface ChangeConstants extends Constants { 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 6735b63585..ad7067408c 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 @@ oldVersionHistory = Old Version History: 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 7245db51e0..8f48ebd4b8 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.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 { public static class GerritCallback extends @@ -29,6 +30,27 @@ public class ChangeDetailCache extends ListenableValue { } } + /* + * 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 { @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 867abddf5f..8d5e105172 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 @@ public class ChangeScreen extends Screen } 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 943e96715c..7771bcf7c3 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 @@ public class ChangeTable extends NavigationTable { 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 199dc9d808..50a8a6aba3 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 @@ 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) { 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 23d5865f3d..47a9395e02 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 @@ public class ChangeDetailFactory extends Handler { 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 e53f0bf22a..4178437aac 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 @@ 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 callback) { + rebaseChangeFactory.create(patchSetId).to(callback); + } + public void revertChange(final PatchSet.Id patchSetId, final String message, final AsyncCallback 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 42242930d3..b672a43977 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 @@ 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); 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 0000000000..3c29074704 --- /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 { + 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 42e822fd70..61e6a97e92 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 @@ public final class Change { 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 19e3e00fd4..3417111595 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 @@ 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))); + } + } + } + } } 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 bbcb14ba28..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,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 toInsert = new ArrayList(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 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 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() { + @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() { + @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 oldReviewers = new HashSet(); + final Set oldCC = new HashSet(); + + 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 bf741a4b90..00562b0bd5 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.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); 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 0000000000..8fc82380cb --- /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 3e65a11263..1a500309a4 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 @@ 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 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 0000000000..e761627691 --- /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