Adds a "revert change"-button to a submitted patchset.

Change-Id: I409e656c88a7021f604c021ca3987d2e917c18d9
This commit is contained in:
Gustaf Lundh 2011-05-18 19:58:59 -07:00 committed by Ulrik Sjölin
parent 0908bff975
commit 262629e6d3
18 changed files with 425 additions and 3 deletions

View File

@ -31,6 +31,7 @@ public class ChangeDetail {
protected boolean allowsAnonymous;
protected boolean canAbandon;
protected boolean canRestore;
protected boolean canRevert;
protected Change change;
protected boolean starred;
protected List<ChangeInfo> dependsOn;
@ -78,6 +79,14 @@ public class ChangeDetail {
canRestore = a;
}
public boolean canRevert() {
return canRevert;
}
public void setCanRevert(boolean a) {
canRevert = a;
}
public Change getChange() {
return change;
}

View File

@ -30,6 +30,10 @@ public interface ChangeManageService extends RemoteJsonService {
void abandonChange(PatchSet.Id patchSetId, String message,
AsyncCallback<ChangeDetail> callback);
@SignInRequired
void revertChange(PatchSet.Id patchSetId, String message,
AsyncCallback<ChangeDetail> callback);
@SignInRequired
void restoreChange(PatchSet.Id patchSetId, String message,
AsyncCallback<ChangeDetail> callback);

View File

@ -20,6 +20,8 @@ public interface GerritCss extends CssResource {
String greenCheckClass();
String abandonChangeDialog();
String abandonMessage();
String revertChangeDialog();
String revertMessage();
String accountContactOnFile();
String accountContactPrivacyDetails();
String accountDashboard();

View File

@ -100,6 +100,12 @@ public interface ChangeConstants extends Constants {
String patchSetInfoCommitter();
String patchSetInfoDownload();
String buttonRevertChangeBegin();
String buttonRevertChangeSend();
String buttonRevertChangeCancel();
String headingRevertMessage();
String revertChangeTitle();
String buttonAbandonChangeBegin();
String buttonAbandonChangeSend();
String buttonAbandonChangeCancel();

View File

@ -83,6 +83,12 @@ buttonAbandonChangeCancel = Cancel
headingAbandonMessage = Abandon Message:
abandonChangeTitle = Code Review - Abandon Change
buttonRevertChangeBegin = Revert Change
buttonRevertChangeSend = Revert Change
buttonRevertChangeCancel = Cancel
headingRevertMessage = Revert Commit Message:
revertChangeTitle = Code Review - Revert Merged Change
buttonRestoreChangeBegin = Restore Change
restoreChangeTitle = Code Review - Restore Change
buttonRestoreChangeCancel = Cancel

View File

@ -24,6 +24,8 @@ public interface ChangeMessages extends Messages {
String changesMergedInProject(String string);
String changesAbandonedInProject(String string);
String revertChangeDefaultMessage(String commitMsg, String commitId);
String changeScreenTitleId(String changeId);
String patchSetHeader(int id);
String loadingPatchSet(int id);

View File

@ -5,6 +5,8 @@ changesOpenInProject = Open Changes In {0}
changesMergedInProject = Merged Changes In {0}
changesAbandonedInProject = Abandoned Changes In {0}
revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}
changeScreenTitleId = Change {0}
patchSetHeader = Patch Set {0}
loadingPatchSet = Loading Patch Set {0} ...

View File

@ -47,6 +47,14 @@ public abstract class CommentedChangeActionDialog extends AutoCenterDialogBox im
final String dialogHeading, final String buttonSend,
final String buttonCancel, final String dialogStyle,
final String messageStyle) {
this(psi, callback, dialogTitle, dialogHeading, buttonSend, buttonCancel, dialogStyle, messageStyle, null);
}
public CommentedChangeActionDialog(final PatchSet.Id psi,
final AsyncCallback<ChangeDetail> callback, final String dialogTitle,
final String dialogHeading, final String buttonSend,
final String buttonCancel, final String dialogStyle,
final String messageStyle, final String defaultMessage) {
super(/* auto hide */false, /* modal */true);
setGlassEnabled(true);
@ -67,6 +75,7 @@ public abstract class CommentedChangeActionDialog extends AutoCenterDialogBox im
message = new NpTextArea();
message.setCharacterWidth(60);
message.setVisibleLines(10);
message.setText(defaultMessage);
DOM.setElementPropertyBoolean(message.getElement(), "spellcheck", true);
mwrap.add(message);

View File

@ -395,6 +395,26 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel implements O
actionsPanel.add(b);
}
if (changeDetail.canRevert()) {
final Button b = new Button(Util.C.buttonRevertChangeBegin());
b.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
b.setEnabled(false);
new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b),
Util.C.revertChangeTitle(), Util.C.headingRevertMessage(),
Util.C.buttonRevertChangeSend(), Util.C.buttonRevertChangeCancel(),
Gerrit.RESOURCES.css().revertChangeDialog(), Gerrit.RESOURCES.css().revertMessage(),
Util.M.revertChangeDefaultMessage(detail.getInfo().getSubject(), detail.getPatchSet().getRevision().get())) {
public void onSend() {
Util.MANAGE_SVC.revertChange(getPatchSetId() , getMessageText(), createCallback());
}
}.center();
}
});
actionsPanel.add(b);
}
if (changeDetail.canAbandon()) {
final Button b = new Button(Util.C.buttonAbandonChangeBegin());
b.addClickHandler(new ClickHandler() {

View File

@ -1230,6 +1230,31 @@ a:hover.downloadLink {
font-size: small;
}
/** RevertChangeDialog **/
.revertChangeDialog .gwt-DisclosurePanel .header td {
font-weight: bold;
white-space: nowrap;
}
.revertChangeDialog .smallHeading {
font-size: small;
font-weight: bold;
white-space: nowrap;
}
.revertChangeDialog .revertMessage {
margin-left: 10px;
background: trimColor;
padding: 5px 5px 5px 5px;
}
.revertChangeDialog .revertMessage textarea {
font-size: small;
}
.revertChangeDialog .gwt-Hyperlink {
white-space: nowrap;
font-size: small;
}
/** PatchBrowserPopup **/
.patchBrowserPopup {

View File

@ -100,10 +100,14 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
detail = new ChangeDetail();
detail.setChange(change);
detail.setAllowsAnonymous(control.forAnonymousUser().isVisible());
detail.setCanAbandon(change.getStatus().isOpen() && control.canAbandon());
detail.setCanRestore(change.getStatus() == Change.Status.ABANDONED && control.canRestore());
detail.setStarred(control.getCurrentUser().getStarredChanges().contains(
changeId));
detail.setCanRevert(change.getStatus() == Change.Status.MERGED && control.canAddPatchSet());
loadPatchSets();
loadMessages();
if (change.currentPatchSetId() != null) {

View File

@ -24,14 +24,17 @@ class ChangeManageServiceImpl implements ChangeManageService {
private final SubmitAction.Factory submitAction;
private final AbandonChange.Factory abandonChangeFactory;
private final RestoreChange.Factory restoreChangeFactory;
private final RevertChange.Factory revertChangeFactory;
@Inject
ChangeManageServiceImpl(final SubmitAction.Factory patchSetAction,
final AbandonChange.Factory abandonChangeFactory,
final RestoreChange.Factory restoreChangeFactory) {
final RestoreChange.Factory restoreChangeFactory,
final RevertChange.Factory revertChangeFactory) {
this.submitAction = patchSetAction;
this.abandonChangeFactory = abandonChangeFactory;
this.restoreChangeFactory = restoreChangeFactory;
this.revertChangeFactory = revertChangeFactory;
}
public void submit(final PatchSet.Id patchSetId,
@ -44,6 +47,11 @@ class ChangeManageServiceImpl implements ChangeManageService {
abandonChangeFactory.create(patchSetId, message).to(callback);
}
public void revertChange(final PatchSet.Id patchSetId, final String message,
final AsyncCallback<ChangeDetail> callback) {
revertChangeFactory.create(patchSetId, message).to(callback);
}
public void restoreChange(final PatchSet.Id patchSetId, final String message,
final AsyncCallback<ChangeDetail> callback) {
restoreChangeFactory.create(patchSetId, message).to(callback);

View File

@ -30,6 +30,7 @@ public class ChangeModule extends RpcServletModule {
protected void configure() {
factory(AbandonChange.Factory.class);
factory(RestoreChange.Factory.class);
factory(RevertChange.Factory.class);
factory(ChangeDetailFactory.Factory.class);
factory(IncludedInDetailFactory.Factory.class);
factory(PatchSetDetailFactory.Factory.class);

View File

@ -0,0 +1,114 @@
// Copyright (C) 2011 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.ChangeDetail;
import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.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.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.NoSuchChangeException;
import com.google.gwtorm.client.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;
import javax.annotation.Nullable;
class RevertChange extends Handler<ChangeDetail> {
interface Factory {
RevertChange create(PatchSet.Id patchSetId, String message);
}
private final ChangeControl.Factory changeControlFactory;
private final ReviewDb db;
private final IdentifiedUser currentUser;
private final RevertedSender.Factory revertedSenderFactory;
private final ChangeDetailFactory.Factory changeDetailFactory;
private final ReplicationQueue replication;
private final PatchSet.Id patchSetId;
@Nullable
private final String message;
private final ChangeHookRunner hooks;
private final GitRepositoryManager gitManager;
private final PatchSetInfoFactory patchSetInfoFactory;
private final PersonIdent myIdent;
@Inject
RevertChange(final ChangeControl.Factory changeControlFactory,
final ReviewDb db, final IdentifiedUser currentUser,
final RevertedSender.Factory revertedSenderFactory,
final ChangeDetailFactory.Factory changeDetailFactory,
@Assisted final PatchSet.Id patchSetId,
@Assisted @Nullable final String message, final ChangeHookRunner hooks,
final GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
final ReplicationQueue replication,
@GerritPersonIdent final PersonIdent myIdent) {
this.changeControlFactory = changeControlFactory;
this.db = db;
this.currentUser = currentUser;
this.revertedSenderFactory = revertedSenderFactory;
this.changeDetailFactory = changeDetailFactory;
this.patchSetId = patchSetId;
this.message = message;
this.hooks = hooks;
this.gitManager = gitManager;
this.patchSetInfoFactory = patchSetInfoFactory;
this.replication = replication;
this.myIdent = myIdent;
}
@Override
public ChangeDetail call() throws NoSuchChangeException, OrmException,
EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException,
MissingObjectException, IncorrectObjectTypeException, IOException {
final Change.Id changeId = patchSetId.getParentKey();
final ChangeControl control = changeControlFactory.validateFor(changeId);
if (!control.canAddPatchSet()) {
throw new NoSuchChangeException(changeId);
}
ChangeUtil.revert(patchSetId, currentUser, message, db,
revertedSenderFactory, hooks, gitManager, patchSetInfoFactory,
replication, myIdent);
return changeDetailFactory.create(changeId).call();
}
}

View File

@ -17,31 +17,48 @@ package com.google.gerrit.server;
import static com.google.gerrit.reviewdb.ApprovalCategory.SUBMIT;
import com.google.gerrit.common.ChangeHookRunner;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.ChangeMessage;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.PatchSetInfo;
import com.google.gerrit.reviewdb.RevId;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.TrackingId;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.TrackingFooter;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MergeQueue;
import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.mail.AbandonedSender;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.mail.RevertedSender;
import com.google.gwtorm.client.AtomicUpdate;
import com.google.gwtorm.client.OrmConcurrencyException;
import com.google.gwtorm.client.OrmException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
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.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.FooterLine;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.Base64;
import org.eclipse.jgit.util.NB;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@ -248,6 +265,108 @@ public class ChangeUtil {
hooks.doChangeAbandonedHook(updatedChange, user.getAccount(), message);
}
public static void revert(final PatchSet.Id patchSetId,
final IdentifiedUser user, final String message, final ReviewDb db,
final RevertedSender.Factory revertedSenderFactory,
final ChangeHookRunner hooks, GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
final ReplicationQueue replication, PersonIdent myIdent)
throws NoSuchChangeException, EmailException, OrmException,
MissingObjectException, IncorrectObjectTypeException, IOException,
PatchSetInfoNotAvailableException {
final Change.Id changeId = patchSetId.getParentKey();
final PatchSet patch = db.patchSets().get(patchSetId);
if (patch == null) {
throw new NoSuchChangeException(changeId);
}
final Repository git;
try {
git = gitManager.openRepository(db.changes().get(changeId).getProject());
} catch (RepositoryNotFoundException e) {
throw new NoSuchChangeException(changeId, e);
};
final RevWalk revWalk = new RevWalk(git);
try {
RevCommit commitToRevert =
revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
PersonIdent authorIdent =
user.newCommitterIdent(myIdent.getWhen(), myIdent.getTimeZone());
RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
revWalk.parseHeaders(parentToCommitToRevert);
CommitBuilder revertCommit = new CommitBuilder();
revertCommit.addParentId(commitToRevert);
revertCommit.setTreeId(parentToCommitToRevert.getTree());
revertCommit.setAuthor(authorIdent);
revertCommit.setCommitter(myIdent);
revertCommit.setMessage(message);
final ObjectInserter oi = git.newObjectInserter();;
ObjectId id;
try {
id = oi.insert(revertCommit);
oi.flush();
} finally {
oi.release();
}
Change.Key changeKey = new Change.Key("I" + id.name());
final Change change =
new Change(changeKey, new Change.Id(db.nextChangeId()),
user.getAccountId(), db.changes().get(changeId).getDest());
change.nextPatchSetId();
final PatchSet ps = new PatchSet(change.currPatchSetId());
ps.setCreatedOn(change.getCreatedOn());
ps.setUploader(user.getAccountId());
ps.setRevision(new RevId(id.getName()));
db.patchSets().insert(Collections.singleton(ps));
final PatchSetInfo info =
patchSetInfoFactory.get(revWalk.parseCommit(id), ps.getId());
change.setCurrentPatchSet(info);
ChangeUtil.updated(change);
db.changes().insert(Collections.singleton(change));
final RefUpdate ru = git.updateRef(ps.getRefName());
ru.setNewObjectId(id);
ru.disableRefLog();
if (ru.update(revWalk) != RefUpdate.Result.NEW) {
throw new IOException("Failed to create ref " + ps.getRefName()
+ " in " + git.getDirectory() + ": " + ru.getResult());
}
replication.scheduleUpdate(db.changes().get(changeId).getProject(),
ru.getName());
final ChangeMessage cmsg =
new ChangeMessage(new ChangeMessage.Key(changeId,
ChangeUtil.messageUUID(db)), user.getAccountId());
final StringBuilder msgBuf =
new StringBuilder("Patch Set " + patchSetId.get() + ": Reverted");
msgBuf.append("\n\n");
msgBuf.append("This patchset was reverted in change: " + changeKey.get());
cmsg.setMessage(msgBuf.toString());
db.changeMessages().insert(Collections.singleton(cmsg));
final RevertedSender cm = revertedSenderFactory.create(change);
cm.setFrom(user.getAccountId());
cm.setChangeMessage(cmsg);
cm.send();
hooks.doPatchsetCreatedHook(change, ps);
} finally {
revWalk.release();
git.close();
}
}
public static void restore(final PatchSet.Id patchSetId,
final IdentifiedUser user, final String message, final ReviewDb db,
final AbandonedSender.Factory abandonedSenderFactory,

View File

@ -32,6 +32,7 @@ import com.google.gerrit.server.mail.MergeFailSender;
import com.google.gerrit.server.mail.MergedSender;
import com.google.gerrit.server.mail.RegisterNewEmailSender;
import com.google.gerrit.server.mail.ReplacePatchSetSender;
import com.google.gerrit.server.mail.RevertedSender;
import com.google.gerrit.server.patch.PublishComments;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectControl;
@ -66,6 +67,7 @@ public class GerritRequestModule extends FactoryModule {
factory(PublishComments.Factory.class);
factory(ReplacePatchSetSender.Factory.class);
factory(AbandonedSender.Factory.class);
factory(RevertedSender.Factory.class);
factory(CommentSender.Factory.class);
factory(MergedSender.Factory.class);
factory(MergeFailSender.Factory.class);

View File

@ -0,0 +1,45 @@
// Copyright (C) 2011 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.Change;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
/** Send notice about a change being reverted. */
public class RevertedSender extends ReplyToChangeSender {
public static interface Factory {
RevertedSender create(Change change);
}
@Inject
public RevertedSender(EmailArguments ea, @Assisted Change c) {
super(ea, c, "revert");
}
@Override
protected void init() throws EmailException {
super.init();
ccAllApprovals();
bccStarredBy();
bccWatchesNotifyAllComments();
}
@Override
protected void formatChange() throws EmailException {
appendText(velocifyFile("Reverted.vm"));
}
}

View File

@ -0,0 +1,44 @@
## Copyright (C) 2010 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.exmaple file to a .vm
## file and edit it appropriately.
##
## This Template:
## --------------
## The Reverted.vm template will determine the contents of the email related
## to a change being reverted. It is a ChangeEmail: see ChangeSubject.vm and
## ChangeFooter.vm.
##
$fromName has reverted this change.
Change subject: $change.subject
......................................................................
#if ($coverLetter)
$coverLetter
#end