diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt index 0b536fc476..aae908a2f7 100644 --- a/Documentation/cmd-stream-events.txt +++ b/Documentation/cmd-stream-events.txt @@ -70,6 +70,16 @@ patchset:: link:json.html#patchset[patchset attribute] abandoner:: link:json.html#account[account attribute] +Change Restored +^^^^^^^^^^^^^^^^ +type:: "change-restored" + +change:: link:json.html#change[change attribute] + +patchset:: link:json.html#patchset[patchset attribute] + +restorer:: link:json.html#account[account attribute] + Change Merged ^^^^^^^^^^^^^ type:: "change-merged" diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt index 4dd7531c51..95fc7bf0bb 100644 --- a/Documentation/config-hooks.txt +++ b/Documentation/config-hooks.txt @@ -57,6 +57,15 @@ Called whenever a change has been abandoned. change-abandoned --change --change-url --project --branch --abandoner --reason ==== +change-restored +~~~~~~~~~~~~~~~~ + +Called whenever a change has been restored. + +==== + change-restored --change --change-url --project --branch --restorer --reason +==== + Configuration Settings ---------------------- 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 80c1c31f28..cd992fdfe8 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 @@ -30,6 +30,7 @@ public class ChangeDetail { protected AccountInfoCache accounts; protected boolean allowsAnonymous; protected boolean canAbandon; + protected boolean canRestore; protected Change change; protected boolean starred; protected List dependsOn; @@ -69,6 +70,14 @@ public class ChangeDetail { canAbandon = a; } + public boolean canRestore() { + return canRestore; + } + + public void setCanRestore(final boolean a) { + canRestore = a; + } + public Change getChange() { return change; } 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 53946f2230..b61c35c1e4 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 @@ -29,4 +29,8 @@ public interface ChangeManageService extends RemoteJsonService { @SignInRequired void abandonChange(PatchSet.Id patchSetId, String message, AsyncCallback callback); + + @SignInRequired + void restoreChange(PatchSet.Id patchSetId, String message, + 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 9a08932687..d5a480f789 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,12 @@ public interface ChangeConstants extends Constants { String headingCoverMessage(); String headingPatchComments(); + String buttonRestoreChangeBegin(); + String restoreChangeTitle(); + String buttonRestoreChangeCancel(); + String headingRestoreMessage(); + String buttonRestoreChangeSend(); + String pagedChangeListPrev(); String pagedChangeListNext(); 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 59d0bd52c1..e93338d5fa 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 @@ -81,6 +81,12 @@ buttonAbandonChangeCancel = Cancel headingAbandonMessage = Abandon Message: abandonChangeTitle = Code Review - Abandon Change +buttonRestoreChangeBegin = Restore Change +restoreChangeTitle = Code Review - Restore Change +buttonRestoreChangeCancel = Cancel +headingRestoreMessage = Restore Message: +buttonRestoreChangeSend = Restore Change + buttonReview = Review buttonPublishCommentsSend = Publish Comments buttonPublishSubmitSend = Publish and Submit 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 ce0cb6ddc3..620fe969c8 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 @@ -418,6 +418,26 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel implements O }); actionsPanel.add(b); } + + if (changeDetail.canRestore()) { + final Button b = new Button(Util.C.buttonRestoreChangeBegin()); + b.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + new RestoreChangeDialog(patchSet.getId(), + new AsyncCallback() { + public void onSuccess(ChangeDetail result) { + changeScreen.display(result); + } + + public void onFailure(Throwable caught) { + b.setEnabled(true); + } + }).center(); + } + }); + actionsPanel.add(b); + } } private void populateDiffAllActions(final PatchSetDetail detail) { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RestoreChangeDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RestoreChangeDialog.java new file mode 100644 index 0000000000..e05c42afc6 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RestoreChangeDialog.java @@ -0,0 +1,133 @@ +// Copyright (C) 2009 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.client.changes; + +import com.google.gerrit.client.Gerrit; +import com.google.gerrit.client.rpc.GerritCallback; +import com.google.gerrit.client.ui.SmallHeading; +import com.google.gerrit.common.data.ChangeDetail; +import com.google.gerrit.reviewdb.PatchSet; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.logical.shared.CloseEvent; +import com.google.gwt.event.logical.shared.CloseHandler; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwtexpui.globalkey.client.GlobalKey; +import com.google.gwtexpui.globalkey.client.NpTextArea; +import com.google.gwtexpui.user.client.AutoCenterDialogBox; + +public class RestoreChangeDialog extends AutoCenterDialogBox implements CloseHandler{ + private final FlowPanel panel; + private final NpTextArea message; + private final Button sendButton; + private final Button cancelButton; + private final PatchSet.Id psid; + private final AsyncCallback callback; + + private boolean buttonClicked = false; + + public RestoreChangeDialog(final PatchSet.Id psi, + final AsyncCallback callback) { + super(/* auto hide */false, /* modal */true); + setGlassEnabled(true); + + psid = psi; + this.callback = callback; + addStyleName(Gerrit.RESOURCES.css().abandonChangeDialog()); + setText(Util.C.restoreChangeTitle()); + + panel = new FlowPanel(); + add(panel); + + panel.add(new SmallHeading(Util.C.headingRestoreMessage())); + + final FlowPanel mwrap = new FlowPanel(); + mwrap.setStyleName(Gerrit.RESOURCES.css().abandonMessage()); + panel.add(mwrap); + + message = new NpTextArea(); + message.setCharacterWidth(60); + message.setVisibleLines(10); + DOM.setElementPropertyBoolean(message.getElement(), "spellcheck", true); + mwrap.add(message); + + final FlowPanel buttonPanel = new FlowPanel(); + panel.add(buttonPanel); + + sendButton = new Button(Util.C.buttonRestoreChangeSend()); + sendButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + sendButton.setEnabled(false); + Util.MANAGE_SVC.restoreChange(psid, message.getText().trim(), + new GerritCallback() { + @Override + public void onSuccess(ChangeDetail result) { + buttonClicked = true; + if (callback != null) { + callback.onSuccess(result); + } + hide(); + } + + @Override + public void onFailure(Throwable caught) { + sendButton.setEnabled(true); + super.onFailure(caught); + } + }); + } + }); + buttonPanel.add(sendButton); + + cancelButton = new Button(Util.C.buttonRestoreChangeCancel()); + DOM.setStyleAttribute(cancelButton.getElement(), "marginLeft", "300px"); + cancelButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + buttonClicked = true; + if (callback != null) { + callback.onFailure(null); + } + hide(); + } + }); + buttonPanel.add(cancelButton); + + addCloseHandler(this); + } + + @Override + public void center() { + super.center(); + GlobalKey.dialog(this); + message.setFocus(true); + } + + @Override + public void onClose(CloseEvent event) { + if (!buttonClicked) { + // the dialog was closed without one of the buttons being pressed + // e.g. the user pressed ESC to close the dialog + if (callback != null) { + callback.onFailure(null); + } + } + } +} 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 bcea11d992..954f0a233b 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 @@ -101,6 +101,7 @@ public class ChangeDetailFactory extends Handler { 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)); loadPatchSets(); 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 196d400de6..0eccc2c3fd 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 @@ -23,12 +23,15 @@ import com.google.inject.Inject; class ChangeManageServiceImpl implements ChangeManageService { private final SubmitAction.Factory submitAction; private final AbandonChange.Factory abandonChangeFactory; + private final RestoreChange.Factory restoreChangeFactory; @Inject ChangeManageServiceImpl(final SubmitAction.Factory patchSetAction, - final AbandonChange.Factory abandonChangeFactory) { + final AbandonChange.Factory abandonChangeFactory, + final RestoreChange.Factory restoreChangeFactory) { this.submitAction = patchSetAction; this.abandonChangeFactory = abandonChangeFactory; + this.restoreChangeFactory = restoreChangeFactory; } public void submit(final PatchSet.Id patchSetId, @@ -40,4 +43,9 @@ class ChangeManageServiceImpl implements ChangeManageService { final AsyncCallback callback) { abandonChangeFactory.create(patchSetId, message).to(callback); } + + public void restoreChange(final PatchSet.Id patchSetId, final String message, + final AsyncCallback callback) { + restoreChangeFactory.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 8605de398c..85e2aef018 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 @@ -29,6 +29,7 @@ public class ChangeModule extends RpcServletModule { @Override protected void configure() { factory(AbandonChange.Factory.class); + factory(RestoreChange.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/RestoreChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChange.java new file mode 100644 index 0000000000..9639f1a2bb --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChange.java @@ -0,0 +1,132 @@ +// Copyright (C) 2009 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.*; +import com.google.gerrit.server.ChangeUtil; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.mail.AbandonedSender; +import com.google.gerrit.server.mail.EmailException; +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.AtomicUpdate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; + +class RestoreChange extends Handler { + interface Factory { + RestoreChange create(PatchSet.Id patchSetId, String message); + } + + private final ChangeControl.Factory changeControlFactory; + private final ReviewDb db; + private final IdentifiedUser currentUser; + private final AbandonedSender.Factory abandonedSenderFactory; + private final ChangeDetailFactory.Factory changeDetailFactory; + + private final PatchSet.Id patchSetId; + @Nullable + private final String message; + + private final ChangeHookRunner hooks; + + @Inject + RestoreChange(final ChangeControl.Factory changeControlFactory, + final ReviewDb db, final IdentifiedUser currentUser, + final AbandonedSender.Factory abandonedSenderFactory, + final ChangeDetailFactory.Factory changeDetailFactory, + @Assisted final PatchSet.Id patchSetId, + @Assisted @Nullable final String message, final ChangeHookRunner hooks) { + this.changeControlFactory = changeControlFactory; + this.db = db; + this.currentUser = currentUser; + this.abandonedSenderFactory = abandonedSenderFactory; + this.changeDetailFactory = changeDetailFactory; + + this.patchSetId = patchSetId; + this.message = message; + this.hooks = hooks; + } + + @Override + public ChangeDetail call() throws NoSuchChangeException, OrmException, + EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException { + final Change.Id changeId = patchSetId.getParentKey(); + final ChangeControl control = changeControlFactory.validateFor(changeId); + if (!control.canRestore()) { + throw new NoSuchChangeException(changeId); + } + final PatchSet patch = db.patchSets().get(patchSetId); + if (patch == null) { + throw new NoSuchChangeException(changeId); + } + + final ChangeMessage cmsg = + new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil + .messageUUID(db)), currentUser.getAccountId()); + final StringBuilder msgBuf = + new StringBuilder("Patch Set " + patchSetId.get() + ": Restored"); + if (message != null && message.length() > 0) { + msgBuf.append("\n\n"); + msgBuf.append(message); + } + cmsg.setMessage(msgBuf.toString()); + + Change change = db.changes().atomicUpdate(changeId, new AtomicUpdate() { + @Override + public Change update(Change change) { + if (change.getStatus() == Change.Status.ABANDONED + && change.currentPatchSetId().equals(patchSetId)) { + change.setStatus(Change.Status.NEW); + ChangeUtil.updated(change); + return change; + } else { + return null; + } + } + }); + + if (change != null) { + db.changeMessages().insert(Collections.singleton(cmsg)); + + final List approvals = + db.patchSetApprovals().byChange(changeId).toList(); + for (PatchSetApproval a : approvals) { + a.cache(change); + } + db.patchSetApprovals().update(approvals); + + // Email the reviewers + final AbandonedSender cm = abandonedSenderFactory.create(change); + cm.setFrom(currentUser.getAccountId()); + cm.setChangeMessage(cmsg); + cm.send(); + } + + hooks.doChangeRestoreHook(change, currentUser.getAccount(), message); + + return changeDetailFactory.create(changeId).call(); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java index f2d0ad08c5..0ba85d11e5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java +++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java @@ -30,6 +30,7 @@ import com.google.gerrit.server.events.ApprovalAttribute; import com.google.gerrit.server.events.ChangeAbandonedEvent; import com.google.gerrit.server.events.ChangeEvent; import com.google.gerrit.server.events.ChangeMergedEvent; +import com.google.gerrit.server.events.ChangeRestoreEvent; import com.google.gerrit.server.events.CommentAddedEvent; import com.google.gerrit.server.events.EventFactory; import com.google.gerrit.server.events.PatchSetCreatedEvent; @@ -40,7 +41,6 @@ import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectState; import com.google.inject.Inject; import com.google.inject.Singleton; - import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Repository; import org.slf4j.Logger; @@ -90,6 +90,9 @@ public class ChangeHookRunner { /** Filename of the change abandoned hook. */ private final File changeAbandonedHook; + /** Filename of the change abandoned hook. */ + private final File changeRestoredHook; + /** Repository Manager. */ private final GitRepositoryManager repoManager; @@ -134,6 +137,7 @@ public class ChangeHookRunner { commentAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "commentAddedHook", "comment-added")).getPath()); changeMergedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeMergedHook", "change-merged")).getPath()); changeAbandonedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeAbandonedHook", "change-abandoned")).getPath()); + changeRestoredHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeRestoredHook", "change-restored")).getPath()); } public void addChangeListener(ChangeListener listener, IdentifiedUser user) { @@ -328,6 +332,40 @@ public class ChangeHookRunner { runHook(getRepo(change), args); } + /** + * Fire the Change Restored Hook. + * + * @param change The change itself. + * @param account The gerrit user who restored the change. + * @param reason Reason for restoring the change. + */ + public void doChangeRestoreHook(final Change change, final Account account, final String reason) { + final ChangeRestoreEvent event = new ChangeRestoreEvent(); + + event.change = eventFactory.asChangeAttribute(change); + event.restorer = eventFactory.asAccountAttribute(account); + event.reason = reason; + fireEvent(change, event); + + final List args = new ArrayList(); + args.add(changeRestoredHook.getAbsolutePath()); + + args.add("--change"); + args.add(event.change.id); + args.add("--change-url"); + args.add(event.change.url); + args.add("--project"); + args.add(event.change.project); + args.add("--branch"); + args.add(event.change.branch); + args.add("--restorer"); + args.add(getDisplayName(account)); + args.add("--reason"); + args.add(reason == null ? "" : reason); + + runHook(getRepo(change), args); + } + private void fireEvent(final Change change, final ChangeEvent event) { for (ChangeListenerHolder holder : listeners.values()) { if (isVisibleTo(change, holder.user)) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java new file mode 100644 index 0000000000..1a2922bb33 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java @@ -0,0 +1,23 @@ +// 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. + +package com.google.gerrit.server.events; + +public class ChangeRestoreEvent extends ChangeEvent { + public final String type = "change-restored"; + public ChangeAttribute change; + public PatchSetAttribute patchSet; + public AccountAttribute restorer; + public String reason; +} 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 c9435d3151..3291f1ac3b 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 @@ -159,6 +159,11 @@ public class ChangeControl { ; } + /** Can this user restore this change? */ + public boolean canRestore() { + return canAbandon(); // Anyone who can abandon the change can restore it back + } + public short normalize(ApprovalCategory.Id category, short score) { return getRefControl().normalize(category, score); }