Merge "Move the 'isPatchSetLocked' check out from the ChangeControl"
diff --git a/java/com/google/gerrit/server/PatchSetUtil.java b/java/com/google/gerrit/server/PatchSetUtil.java
index 82ca8d2..e9ed51b 100644
--- a/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/java/com/google/gerrit/server/PatchSetUtil.java
@@ -26,15 +26,22 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 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.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.sql.Timestamp;
@@ -48,10 +55,20 @@
 @Singleton
 public class PatchSetUtil {
   private final NotesMigration migration;
+  private final Provider<ApprovalsUtil> approvalsUtilProvider;
+  private final ProjectCache projectCache;
+  private final Provider<ReviewDb> dbProvider;
 
   @Inject
-  PatchSetUtil(NotesMigration migration) {
+  PatchSetUtil(
+      NotesMigration migration,
+      Provider<ApprovalsUtil> approvalsUtilProvider,
+      ProjectCache projectCache,
+      Provider<ReviewDb> dbProvider) {
     this.migration = migration;
+    this.approvalsUtilProvider = approvalsUtilProvider;
+    this.projectCache = projectCache;
+    this.dbProvider = dbProvider;
   }
 
   public PatchSet current(ReviewDb db, ChangeNotes notes) throws OrmException {
@@ -155,4 +172,38 @@
     update.setGroups(groups);
     db.patchSets().update(Collections.singleton(ps));
   }
+
+  /** Check if the current patch set of the change is locked. */
+  public void checkPatchSetNotLocked(ChangeNotes notes, CurrentUser user)
+      throws OrmException, IOException, ResourceConflictException {
+    if (isPatchSetLocked(notes, user)) {
+      throw new ResourceConflictException(
+          String.format("The current patch set of change %s is locked", notes.getChangeId()));
+    }
+  }
+
+  /** Is the current patch set locked against state changes? */
+  public boolean isPatchSetLocked(ChangeNotes notes, CurrentUser user)
+      throws OrmException, IOException {
+    Change change = notes.getChange();
+    if (change.getStatus() == Change.Status.MERGED) {
+      return false;
+    }
+
+    ProjectState projectState = projectCache.checkedGet(notes.getProjectName());
+    checkNotNull(projectState, "Failed to load project %s", notes.getProjectName());
+
+    ApprovalsUtil approvalsUtil = approvalsUtilProvider.get();
+    for (PatchSetApproval ap :
+        approvalsUtil.byPatchSet(
+            dbProvider.get(), notes, user, change.currentPatchSetId(), null, null)) {
+      LabelType type = projectState.getLabelTypes(notes, user).byLabel(ap.getLabel());
+      if (type != null
+          && ap.getValue() == 1
+          && type.getFunction() == LabelFunction.PATCH_SET_LOCK) {
+        return true;
+      }
+    }
+    return false;
+  }
 }
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index 3a32f8f..d05d133 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -311,7 +311,11 @@
   }
 
   private void validate(RepoContext ctx)
-      throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
+      throws AuthException, ResourceConflictException, IOException, PermissionBackendException,
+          OrmException {
+    // Not allowed to create a new patch set if the current patch set is locked.
+    psUtil.checkPatchSetNotLocked(origNotes, ctx.getUser());
+
     if (checkAddPatchSetPermission) {
       permissionBackend
           .user(ctx.getUser())
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 83f0120..80d1cd1 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -394,7 +394,8 @@
   }
 
   private void assertCanEdit(ChangeNotes notes)
-      throws AuthException, PermissionBackendException, IOException, ResourceConflictException {
+      throws AuthException, PermissionBackendException, IOException, ResourceConflictException,
+          OrmException {
     if (!currentUser.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
@@ -406,6 +407,8 @@
               "change %s is %s", c.getChangeId(), c.getStatus().toString().toLowerCase()));
     }
 
+    // Not allowed to edit if the current patch set is locked.
+    patchSetUtil.checkPatchSetNotLocked(notes, currentUser.get());
     try {
       permissionBackend
           .currentUser()
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 5f65814..dbb6bcf 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -2452,6 +2452,13 @@
 
       RevCommit newCommit = rp.getRevWalk().parseCommit(newCommitId);
       RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
+
+      // Not allowed to create a new patch set if the current patch set is locked.
+      if (psUtil.isPatchSetLocked(notes, user)) {
+        reject(inputCommand, "cannot add patch set to " + ontoChange + ".");
+        return false;
+      }
+
       try {
         permissions.change(notes).database(db).check(ChangePermission.ADD_PATCH_SET);
       } catch (AuthException no) {
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 990c318..b13d921 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -21,19 +21,14 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.LabelFunction;
-import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -52,19 +47,11 @@
   static class Factory {
     private final ChangeData.Factory changeDataFactory;
     private final ChangeNotes.Factory notesFactory;
-    private final ApprovalsUtil approvalsUtil;
-    private final PatchSetUtil patchSetUtil;
 
     @Inject
-    Factory(
-        ChangeData.Factory changeDataFactory,
-        ChangeNotes.Factory notesFactory,
-        ApprovalsUtil approvalsUtil,
-        PatchSetUtil patchSetUtil) {
+    Factory(ChangeData.Factory changeDataFactory, ChangeNotes.Factory notesFactory) {
       this.changeDataFactory = changeDataFactory;
       this.notesFactory = notesFactory;
-      this.approvalsUtil = approvalsUtil;
-      this.patchSetUtil = patchSetUtil;
     }
 
     ChangeControl create(
@@ -74,27 +61,19 @@
     }
 
     ChangeControl create(RefControl refControl, ChangeNotes notes) {
-      return new ChangeControl(changeDataFactory, approvalsUtil, refControl, notes, patchSetUtil);
+      return new ChangeControl(changeDataFactory, refControl, notes);
     }
   }
 
   private final ChangeData.Factory changeDataFactory;
-  private final ApprovalsUtil approvalsUtil;
   private final RefControl refControl;
   private final ChangeNotes notes;
-  private final PatchSetUtil patchSetUtil;
 
   private ChangeControl(
-      ChangeData.Factory changeDataFactory,
-      ApprovalsUtil approvalsUtil,
-      RefControl refControl,
-      ChangeNotes notes,
-      PatchSetUtil patchSetUtil) {
+      ChangeData.Factory changeDataFactory, RefControl refControl, ChangeNotes notes) {
     this.changeDataFactory = changeDataFactory;
-    this.approvalsUtil = approvalsUtil;
     this.refControl = refControl;
     this.notes = notes;
-    this.patchSetUtil = patchSetUtil;
   }
 
   ForChange asForChange(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
@@ -105,8 +84,7 @@
     if (getUser().equals(who)) {
       return this;
     }
-    return new ChangeControl(
-        changeDataFactory, approvalsUtil, refControl.forUser(who), notes, patchSetUtil);
+    return new ChangeControl(changeDataFactory, refControl.forUser(who), notes);
   }
 
   private CurrentUser getUser() {
@@ -130,26 +108,24 @@
   }
 
   /** Can this user abandon this change? */
-  private boolean canAbandon(ReviewDb db) throws OrmException {
-    return (isOwner() // owner (aka creator) of the change can abandon
-            || refControl.isOwner() // branch owner can abandon
-            || getProjectControl().isOwner() // project owner can abandon
-            || refControl.canPerform(Permission.ABANDON) // user can abandon a specific ref
-            || getProjectControl().isAdmin())
-        && !isPatchSetLocked(db);
+  private boolean canAbandon() {
+    return isOwner() // owner (aka creator) of the change can abandon
+        || refControl.isOwner() // branch owner can abandon
+        || getProjectControl().isOwner() // project owner can abandon
+        || refControl.canPerform(Permission.ABANDON) // user can abandon a specific ref
+        || getProjectControl().isAdmin();
   }
 
   /** Can this user rebase this change? */
-  private boolean canRebase(ReviewDb db) throws OrmException {
+  private boolean canRebase() {
     return (isOwner() || refControl.canSubmit(isOwner()) || refControl.canRebase())
-        && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE)
-        && !isPatchSetLocked(db);
+        && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE);
   }
 
   /** Can this user restore this change? */
-  private boolean canRestore(ReviewDb db) throws OrmException {
+  private boolean canRestore() {
     // Anyone who can abandon the change can restore it, as long as they can create changes.
-    return canAbandon(db) && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE);
+    return canAbandon() && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE);
   }
 
   /** The range of permitted values associated with a label permission. */
@@ -158,8 +134,8 @@
   }
 
   /** Can this user add a patch set to this change? */
-  private boolean canAddPatchSet(ReviewDb db) throws OrmException {
-    if (!(refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE)) || isPatchSetLocked(db)) {
+  private boolean canAddPatchSet() {
+    if (!refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE)) {
       return false;
     }
     if (isOwner()) {
@@ -168,29 +144,6 @@
     return refControl.canAddPatchSet();
   }
 
-  /** Is the current patch set locked against state changes? */
-  private boolean isPatchSetLocked(ReviewDb db) throws OrmException {
-    if (getChange().getStatus() == Change.Status.MERGED) {
-      return false;
-    }
-
-    for (PatchSetApproval ap :
-        approvalsUtil.byPatchSet(
-            db, notes, getUser(), getChange().currentPatchSetId(), null, null)) {
-      LabelType type =
-          getProjectControl()
-              .getProjectState()
-              .getLabelTypes(notes, getUser())
-              .byLabel(ap.getLabel());
-      if (type != null
-          && ap.getValue() == 1
-          && type.getFunction() == LabelFunction.PATCH_SET_LOCK) {
-        return true;
-      }
-    }
-    return false;
-  }
-
   /** Is this user the owner of the change? */
   private boolean isOwner() {
     if (getUser().isIdentifiedUser()) {
@@ -355,12 +308,12 @@
           case READ:
             return isVisible(db(), changeData());
           case ABANDON:
-            return canAbandon(db());
+            return canAbandon();
           case DELETE:
             return (isOwner() && refControl.canPerform(Permission.DELETE_OWN_CHANGES))
                 || getProjectControl().isAdmin();
           case ADD_PATCH_SET:
-            return canAddPatchSet(db());
+            return canAddPatchSet();
           case EDIT_ASSIGNEE:
             return canEditAssignee();
           case EDIT_DESCRIPTION:
@@ -370,9 +323,9 @@
           case EDIT_TOPIC_NAME:
             return canEditTopicName();
           case REBASE:
-            return canRebase(db());
+            return canRebase();
           case RESTORE:
-            return canRestore(db());
+            return canRestore();
           case SUBMIT:
             return refControl.canSubmit(isOwner());
 
diff --git a/java/com/google/gerrit/server/permissions/ChangePermission.java b/java/com/google/gerrit/server/permissions/ChangePermission.java
index 6a23cdd..ba1785d 100644
--- a/java/com/google/gerrit/server/permissions/ChangePermission.java
+++ b/java/com/google/gerrit/server/permissions/ChangePermission.java
@@ -20,15 +20,39 @@
 
 public enum ChangePermission implements ChangePermissionOrLabel {
   READ,
+  /**
+   * The change can't be restored if its current patch set is locked.
+   *
+   * <p>Before checking this permission, the caller should first verify the current patch set of the
+   * change is not locked by calling {@code PatchSetUtil.isPatchSetLocked}.
+   */
   RESTORE,
   DELETE,
+  /**
+   * The change can't be abandoned if its current patch set is locked.
+   *
+   * <p>Before checking this permission, the caller should first verify the current patch set of the
+   * change is not locked by calling {@code PatchSetUtil.isPatchSetLocked}.
+   */
   ABANDON,
   EDIT_ASSIGNEE,
   EDIT_DESCRIPTION,
   EDIT_HASHTAGS,
   EDIT_TOPIC_NAME,
   REMOVE_REVIEWER,
+  /**
+   * A new patch set can't be added if the patch set is locked for the change.
+   *
+   * <p>Before checking this permission, the caller should first verify the current patch set of the
+   * change is not locked by calling {@code PatchSetUtil.isPatchSetLocked}.
+   */
   ADD_PATCH_SET,
+  /**
+   * The change can't be rebased if its current patch set is locked.
+   *
+   * <p>Before checking this permission, the caller should first verify the current patch set of the
+   * change is not locked by calling {@code PatchSetUtil.isPatchSetLocked}.
+   */
   REBASE,
   SUBMIT,
   SUBMIT_AS("submit on behalf of other users");
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java
index 5485cf6..116cbdb 100644
--- a/java/com/google/gerrit/server/restapi/change/Abandon.java
+++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
-
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.gerrit.common.TimeUtil;
@@ -29,6 +27,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.change.AbandonOp;
 import com.google.gerrit.server.change.ChangeJson;
@@ -47,14 +46,19 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Abandon extends RetryingRestModifyView<ChangeResource, AbandonInput, ChangeInfo>
     implements UiAction<ChangeResource> {
+  private static final Logger log = LoggerFactory.getLogger(Abandon.class);
+
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson.Factory json;
   private final AbandonOp.Factory abandonOpFactory;
   private final NotifyUtil notifyUtil;
+  private final PatchSetUtil patchSetUtil;
 
   @Inject
   Abandon(
@@ -62,27 +66,32 @@
       ChangeJson.Factory json,
       RetryHelper retryHelper,
       AbandonOp.Factory abandonOpFactory,
-      NotifyUtil notifyUtil) {
+      NotifyUtil notifyUtil,
+      PatchSetUtil patchSetUtil) {
     super(retryHelper);
     this.dbProvider = dbProvider;
     this.json = json;
     this.abandonOpFactory = abandonOpFactory;
     this.notifyUtil = notifyUtil;
+    this.patchSetUtil = patchSetUtil;
   }
 
   @Override
   protected ChangeInfo applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource req, AbandonInput input)
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, AbandonInput input)
       throws RestApiException, UpdateException, OrmException, PermissionBackendException,
           IOException, ConfigInvalidException {
-    req.permissions().database(dbProvider).check(ChangePermission.ABANDON);
+    // Not allowed to abandon if the current patch set is locked.
+    patchSetUtil.checkPatchSetNotLocked(rsrc.getNotes(), rsrc.getUser());
 
-    NotifyHandling notify = input.notify == null ? defaultNotify(req.getChange()) : input.notify;
+    rsrc.permissions().database(dbProvider).check(ChangePermission.ABANDON);
+
+    NotifyHandling notify = input.notify == null ? defaultNotify(rsrc.getChange()) : input.notify;
     Change change =
         abandon(
             updateFactory,
-            req.getNotes(),
-            req.getUser(),
+            rsrc.getNotes(),
+            rsrc.getUser(),
             input.message,
             notify,
             notifyUtil.resolveAccounts(input.notifyDetails));
@@ -135,13 +144,29 @@
 
   @Override
   public UiAction.Description getDescription(ChangeResource rsrc) {
+    UiAction.Description description =
+        new UiAction.Description()
+            .setLabel("Abandon")
+            .setTitle("Abandon the change")
+            .setVisible(false);
+
     Change change = rsrc.getChange();
-    return new UiAction.Description()
-        .setLabel("Abandon")
-        .setTitle("Abandon the change")
-        .setVisible(
-            and(
-                change.getStatus().isOpen(),
-                rsrc.permissions().database(dbProvider).testCond(ChangePermission.ABANDON)));
+    if (!change.getStatus().isOpen()) {
+      return description;
+    }
+
+    try {
+      if (patchSetUtil.isPatchSetLocked(rsrc.getNotes(), rsrc.getUser())) {
+        return description;
+      }
+    } catch (OrmException | IOException e) {
+      log.error(
+          String.format(
+              "Failed to check if the current patch set of change %s is locked", change.getId()),
+          e);
+      return description;
+    }
+
+    return description.setVisible(rsrc.permissions().testOrFalse(ChangePermission.ABANDON));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index 374aaec..ff85880 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -50,7 +50,6 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.restapi.project.CommitsCollection;
@@ -126,8 +125,11 @@
   @Override
   protected Response<ChangeInfo> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, MergePatchSetInput in)
-      throws OrmException, IOException, InvalidChangeOperationException, RestApiException,
-          UpdateException, PermissionBackendException {
+      throws OrmException, IOException, RestApiException, UpdateException,
+          PermissionBackendException {
+    // Not allowed to create a new patch set if the current patch set is locked.
+    psUtil.checkPatchSetNotLocked(rsrc.getNotes(), rsrc.getUser());
+
     rsrc.permissions().database(db).check(ChangePermission.ADD_PATCH_SET);
 
     ProjectState projectState = projectCache.checkedGet(rsrc.getProject());
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index 2ad954d..6e049438 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -134,6 +134,9 @@
       throw new ResourceConflictException("Change is already destined for the specified branch");
     }
 
+    // Not allowed to move if the current patch set is locked.
+    psUtil.checkPatchSetNotLocked(rsrc.getNotes(), rsrc.getUser());
+
     // Move requires abandoning this change, and creating a new change.
     try {
       rsrc.permissions().database(dbProvider).check(ABANDON);
@@ -279,24 +282,41 @@
 
   @Override
   public UiAction.Description getDescription(ChangeResource rsrc) {
+    UiAction.Description description =
+        new UiAction.Description()
+            .setLabel("Move Change")
+            .setTitle("Move change to a different branch")
+            .setVisible(false);
+
     Change change = rsrc.getChange();
-    boolean projectStatePermitsWrite = false;
+    if (!change.getStatus().isOpen()) {
+      return description;
+    }
+
     try {
-      projectStatePermitsWrite = projectCache.checkedGet(rsrc.getProject()).statePermitsWrite();
+      if (!projectCache.checkedGet(rsrc.getProject()).statePermitsWrite()) {
+        return description;
+      }
     } catch (IOException e) {
       log.error("Failed to check if project state permits write: " + rsrc.getProject(), e);
+      return description;
     }
-    return new UiAction.Description()
-        .setLabel("Move Change")
-        .setTitle("Move change to a different branch")
-        .setVisible(
-            and(
-                change.getStatus().isOpen() && projectStatePermitsWrite,
-                and(
-                    permissionBackend
-                        .user(rsrc.getUser())
-                        .ref(change.getDest())
-                        .testCond(CREATE_CHANGE),
-                    rsrc.permissions().database(dbProvider).testCond(ABANDON))));
+
+    try {
+      if (psUtil.isPatchSetLocked(rsrc.getNotes(), rsrc.getUser())) {
+        return description;
+      }
+    } catch (OrmException | IOException e) {
+      log.error(
+          String.format(
+              "Failed to check if the current patch set of change %s is locked", change.getId()),
+          e);
+      return description;
+    }
+
+    return description.setVisible(
+        and(
+            permissionBackend.user(rsrc.getUser()).ref(change.getDest()).testCond(CREATE_CHANGE),
+            rsrc.permissions().database(dbProvider).testCond(ABANDON)));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index c9c43cb..eb46521 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.NotifyUtil;
 import com.google.gerrit.server.change.PatchSetInserter;
-import com.google.gerrit.server.edit.UnchangedCommitMessageException;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -68,7 +67,7 @@
     extends RetryingRestModifyView<ChangeResource, CommitMessageInput, Response<?>> {
 
   private final GitRepositoryManager repositoryManager;
-  private final Provider<CurrentUser> currentUserProvider;
+  private final Provider<CurrentUser> userProvider;
   private final Provider<ReviewDb> db;
   private final TimeZone tz;
   private final PatchSetInserter.Factory psInserterFactory;
@@ -81,7 +80,7 @@
   PutMessage(
       RetryHelper retryHelper,
       GitRepositoryManager repositoryManager,
-      Provider<CurrentUser> currentUserProvider,
+      Provider<CurrentUser> userProvider,
       Provider<ReviewDb> db,
       PatchSetInserter.Factory psInserterFactory,
       PermissionBackend permissionBackend,
@@ -91,7 +90,7 @@
       ProjectCache projectCache) {
     super(retryHelper);
     this.repositoryManager = repositoryManager;
-    this.currentUserProvider = currentUserProvider;
+    this.userProvider = userProvider;
     this.db = db;
     this.psInserterFactory = psInserterFactory;
     this.tz = gerritIdent.getTimeZone();
@@ -104,8 +103,8 @@
   @Override
   protected Response<String> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource resource, CommitMessageInput input)
-      throws IOException, UnchangedCommitMessageException, RestApiException, UpdateException,
-          PermissionBackendException, OrmException, ConfigInvalidException {
+      throws IOException, RestApiException, UpdateException, PermissionBackendException,
+          OrmException, ConfigInvalidException {
     PatchSet ps = psUtil.current(db.get(), resource.getNotes());
     if (ps == null) {
       throw new ResourceConflictException("current revision is missing");
@@ -140,7 +139,7 @@
       Timestamp ts = TimeUtil.nowTs();
       try (BatchUpdate bu =
           updateFactory.create(
-              db.get(), resource.getChange().getProject(), currentUserProvider.get(), ts)) {
+              db.get(), resource.getChange().getProject(), userProvider.get(), ts)) {
         // Ensure that BatchUpdate will update the same repo
         bu.setRepository(repository, new RevWalk(objectInserter.newReader()), objectInserter);
 
@@ -170,8 +169,7 @@
     builder.setTreeId(basePatchSetCommit.getTree());
     builder.setParentIds(basePatchSetCommit.getParents());
     builder.setAuthor(basePatchSetCommit.getAuthorIdent());
-    builder.setCommitter(
-        currentUserProvider.get().asIdentifiedUser().newCommitterIdent(timestamp, tz));
+    builder.setCommitter(userProvider.get().asIdentifiedUser().newCommitterIdent(timestamp, tz));
     builder.setMessage(commitMessage);
     ObjectId newCommitId = objectInserter.insert(builder);
     objectInserter.flush();
@@ -179,13 +177,17 @@
   }
 
   private void ensureCanEditCommitMessage(ChangeNotes changeNotes)
-      throws AuthException, PermissionBackendException, IOException, ResourceConflictException {
-    if (!currentUserProvider.get().isIdentifiedUser()) {
+      throws AuthException, PermissionBackendException, IOException, ResourceConflictException,
+          OrmException {
+    if (!userProvider.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
+
+    // Not allowed to put message if the current patch set is locked.
+    psUtil.checkPatchSetNotLocked(changeNotes, userProvider.get());
     try {
       permissionBackend
-          .user(currentUserProvider.get())
+          .user(userProvider.get())
           .database(db.get())
           .change(changeNotes)
           .check(ChangePermission.ADD_PATCH_SET);
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 767ef7b..02e7c18 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -14,16 +14,12 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
-
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -81,6 +77,7 @@
   private final Provider<ReviewDb> dbProvider;
   private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
+  private final PatchSetUtil patchSetUtil;
 
   @Inject
   public Rebase(
@@ -91,7 +88,8 @@
       ChangeJson.Factory json,
       Provider<ReviewDb> dbProvider,
       PermissionBackend permissionBackend,
-      ProjectCache projectCache) {
+      ProjectCache projectCache,
+      PatchSetUtil patchSetUtil) {
     super(retryHelper);
     this.repoManager = repoManager;
     this.rebaseFactory = rebaseFactory;
@@ -100,13 +98,17 @@
     this.dbProvider = dbProvider;
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
+    this.patchSetUtil = patchSetUtil;
   }
 
   @Override
   protected ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource rsrc, RebaseInput input)
-      throws EmailException, OrmException, UpdateException, RestApiException, IOException,
-          NoSuchChangeException, PermissionBackendException {
+      throws OrmException, UpdateException, RestApiException, IOException,
+          PermissionBackendException {
+    // Not allowed to rebase if the current patch set is locked.
+    patchSetUtil.checkPatchSetNotLocked(rsrc.getNotes(), rsrc.getUser());
+
     rsrc.permissions().database(dbProvider).check(ChangePermission.REBASE);
     projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite();
 
@@ -203,40 +205,54 @@
   }
 
   @Override
-  public UiAction.Description getDescription(RevisionResource resource) {
-    PatchSet patchSet = resource.getPatchSet();
-    Change change = resource.getChange();
-    Branch.NameKey dest = change.getDest();
-    boolean visible = change.getStatus().isOpen() && resource.isCurrent();
-    boolean enabled = false;
+  public UiAction.Description getDescription(RevisionResource rsrc) {
+    UiAction.Description description =
+        new UiAction.Description()
+            .setLabel("Rebase")
+            .setTitle("Rebase onto tip of branch or parent change")
+            .setVisible(false);
+
+    Change change = rsrc.getChange();
+    if (!(change.getStatus().isOpen() && rsrc.isCurrent())) {
+      return description;
+    }
 
     try {
-      visible &= projectCache.checkedGet(resource.getProject()).statePermitsWrite();
-    } catch (IOException e) {
-      log.error("Failed to check if project state permits write: " + resource.getProject(), e);
-      visible = false;
-    }
-
-    if (visible) {
-      try (Repository repo = repoManager.openRepository(dest.getParentKey());
-          RevWalk rw = new RevWalk(repo)) {
-        visible = hasOneParent(rw, resource.getPatchSet());
-        if (visible) {
-          enabled = rebaseUtil.canRebase(patchSet, dest, repo, rw);
-        }
-      } catch (IOException e) {
-        log.error("Failed to check if patch set can be rebased: " + resource.getPatchSet(), e);
-        visible = false;
+      if (!projectCache.checkedGet(rsrc.getProject()).statePermitsWrite()) {
+        return description;
       }
+    } catch (IOException e) {
+      log.error("Failed to check if project state permits write: " + rsrc.getProject(), e);
+      return description;
     }
 
-    BooleanCondition permissionCond =
-        resource.permissions().database(dbProvider).testCond(ChangePermission.REBASE);
-    return new UiAction.Description()
-        .setLabel("Rebase")
-        .setTitle("Rebase onto tip of branch or parent change")
-        .setVisible(and(visible, permissionCond))
-        .setEnabled(and(enabled, permissionCond));
+    try {
+      if (patchSetUtil.isPatchSetLocked(rsrc.getNotes(), rsrc.getUser())) {
+        return description;
+      }
+    } catch (OrmException | IOException e) {
+      log.error(
+          String.format(
+              "Failed to check if the current patch set of change %s is locked", change.getId()),
+          e);
+      return description;
+    }
+
+    boolean enabled = false;
+    try (Repository repo = repoManager.openRepository(change.getDest().getParentKey());
+        RevWalk rw = new RevWalk(repo)) {
+      if (hasOneParent(rw, rsrc.getPatchSet())) {
+        enabled = rebaseUtil.canRebase(rsrc.getPatchSet(), change.getDest(), repo, rw);
+      }
+    } catch (IOException e) {
+      log.error("Failed to check if patch set can be rebased: " + rsrc.getPatchSet(), e);
+      return description;
+    }
+
+    if (rsrc.permissions().database(dbProvider).testOrFalse(ChangePermission.REBASE)) {
+      return description.setVisible(true).setEnabled(enabled);
+    }
+    return description;
   }
 
   public static class CurrentRevision
@@ -254,7 +270,7 @@
     @Override
     protected ChangeInfo applyImpl(
         BatchUpdate.Factory updateFactory, ChangeResource rsrc, RebaseInput input)
-        throws EmailException, OrmException, UpdateException, RestApiException, IOException,
+        throws OrmException, UpdateException, RestApiException, IOException,
             PermissionBackendException {
       PatchSet ps = psUtil.current(rebase.dbProvider.get(), rsrc.getNotes());
       if (ps == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index 642c35a..d5bfea1 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
-
 import com.google.common.base.Strings;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
@@ -90,17 +88,20 @@
 
   @Override
   protected ChangeInfo applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource req, RestoreInput input)
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, RestoreInput input)
       throws RestApiException, UpdateException, OrmException, PermissionBackendException,
           IOException {
-    req.permissions().database(dbProvider).check(ChangePermission.RESTORE);
-    projectCache.checkedGet(req.getProject()).checkStatePermitsWrite();
+    // Not allowed to restore if the current patch set is locked.
+    psUtil.checkPatchSetNotLocked(rsrc.getNotes(), rsrc.getUser());
+
+    rsrc.permissions().database(dbProvider).check(ChangePermission.RESTORE);
+    projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite();
 
     Op op = new Op(input);
     try (BatchUpdate u =
         updateFactory.create(
-            dbProvider.get(), req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) {
-      u.addOp(req.getId(), op).execute();
+            dbProvider.get(), rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+      u.addOp(rsrc.getId(), op).execute();
     }
     return json.noOptions().format(op.change);
   }
@@ -161,18 +162,39 @@
 
   @Override
   public UiAction.Description getDescription(ChangeResource rsrc) {
-    boolean projectStatePermitsWrite = false;
+    UiAction.Description description =
+        new UiAction.Description()
+            .setLabel("Restore")
+            .setTitle("Restore the change")
+            .setVisible(false);
+
+    Change change = rsrc.getChange();
+    if (change.getStatus() != Status.ABANDONED) {
+      return description;
+    }
+
     try {
-      projectStatePermitsWrite = projectCache.checkedGet(rsrc.getProject()).statePermitsWrite();
+      if (!projectCache.checkedGet(rsrc.getProject()).statePermitsWrite()) {
+        return description;
+      }
     } catch (IOException e) {
       log.error("Failed to check if project state permits write: " + rsrc.getProject(), e);
+      return description;
     }
-    return new UiAction.Description()
-        .setLabel("Restore")
-        .setTitle("Restore the change")
-        .setVisible(
-            and(
-                rsrc.getChange().getStatus() == Status.ABANDONED && projectStatePermitsWrite,
-                rsrc.permissions().database(dbProvider).testCond(ChangePermission.RESTORE)));
+
+    try {
+      if (psUtil.isPatchSetLocked(rsrc.getNotes(), rsrc.getUser())) {
+        return description;
+      }
+    } catch (OrmException | IOException e) {
+      log.error(
+          String.format(
+              "Failed to check if the current patch set of change %s is locked", change.getId()),
+          e);
+      return description;
+    }
+
+    boolean visible = rsrc.permissions().database(dbProvider).testOrFalse(ChangePermission.RESTORE);
+    return description.setVisible(visible);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
index 93ad2fe..be0879a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -229,8 +229,9 @@
     grant(project, "refs/heads/*", Permission.LABEL + "Patch-Set-Lock");
     revision(r).review(new ReviewInput().label("Patch-Set-Lock", 1));
 
-    exception.expect(AuthException.class);
-    exception.expectMessage("move not permitted");
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage(
+        String.format("The current patch set of change %s is locked", r.getChange().getId()));
     move(r.getChangeId(), newBranch.get());
   }