Implement 'Restore Change' feature.
Now it is possible to restore status of abandoned changes to 'NEW'. It helps in a situation when a change has been abandoned accidently. Bug: issue 312 Change-Id: Iba61dcf82a9b5ee5b78cd529b041f92a042a9611
This commit is contained in:
parent
91c1532130
commit
3200245774
|
@ -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"
|
||||
|
|
|
@ -57,6 +57,15 @@ Called whenever a change has been abandoned.
|
|||
change-abandoned --change <change id> --change-url <change url> --project <project name> --branch <branch> --abandoner <abandoner> --reason <reason>
|
||||
====
|
||||
|
||||
change-restored
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Called whenever a change has been restored.
|
||||
|
||||
====
|
||||
change-restored --change <change id> --change-url <change url> --project <project name> --branch <branch> --restorer <restorer> --reason <reason>
|
||||
====
|
||||
|
||||
|
||||
Configuration Settings
|
||||
----------------------
|
||||
|
|
|
@ -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<ChangeInfo> 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;
|
||||
}
|
||||
|
|
|
@ -29,4 +29,8 @@ public interface ChangeManageService extends RemoteJsonService {
|
|||
@SignInRequired
|
||||
void abandonChange(PatchSet.Id patchSetId, String message,
|
||||
AsyncCallback<ChangeDetail> callback);
|
||||
|
||||
@SignInRequired
|
||||
void restoreChange(PatchSet.Id patchSetId, String message,
|
||||
AsyncCallback<ChangeDetail> callback);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<ChangeDetail>() {
|
||||
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) {
|
||||
|
|
|
@ -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<PopupPanel>{
|
||||
private final FlowPanel panel;
|
||||
private final NpTextArea message;
|
||||
private final Button sendButton;
|
||||
private final Button cancelButton;
|
||||
private final PatchSet.Id psid;
|
||||
private final AsyncCallback<ChangeDetail> callback;
|
||||
|
||||
private boolean buttonClicked = false;
|
||||
|
||||
public RestoreChangeDialog(final PatchSet.Id psi,
|
||||
final AsyncCallback<ChangeDetail> 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<ChangeDetail>() {
|
||||
@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<PopupPanel> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -101,6 +101,7 @@ public class ChangeDetailFactory extends Handler<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));
|
||||
loadPatchSets();
|
||||
|
|
|
@ -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<ChangeDetail> callback) {
|
||||
abandonChangeFactory.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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<ChangeDetail> {
|
||||
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<Change>() {
|
||||
@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<PatchSetApproval> 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();
|
||||
}
|
||||
}
|
|
@ -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<String> args = new ArrayList<String>();
|
||||
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)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue