Allow deletion of open and abandoned changes
If a user includes some sensitive information in a change, admins need to be able to delete it. Previously, only draft changes could be deleted. Deleting merged changes isn't supported because we would need to rewrite history. Feature: Issue 4509 Change-Id: I6a5bf055257b3762271c61fdd82a349e24148be3
This commit is contained in:
		@@ -1767,13 +1767,19 @@ Publishes a draft change.
 | 
			
		||||
  HTTP/1.1 204 No Content
 | 
			
		||||
----
 | 
			
		||||
 | 
			
		||||
[[delete-draft-change]]
 | 
			
		||||
=== Delete Draft Change
 | 
			
		||||
[[delete-change]]
 | 
			
		||||
=== Delete Change
 | 
			
		||||
--
 | 
			
		||||
'DELETE /changes/link:#change-id[\{change-id\}]'
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
Deletes a draft change.
 | 
			
		||||
Deletes a change.
 | 
			
		||||
 | 
			
		||||
New or abandoned changes can only be deleted by administrators. The deletion of
 | 
			
		||||
merged changes isn't supported at the moment. Draft changes can only be deleted
 | 
			
		||||
by their owner or other users who have the permissions to view and delete
 | 
			
		||||
drafts. If the draft workflow is disabled, only administrators with those
 | 
			
		||||
permissions may delete draft changes.
 | 
			
		||||
 | 
			
		||||
.Request
 | 
			
		||||
----
 | 
			
		||||
 
 | 
			
		||||
@@ -68,6 +68,7 @@ import com.google.gerrit.extensions.common.MergeInput;
 | 
			
		||||
import com.google.gerrit.extensions.common.MergePatchSetInput;
 | 
			
		||||
import com.google.gerrit.extensions.common.RevisionInfo;
 | 
			
		||||
import com.google.gerrit.extensions.restapi.AuthException;
 | 
			
		||||
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 | 
			
		||||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
 | 
			
		||||
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 | 
			
		||||
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 | 
			
		||||
@@ -398,7 +399,7 @@ public class ChangeIT extends AbstractDaemonTest {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void delete() throws Exception {
 | 
			
		||||
  public void deleteDraftChange() throws Exception {
 | 
			
		||||
    PushOneCommit.Result r = createChange("refs/drafts/master");
 | 
			
		||||
    assertThat(query(r.getChangeId())).hasSize(1);
 | 
			
		||||
    assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.DRAFT);
 | 
			
		||||
@@ -408,6 +409,110 @@ public class ChangeIT extends AbstractDaemonTest {
 | 
			
		||||
    assertThat(query(r.getChangeId())).isEmpty();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void deleteNewChangeAsAdmin() throws Exception {
 | 
			
		||||
    PushOneCommit.Result changeResult = createChange();
 | 
			
		||||
    String changeId = changeResult.getChangeId();
 | 
			
		||||
 | 
			
		||||
    gApi.changes()
 | 
			
		||||
        .id(changeId)
 | 
			
		||||
        .delete();
 | 
			
		||||
 | 
			
		||||
    assertThat(query(changeId)).isEmpty();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestProjectInput(cloneAs = "user")
 | 
			
		||||
  public void deleteNewChangeAsNormalUser() throws Exception {
 | 
			
		||||
    PushOneCommit.Result changeResult =
 | 
			
		||||
        pushFactory.create(db, user.getIdent(), testRepo)
 | 
			
		||||
            .to("refs/for/master");
 | 
			
		||||
    String changeId = changeResult.getChangeId();
 | 
			
		||||
    Change.Id id = changeResult.getChange().getId();
 | 
			
		||||
 | 
			
		||||
    setApiUser(user);
 | 
			
		||||
    exception.expect(AuthException.class);
 | 
			
		||||
    exception.expectMessage(String.format(
 | 
			
		||||
        "Deleting change %s is not permitted", id));
 | 
			
		||||
    gApi.changes()
 | 
			
		||||
        .id(changeId)
 | 
			
		||||
        .delete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestProjectInput(cloneAs = "user")
 | 
			
		||||
  public void deleteNewChangeOfAnotherUserAsAdmin() throws Exception {
 | 
			
		||||
    PushOneCommit.Result changeResult =
 | 
			
		||||
        pushFactory.create(db, user.getIdent(), testRepo)
 | 
			
		||||
            .to("refs/for/master");
 | 
			
		||||
    changeResult.assertOkStatus();
 | 
			
		||||
    String changeId = changeResult.getChangeId();
 | 
			
		||||
 | 
			
		||||
    setApiUser(admin);
 | 
			
		||||
    gApi.changes()
 | 
			
		||||
        .id(changeId)
 | 
			
		||||
        .delete();
 | 
			
		||||
 | 
			
		||||
    assertThat(query(changeId)).isEmpty();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestProjectInput(cloneAs = "user")
 | 
			
		||||
  public void deleteAbandonedChangeAsNormalUser() throws Exception {
 | 
			
		||||
    PushOneCommit.Result changeResult =
 | 
			
		||||
        pushFactory.create(db, user.getIdent(), testRepo)
 | 
			
		||||
        .to("refs/for/master");
 | 
			
		||||
    String changeId = changeResult.getChangeId();
 | 
			
		||||
    Change.Id id = changeResult.getChange().getId();
 | 
			
		||||
 | 
			
		||||
    setApiUser(user);
 | 
			
		||||
    gApi.changes()
 | 
			
		||||
        .id(changeId)
 | 
			
		||||
        .abandon();
 | 
			
		||||
 | 
			
		||||
    exception.expect(AuthException.class);
 | 
			
		||||
    exception.expectMessage(String.format(
 | 
			
		||||
        "Deleting change %s is not permitted", id));
 | 
			
		||||
    gApi.changes()
 | 
			
		||||
        .id(changeId)
 | 
			
		||||
        .delete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestProjectInput(cloneAs = "user")
 | 
			
		||||
  public void deleteAbandonedChangeOfAnotherUserAsAdmin() throws Exception {
 | 
			
		||||
    PushOneCommit.Result changeResult =
 | 
			
		||||
        pushFactory.create(db, user.getIdent(), testRepo)
 | 
			
		||||
        .to("refs/for/master");
 | 
			
		||||
    String changeId = changeResult.getChangeId();
 | 
			
		||||
 | 
			
		||||
    gApi.changes()
 | 
			
		||||
        .id(changeId)
 | 
			
		||||
        .abandon();
 | 
			
		||||
 | 
			
		||||
    gApi.changes()
 | 
			
		||||
        .id(changeId)
 | 
			
		||||
        .delete();
 | 
			
		||||
 | 
			
		||||
    assertThat(query(changeId)).isEmpty();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void deleteMergedChange() throws Exception {
 | 
			
		||||
    PushOneCommit.Result changeResult = createChange();
 | 
			
		||||
    String changeId = changeResult.getChangeId();
 | 
			
		||||
    Change.Id id = changeResult.getChange().getId();
 | 
			
		||||
 | 
			
		||||
    merge(changeResult);
 | 
			
		||||
 | 
			
		||||
    exception.expect(MethodNotAllowedException.class);
 | 
			
		||||
    exception.expectMessage(String.format(
 | 
			
		||||
        "Deleting merged change %s is not allowed", id));
 | 
			
		||||
    gApi.changes()
 | 
			
		||||
        .id(changeId)
 | 
			
		||||
        .delete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void rebaseUpToDateChange() throws Exception {
 | 
			
		||||
    PushOneCommit.Result r = createChange();
 | 
			
		||||
 
 | 
			
		||||
@@ -22,20 +22,38 @@ import com.google.gerrit.acceptance.AbstractDaemonTest;
 | 
			
		||||
import com.google.gerrit.acceptance.PushOneCommit;
 | 
			
		||||
import com.google.gerrit.acceptance.RestResponse;
 | 
			
		||||
import com.google.gerrit.acceptance.RestSession;
 | 
			
		||||
import com.google.gerrit.acceptance.TestProjectInput;
 | 
			
		||||
import com.google.gerrit.common.TimeUtil;
 | 
			
		||||
import com.google.gerrit.common.data.Permission;
 | 
			
		||||
import com.google.gerrit.extensions.api.changes.ReviewInput;
 | 
			
		||||
import com.google.gerrit.extensions.client.ChangeStatus;
 | 
			
		||||
import com.google.gerrit.extensions.client.ListChangesOption;
 | 
			
		||||
import com.google.gerrit.extensions.client.ReviewerState;
 | 
			
		||||
import com.google.gerrit.extensions.common.AccountInfo;
 | 
			
		||||
import com.google.gerrit.extensions.common.ChangeInfo;
 | 
			
		||||
import com.google.gerrit.extensions.common.LabelInfo;
 | 
			
		||||
import com.google.gerrit.extensions.common.RevisionInfo;
 | 
			
		||||
import com.google.gerrit.extensions.restapi.AuthException;
 | 
			
		||||
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 | 
			
		||||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
 | 
			
		||||
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 | 
			
		||||
import com.google.gerrit.extensions.restapi.RestApiException;
 | 
			
		||||
import com.google.gerrit.reviewdb.client.Change;
 | 
			
		||||
import com.google.gerrit.reviewdb.client.PatchSet;
 | 
			
		||||
import com.google.gerrit.server.git.BatchUpdate;
 | 
			
		||||
import com.google.gerrit.server.git.UpdateException;
 | 
			
		||||
import com.google.gerrit.server.notedb.PatchSetState;
 | 
			
		||||
import com.google.gerrit.testutil.ConfigSuite;
 | 
			
		||||
import com.google.gwtorm.server.OrmException;
 | 
			
		||||
import com.google.inject.Inject;
 | 
			
		||||
 | 
			
		||||
import org.eclipse.jgit.lib.Config;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.EnumSet;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
public class DraftChangeIT extends AbstractDaemonTest {
 | 
			
		||||
  @ConfigSuite.Config
 | 
			
		||||
@@ -43,20 +61,8 @@ public class DraftChangeIT extends AbstractDaemonTest {
 | 
			
		||||
    return allowDraftsDisabledConfig();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void deleteChange() throws Exception {
 | 
			
		||||
    PushOneCommit.Result result = createChange();
 | 
			
		||||
    result.assertOkStatus();
 | 
			
		||||
    String changeId = result.getChangeId();
 | 
			
		||||
    String triplet = project.get() + "~master~" + changeId;
 | 
			
		||||
    ChangeInfo c = get(triplet);
 | 
			
		||||
    assertThat(c.id).isEqualTo(triplet);
 | 
			
		||||
    assertThat(c.status).isEqualTo(ChangeStatus.NEW);
 | 
			
		||||
    RestResponse response = deleteChange(changeId, adminRestSession);
 | 
			
		||||
    assertThat(response.getEntityContent())
 | 
			
		||||
        .isEqualTo("Change is not a draft: " + c._number);
 | 
			
		||||
    response.assertConflict();
 | 
			
		||||
  }
 | 
			
		||||
  @Inject
 | 
			
		||||
  private BatchUpdate.Factory updateFactory;
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void deleteDraftChange() throws Exception {
 | 
			
		||||
@@ -74,6 +80,104 @@ public class DraftChangeIT extends AbstractDaemonTest {
 | 
			
		||||
    get(triplet);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void deleteDraftChangeOfAnotherUser() throws Exception {
 | 
			
		||||
    assume().that(isAllowDrafts()).isTrue();
 | 
			
		||||
    PushOneCommit.Result changeResult = createDraftChange();
 | 
			
		||||
    changeResult.assertOkStatus();
 | 
			
		||||
    String changeId = changeResult.getChangeId();
 | 
			
		||||
    Change.Id id = changeResult.getChange().getId();
 | 
			
		||||
 | 
			
		||||
    // The user needs to be able to see the draft change (which reviewers can).
 | 
			
		||||
    gApi.changes()
 | 
			
		||||
        .id(changeId)
 | 
			
		||||
        .addReviewer(user.fullName);
 | 
			
		||||
 | 
			
		||||
    setApiUser(user);
 | 
			
		||||
    exception.expect(AuthException.class);
 | 
			
		||||
    exception.expectMessage(String.format(
 | 
			
		||||
        "Deleting change %s is not permitted", id));
 | 
			
		||||
    gApi.changes()
 | 
			
		||||
        .id(changeId)
 | 
			
		||||
        .delete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestProjectInput(cloneAs = "user")
 | 
			
		||||
  public void deleteDraftChangeWhenDraftsNotAllowedAsNormalUser()
 | 
			
		||||
      throws Exception {
 | 
			
		||||
    assume().that(isAllowDrafts()).isFalse();
 | 
			
		||||
 | 
			
		||||
    setApiUser(user);
 | 
			
		||||
    // We can't create a draft change while the draft workflow is disabled.
 | 
			
		||||
    // For this reason, we create a normal change and modify the database.
 | 
			
		||||
    PushOneCommit.Result changeResult =
 | 
			
		||||
        pushFactory.create(db, user.getIdent(), testRepo)
 | 
			
		||||
            .to("refs/for/master");
 | 
			
		||||
    Change.Id id = changeResult.getChange().getId();
 | 
			
		||||
    markChangeAsDraft(id);
 | 
			
		||||
    setDraftStatusOfPatchSetsOfChange(id, true);
 | 
			
		||||
 | 
			
		||||
    String changeId = changeResult.getChangeId();
 | 
			
		||||
    exception.expect(MethodNotAllowedException.class);
 | 
			
		||||
    exception.expectMessage("Draft workflow is disabled");
 | 
			
		||||
    gApi.changes()
 | 
			
		||||
        .id(changeId)
 | 
			
		||||
        .delete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestProjectInput(cloneAs = "user")
 | 
			
		||||
  public void deleteDraftChangeWhenDraftsNotAllowedAsAdmin() throws Exception {
 | 
			
		||||
    assume().that(isAllowDrafts()).isFalse();
 | 
			
		||||
 | 
			
		||||
    setApiUser(user);
 | 
			
		||||
    // We can't create a draft change while the draft workflow is disabled.
 | 
			
		||||
    // For this reason, we create a normal change and modify the database.
 | 
			
		||||
    PushOneCommit.Result changeResult =
 | 
			
		||||
        pushFactory.create(db, user.getIdent(), testRepo)
 | 
			
		||||
        .to("refs/for/master");
 | 
			
		||||
    Change.Id id = changeResult.getChange().getId();
 | 
			
		||||
    markChangeAsDraft(id);
 | 
			
		||||
    setDraftStatusOfPatchSetsOfChange(id, true);
 | 
			
		||||
 | 
			
		||||
    String changeId = changeResult.getChangeId();
 | 
			
		||||
 | 
			
		||||
    // Grant those permissions to admins.
 | 
			
		||||
    grant(Permission.VIEW_DRAFTS, project, "refs/*");
 | 
			
		||||
    grant(Permission.DELETE_DRAFTS, project, "refs/*");
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      setApiUser(admin);
 | 
			
		||||
      gApi.changes()
 | 
			
		||||
          .id(changeId)
 | 
			
		||||
          .delete();
 | 
			
		||||
    } finally {
 | 
			
		||||
      removePermission(Permission.DELETE_DRAFTS, project, "refs/*");
 | 
			
		||||
      removePermission(Permission.VIEW_DRAFTS, project, "refs/*");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setApiUser(user);
 | 
			
		||||
    assertThat(query(changeId)).isEmpty();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void deleteDraftChangeWithNonDraftPatchSet() throws Exception {
 | 
			
		||||
    assume().that(isAllowDrafts()).isTrue();
 | 
			
		||||
 | 
			
		||||
    PushOneCommit.Result changeResult = createDraftChange();
 | 
			
		||||
    Change.Id id = changeResult.getChange().getId();
 | 
			
		||||
    setDraftStatusOfPatchSetsOfChange(id, false);
 | 
			
		||||
 | 
			
		||||
    String changeId = changeResult.getChangeId();
 | 
			
		||||
    exception.expect(ResourceConflictException.class);
 | 
			
		||||
    exception.expectMessage(String.format(
 | 
			
		||||
        "Cannot delete draft change %s: patch set 1 is not a draft", id));
 | 
			
		||||
    gApi.changes()
 | 
			
		||||
        .id(changeId)
 | 
			
		||||
        .delete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void publishDraftChange() throws Exception {
 | 
			
		||||
    assume().that(isAllowDrafts()).isTrue();
 | 
			
		||||
@@ -160,4 +264,92 @@ public class DraftChangeIT extends AbstractDaemonTest {
 | 
			
		||||
        + patchSet.getRevision().get()
 | 
			
		||||
        + "/publish");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void markChangeAsDraft(Change.Id id) throws OrmException,
 | 
			
		||||
      RestApiException, UpdateException {
 | 
			
		||||
    try (BatchUpdate batchUpdate = updateFactory
 | 
			
		||||
        .create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) {
 | 
			
		||||
      batchUpdate.addOp(id, new MarkChangeAsDraftUpdateOp());
 | 
			
		||||
      batchUpdate.execute();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ChangeStatus changeStatus = gApi.changes()
 | 
			
		||||
        .id(id.get())
 | 
			
		||||
        .get()
 | 
			
		||||
        .status;
 | 
			
		||||
    assertThat(changeStatus).isEqualTo(ChangeStatus.DRAFT);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void setDraftStatusOfPatchSetsOfChange(Change.Id id,
 | 
			
		||||
      boolean draftStatus) throws OrmException, RestApiException,
 | 
			
		||||
      UpdateException {
 | 
			
		||||
    try (BatchUpdate batchUpdate = updateFactory
 | 
			
		||||
        .create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) {
 | 
			
		||||
      batchUpdate.addOp(id, new DraftStatusOfPatchSetsUpdateOp(draftStatus));
 | 
			
		||||
      batchUpdate.execute();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Boolean expectedDraftStatus = draftStatus ? Boolean.TRUE : null;
 | 
			
		||||
    List<Boolean> patchSetDraftStatuses = getPatchSetDraftStatuses(id);
 | 
			
		||||
    patchSetDraftStatuses.forEach(status ->
 | 
			
		||||
        assertThat(status).isEqualTo(expectedDraftStatus));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private List<Boolean> getPatchSetDraftStatuses(Change.Id id)
 | 
			
		||||
      throws RestApiException {
 | 
			
		||||
    Collection<RevisionInfo> revisionInfos = gApi.changes()
 | 
			
		||||
        .id(id.get())
 | 
			
		||||
        .get(EnumSet.of(ListChangesOption.ALL_REVISIONS))
 | 
			
		||||
        .revisions
 | 
			
		||||
        .values();
 | 
			
		||||
    return revisionInfos.stream()
 | 
			
		||||
        .map(revisionInfo -> revisionInfo.draft)
 | 
			
		||||
        .collect(Collectors.toList());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private class MarkChangeAsDraftUpdateOp extends BatchUpdate.Op {
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean updateChange(BatchUpdate.ChangeContext ctx)
 | 
			
		||||
        throws Exception {
 | 
			
		||||
      Change change = ctx.getChange();
 | 
			
		||||
 | 
			
		||||
      // Change status in database.
 | 
			
		||||
      change.setStatus(Change.Status.DRAFT);
 | 
			
		||||
 | 
			
		||||
      // Change status in NoteDb.
 | 
			
		||||
      PatchSet.Id currentPatchSetId = change.currentPatchSetId();
 | 
			
		||||
      ctx.getUpdate(currentPatchSetId).setStatus(Change.Status.DRAFT);
 | 
			
		||||
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private class DraftStatusOfPatchSetsUpdateOp extends BatchUpdate.Op {
 | 
			
		||||
    private final boolean draftStatus;
 | 
			
		||||
 | 
			
		||||
    DraftStatusOfPatchSetsUpdateOp(boolean draftStatus) {
 | 
			
		||||
      this.draftStatus = draftStatus;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean updateChange(BatchUpdate.ChangeContext ctx)
 | 
			
		||||
        throws Exception {
 | 
			
		||||
      Collection<PatchSet> patchSets = psUtil.byChange(db, ctx.getNotes());
 | 
			
		||||
 | 
			
		||||
      // Change status in database.
 | 
			
		||||
      patchSets.forEach(patchSet -> patchSet.setDraft(draftStatus));
 | 
			
		||||
      db.patchSets().update(patchSets);
 | 
			
		||||
 | 
			
		||||
      // Change status in NoteDb.
 | 
			
		||||
      PatchSetState patchSetState = draftStatus ? PatchSetState.DRAFT
 | 
			
		||||
          : PatchSetState.PUBLISHED;
 | 
			
		||||
      patchSets.stream()
 | 
			
		||||
          .map(PatchSet::getId)
 | 
			
		||||
          .map(ctx::getUpdate)
 | 
			
		||||
          .forEach(changeUpdate ->
 | 
			
		||||
              changeUpdate.setPatchSetState(patchSetState));
 | 
			
		||||
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -112,7 +112,7 @@ public interface ChangeApi {
 | 
			
		||||
  void publish() throws RestApiException;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Deletes a draft change.
 | 
			
		||||
   * Deletes a change.
 | 
			
		||||
   */
 | 
			
		||||
  void delete() throws RestApiException;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,6 @@ public interface ChangeConstants extends Constants {
 | 
			
		||||
  String abandoned();
 | 
			
		||||
 | 
			
		||||
  String deleteChangeEdit();
 | 
			
		||||
  String deleteDraftChange();
 | 
			
		||||
  String deleteChange();
 | 
			
		||||
  String deleteDraftRevision();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,5 +34,5 @@ abandoned = Abandoned
 | 
			
		||||
deleteChangeEdit = Delete Change Edit?\n\
 | 
			
		||||
  \n\
 | 
			
		||||
  All changes made in the edit revision will be lost.
 | 
			
		||||
deleteDraftChange = Delete Draft Change?
 | 
			
		||||
deleteChange = Delete Change?
 | 
			
		||||
deleteDraftRevision = Delete Draft Revision?
 | 
			
		||||
 
 | 
			
		||||
@@ -495,15 +495,13 @@ public class ChangeScreen extends Screen {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void initChangeAction(ChangeInfo info) {
 | 
			
		||||
    if (info.status() == Status.DRAFT) {
 | 
			
		||||
      NativeMap<ActionInfo> actions = info.hasActions()
 | 
			
		||||
          ? info.actions()
 | 
			
		||||
          : NativeMap.<ActionInfo> create();
 | 
			
		||||
      actions.copyKeysIntoChildren("id");
 | 
			
		||||
      if (actions.containsKey("/")) {
 | 
			
		||||
        deleteChange.setVisible(true);
 | 
			
		||||
        deleteChange.setTitle(actions.get("/").title());
 | 
			
		||||
      }
 | 
			
		||||
    NativeMap<ActionInfo> actions = info.hasActions()
 | 
			
		||||
        ? info.actions()
 | 
			
		||||
        : NativeMap.create();
 | 
			
		||||
    actions.copyKeysIntoChildren("id");
 | 
			
		||||
    if (actions.containsKey("/")) {
 | 
			
		||||
      deleteChange.setVisible(true);
 | 
			
		||||
      deleteChange.setTitle(actions.get("/").title());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -679,7 +677,7 @@ public class ChangeScreen extends Screen {
 | 
			
		||||
 | 
			
		||||
  @UiHandler("deleteChange")
 | 
			
		||||
  void onDeleteChange(@SuppressWarnings("unused") ClickEvent e) {
 | 
			
		||||
    if (Window.confirm(Resources.C.deleteDraftChange())) {
 | 
			
		||||
    if (Window.confirm(Resources.C.deleteChange())) {
 | 
			
		||||
      DraftActions.delete(changeId, publish, deleteRevision, deleteChange);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ import com.google.gerrit.server.change.ChangeResource;
 | 
			
		||||
import com.google.gerrit.server.change.Check;
 | 
			
		||||
import com.google.gerrit.server.change.CreateMergePatchSet;
 | 
			
		||||
import com.google.gerrit.server.change.DeleteAssignee;
 | 
			
		||||
import com.google.gerrit.server.change.DeleteDraftChange;
 | 
			
		||||
import com.google.gerrit.server.change.DeleteChange;
 | 
			
		||||
import com.google.gerrit.server.change.GetAssignee;
 | 
			
		||||
import com.google.gerrit.server.change.GetHashtags;
 | 
			
		||||
import com.google.gerrit.server.change.GetPastAssignees;
 | 
			
		||||
@@ -98,7 +98,7 @@ class ChangeApiImpl implements ChangeApi {
 | 
			
		||||
  private final Provider<SubmittedTogether> submittedTogether;
 | 
			
		||||
  private final PublishDraftPatchSet.CurrentRevision
 | 
			
		||||
    publishDraftChange;
 | 
			
		||||
  private final DeleteDraftChange deleteDraftChange;
 | 
			
		||||
  private final DeleteChange deleteChange;
 | 
			
		||||
  private final GetTopic getTopic;
 | 
			
		||||
  private final PutTopic putTopic;
 | 
			
		||||
  private final PostReviewers postReviewers;
 | 
			
		||||
@@ -129,7 +129,7 @@ class ChangeApiImpl implements ChangeApi {
 | 
			
		||||
      CreateMergePatchSet updateByMerge,
 | 
			
		||||
      Provider<SubmittedTogether> submittedTogether,
 | 
			
		||||
      PublishDraftPatchSet.CurrentRevision publishDraftChange,
 | 
			
		||||
      DeleteDraftChange deleteDraftChange,
 | 
			
		||||
      DeleteChange deleteChange,
 | 
			
		||||
      GetTopic getTopic,
 | 
			
		||||
      PutTopic putTopic,
 | 
			
		||||
      PostReviewers postReviewers,
 | 
			
		||||
@@ -159,7 +159,7 @@ class ChangeApiImpl implements ChangeApi {
 | 
			
		||||
    this.updateByMerge = updateByMerge;
 | 
			
		||||
    this.submittedTogether = submittedTogether;
 | 
			
		||||
    this.publishDraftChange = publishDraftChange;
 | 
			
		||||
    this.deleteDraftChange = deleteDraftChange;
 | 
			
		||||
    this.deleteChange = deleteChange;
 | 
			
		||||
    this.getTopic = getTopic;
 | 
			
		||||
    this.putTopic = putTopic;
 | 
			
		||||
    this.postReviewers = postReviewers;
 | 
			
		||||
@@ -324,7 +324,7 @@ class ChangeApiImpl implements ChangeApi {
 | 
			
		||||
  @Override
 | 
			
		||||
  public void delete() throws RestApiException {
 | 
			
		||||
    try {
 | 
			
		||||
      deleteDraftChange.apply(change, null);
 | 
			
		||||
      deleteChange.apply(change, null);
 | 
			
		||||
    } catch (UpdateException e) {
 | 
			
		||||
      throw new RestApiException("Cannot delete change", e);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -658,7 +658,7 @@ public class ConsistencyChecker {
 | 
			
		||||
    public boolean updateChange(ChangeContext ctx)
 | 
			
		||||
        throws OrmException, PatchSetInfoNotAvailableException {
 | 
			
		||||
      // Delete dangling key references.
 | 
			
		||||
      ReviewDb db = DeleteDraftChangeOp.unwrap(ctx.getDb());
 | 
			
		||||
      ReviewDb db = DeleteChangeOp.unwrap(ctx.getDb());
 | 
			
		||||
      accountPatchReviewStore.get().clearReviewed(psId);
 | 
			
		||||
      db.changeMessages().delete(
 | 
			
		||||
          db.changeMessages().byChange(psId.getParentKey()));
 | 
			
		||||
 
 | 
			
		||||
@@ -22,10 +22,11 @@ import com.google.gerrit.extensions.webui.UiAction;
 | 
			
		||||
import com.google.gerrit.reviewdb.client.Change;
 | 
			
		||||
import com.google.gerrit.reviewdb.client.Change.Status;
 | 
			
		||||
import com.google.gerrit.reviewdb.server.ReviewDb;
 | 
			
		||||
import com.google.gerrit.server.change.DeleteDraftChange.Input;
 | 
			
		||||
import com.google.gerrit.server.change.DeleteChange.Input;
 | 
			
		||||
import com.google.gerrit.server.config.GerritServerConfig;
 | 
			
		||||
import com.google.gerrit.server.git.BatchUpdate;
 | 
			
		||||
import com.google.gerrit.server.git.UpdateException;
 | 
			
		||||
import com.google.gerrit.server.project.ChangeControl;
 | 
			
		||||
import com.google.gwtorm.server.OrmException;
 | 
			
		||||
import com.google.inject.Inject;
 | 
			
		||||
import com.google.inject.Provider;
 | 
			
		||||
@@ -34,25 +35,25 @@ import com.google.inject.Singleton;
 | 
			
		||||
import org.eclipse.jgit.lib.Config;
 | 
			
		||||
 | 
			
		||||
@Singleton
 | 
			
		||||
public class DeleteDraftChange implements
 | 
			
		||||
public class DeleteChange implements
 | 
			
		||||
    RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
 | 
			
		||||
  public static class Input {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private final Provider<ReviewDb> db;
 | 
			
		||||
  private final BatchUpdate.Factory updateFactory;
 | 
			
		||||
  private final Provider<DeleteDraftChangeOp> opProvider;
 | 
			
		||||
  private final Provider<DeleteChangeOp> opProvider;
 | 
			
		||||
  private final boolean allowDrafts;
 | 
			
		||||
 | 
			
		||||
  @Inject
 | 
			
		||||
  public DeleteDraftChange(Provider<ReviewDb> db,
 | 
			
		||||
  public DeleteChange(Provider<ReviewDb> db,
 | 
			
		||||
      BatchUpdate.Factory updateFactory,
 | 
			
		||||
      Provider<DeleteDraftChangeOp> opProvider,
 | 
			
		||||
      Provider<DeleteChangeOp> opProvider,
 | 
			
		||||
      @GerritServerConfig Config cfg) {
 | 
			
		||||
    this.db = db;
 | 
			
		||||
    this.updateFactory = updateFactory;
 | 
			
		||||
    this.opProvider = opProvider;
 | 
			
		||||
    this.allowDrafts = DeleteDraftChangeOp.allowDrafts(cfg);
 | 
			
		||||
    this.allowDrafts = DeleteChangeOp.allowDrafts(cfg);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
@@ -71,14 +72,21 @@ public class DeleteDraftChange implements
 | 
			
		||||
  @Override
 | 
			
		||||
  public UiAction.Description getDescription(ChangeResource rsrc) {
 | 
			
		||||
    try {
 | 
			
		||||
      Change.Status status = rsrc.getChange().getStatus();
 | 
			
		||||
      ChangeControl changeControl = rsrc.getControl();
 | 
			
		||||
      boolean visible = isActionAllowed(changeControl, status)
 | 
			
		||||
          && changeControl.canDelete(db.get(), status);
 | 
			
		||||
      return new UiAction.Description()
 | 
			
		||||
        .setLabel("Delete")
 | 
			
		||||
        .setTitle("Delete draft change " + rsrc.getId())
 | 
			
		||||
        .setVisible(allowDrafts
 | 
			
		||||
            && rsrc.getChange().getStatus() == Status.DRAFT
 | 
			
		||||
            && rsrc.getControl().canDeleteDraft(db.get()));
 | 
			
		||||
        .setTitle("Delete change " + rsrc.getId())
 | 
			
		||||
        .setVisible(visible);
 | 
			
		||||
    } catch (OrmException e) {
 | 
			
		||||
      throw new IllegalStateException(e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private boolean isActionAllowed(ChangeControl changeControl,
 | 
			
		||||
      Status status) {
 | 
			
		||||
    return status != Status.DRAFT || allowDrafts || changeControl.isAdmin();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -44,7 +44,7 @@ import org.eclipse.jgit.transport.ReceiveCommand;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
 | 
			
		||||
class DeleteDraftChangeOp extends BatchUpdate.Op {
 | 
			
		||||
class DeleteChangeOp extends BatchUpdate.Op {
 | 
			
		||||
  static boolean allowDrafts(Config cfg) {
 | 
			
		||||
    return cfg.getBoolean("change", "allowDrafts", true);
 | 
			
		||||
  }
 | 
			
		||||
@@ -68,7 +68,7 @@ class DeleteDraftChangeOp extends BatchUpdate.Op {
 | 
			
		||||
  private Change.Id id;
 | 
			
		||||
 | 
			
		||||
  @Inject
 | 
			
		||||
  DeleteDraftChangeOp(PatchSetUtil psUtil,
 | 
			
		||||
  DeleteChangeOp(PatchSetUtil psUtil,
 | 
			
		||||
      StarredChangesUtil starredChangesUtil,
 | 
			
		||||
      DynamicItem<AccountPatchReviewStore> accountPatchReviewStore,
 | 
			
		||||
      @GerritServerConfig Config cfg) {
 | 
			
		||||
@@ -82,16 +82,18 @@ class DeleteDraftChangeOp extends BatchUpdate.Op {
 | 
			
		||||
  public boolean updateChange(ChangeContext ctx) throws RestApiException,
 | 
			
		||||
      OrmException, IOException, NoSuchChangeException {
 | 
			
		||||
    checkState(ctx.getOrder() == BatchUpdate.Order.DB_BEFORE_REPO,
 | 
			
		||||
        "must use DeleteDraftChangeOp with DB_BEFORE_REPO");
 | 
			
		||||
    checkState(id == null, "cannot reuse DeleteDraftChangeOp");
 | 
			
		||||
        "must use DeleteChangeOp with DB_BEFORE_REPO");
 | 
			
		||||
    checkState(id == null, "cannot reuse DeleteChangeOp");
 | 
			
		||||
 | 
			
		||||
    id = ctx.getChange().getId();
 | 
			
		||||
    Collection<PatchSet> patchSets = psUtil.byChange(ctx.getDb(),
 | 
			
		||||
        ctx.getNotes());
 | 
			
		||||
 | 
			
		||||
    ensureDeletable(ctx, id, patchSets);
 | 
			
		||||
    deleteChangeElementsFromDb(ctx, id);
 | 
			
		||||
    // Cleaning up is only possible as long as the change and its elements are
 | 
			
		||||
    // still part of the database.
 | 
			
		||||
    cleanUpReferences(ctx, id, patchSets);
 | 
			
		||||
    deleteChangeElementsFromDb(ctx, id);
 | 
			
		||||
 | 
			
		||||
    ctx.deleteChange();
 | 
			
		||||
    return true;
 | 
			
		||||
@@ -100,19 +102,25 @@ class DeleteDraftChangeOp extends BatchUpdate.Op {
 | 
			
		||||
  private void ensureDeletable(ChangeContext ctx, Change.Id id,
 | 
			
		||||
      Collection<PatchSet> patchSets) throws ResourceConflictException,
 | 
			
		||||
      MethodNotAllowedException, OrmException, AuthException {
 | 
			
		||||
    if (ctx.getChange().getStatus() != Change.Status.DRAFT) {
 | 
			
		||||
      throw new ResourceConflictException("Change is not a draft: " + id);
 | 
			
		||||
    Change.Status status = ctx.getChange().getStatus();
 | 
			
		||||
    if (status == Change.Status.MERGED) {
 | 
			
		||||
      throw new MethodNotAllowedException("Deleting merged change " + id
 | 
			
		||||
          + " is not allowed");
 | 
			
		||||
    }
 | 
			
		||||
    if (!allowDrafts) {
 | 
			
		||||
      throw new MethodNotAllowedException("Draft workflow is disabled");
 | 
			
		||||
 | 
			
		||||
    if (!ctx.getControl().canDelete(ctx.getDb(), status)) {
 | 
			
		||||
      throw new AuthException("Deleting change " + id + " is not permitted");
 | 
			
		||||
    }
 | 
			
		||||
    if (!ctx.getControl().canDeleteDraft(ctx.getDb())) {
 | 
			
		||||
      throw new AuthException("Not permitted to delete this draft change");
 | 
			
		||||
    }
 | 
			
		||||
    for (PatchSet ps : patchSets) {
 | 
			
		||||
      if (!ps.isDraft()) {
 | 
			
		||||
        throw new ResourceConflictException("Cannot delete draft change " + id
 | 
			
		||||
            + ": patch set " + ps.getPatchSetId() + " is not a draft");
 | 
			
		||||
 | 
			
		||||
    if (status == Change.Status.DRAFT) {
 | 
			
		||||
      if (!allowDrafts && !ctx.getControl().isAdmin()) {
 | 
			
		||||
        throw new MethodNotAllowedException("Draft workflow is disabled");
 | 
			
		||||
      }
 | 
			
		||||
      for (PatchSet ps : patchSets) {
 | 
			
		||||
        if (!ps.isDraft()) {
 | 
			
		||||
          throw new ResourceConflictException("Cannot delete draft change " + id
 | 
			
		||||
              + ": patch set " + ps.getPatchSetId() + " is not a draft");
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -59,7 +59,7 @@ public class DeleteDraftPatchSet implements RestModifyView<RevisionResource, Inp
 | 
			
		||||
  private final BatchUpdate.Factory updateFactory;
 | 
			
		||||
  private final PatchSetInfoFactory patchSetInfoFactory;
 | 
			
		||||
  private final PatchSetUtil psUtil;
 | 
			
		||||
  private final Provider<DeleteDraftChangeOp> deleteChangeOpProvider;
 | 
			
		||||
  private final Provider<DeleteChangeOp> deleteChangeOpProvider;
 | 
			
		||||
  private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore;
 | 
			
		||||
  private final boolean allowDrafts;
 | 
			
		||||
 | 
			
		||||
@@ -68,7 +68,7 @@ public class DeleteDraftPatchSet implements RestModifyView<RevisionResource, Inp
 | 
			
		||||
      BatchUpdate.Factory updateFactory,
 | 
			
		||||
      PatchSetInfoFactory patchSetInfoFactory,
 | 
			
		||||
      PatchSetUtil psUtil,
 | 
			
		||||
      Provider<DeleteDraftChangeOp> deleteChangeOpProvider,
 | 
			
		||||
      Provider<DeleteChangeOp> deleteChangeOpProvider,
 | 
			
		||||
      DynamicItem<AccountPatchReviewStore> accountPatchReviewStore,
 | 
			
		||||
      @GerritServerConfig Config cfg) {
 | 
			
		||||
    this.db = db;
 | 
			
		||||
@@ -97,7 +97,7 @@ public class DeleteDraftPatchSet implements RestModifyView<RevisionResource, Inp
 | 
			
		||||
 | 
			
		||||
    private Collection<PatchSet> patchSetsBeforeDeletion;
 | 
			
		||||
    private PatchSet patchSet;
 | 
			
		||||
    private DeleteDraftChangeOp deleteChangeOp;
 | 
			
		||||
    private DeleteChangeOp deleteChangeOp;
 | 
			
		||||
 | 
			
		||||
    private Op(PatchSet.Id psId) {
 | 
			
		||||
      this.psId = psId;
 | 
			
		||||
@@ -116,7 +116,7 @@ public class DeleteDraftPatchSet implements RestModifyView<RevisionResource, Inp
 | 
			
		||||
      if (!allowDrafts) {
 | 
			
		||||
        throw new MethodNotAllowedException("Draft workflow is disabled");
 | 
			
		||||
      }
 | 
			
		||||
      if (!ctx.getControl().canDeleteDraft(ctx.getDb())) {
 | 
			
		||||
      if (!ctx.getControl().canDelete(ctx.getDb(), Change.Status.DRAFT)) {
 | 
			
		||||
        throw new AuthException("Not permitted to delete this draft patch set");
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@@ -146,8 +146,8 @@ public class DeleteDraftPatchSet implements RestModifyView<RevisionResource, Inp
 | 
			
		||||
      psUtil.delete(ctx.getDb(), ctx.getUpdate(patchSet.getId()), patchSet);
 | 
			
		||||
 | 
			
		||||
      accountPatchReviewStore.get().clearReviewed(psId);
 | 
			
		||||
      // Use the unwrap from DeleteDraftChangeOp to handle BatchUpdateReviewDb.
 | 
			
		||||
      ReviewDb db = DeleteDraftChangeOp.unwrap(ctx.getDb());
 | 
			
		||||
      // Use the unwrap from DeleteChangeOp to handle BatchUpdateReviewDb.
 | 
			
		||||
      ReviewDb db = DeleteChangeOp.unwrap(ctx.getDb());
 | 
			
		||||
      db.changeMessages().delete(db.changeMessages().byPatchSet(psId));
 | 
			
		||||
      db.patchComments().delete(db.patchComments().byPatchSet(psId));
 | 
			
		||||
      db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(psId));
 | 
			
		||||
@@ -195,7 +195,7 @@ public class DeleteDraftPatchSet implements RestModifyView<RevisionResource, Inp
 | 
			
		||||
            rsrc.getPatchSet().getPatchSetId()))
 | 
			
		||||
        .setVisible(allowDrafts
 | 
			
		||||
            && rsrc.getPatchSet().isDraft()
 | 
			
		||||
            && rsrc.getControl().canDeleteDraft(db.get())
 | 
			
		||||
            && rsrc.getControl().canDelete(db.get(), Change.Status.DRAFT)
 | 
			
		||||
            && psCount > 1);
 | 
			
		||||
    } catch (OrmException e) {
 | 
			
		||||
      throw new IllegalStateException(e);
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@ public class Module extends RestApiModule {
 | 
			
		||||
    post(CHANGE_KIND, "check").to(Check.class);
 | 
			
		||||
    put(CHANGE_KIND, "topic").to(PutTopic.class);
 | 
			
		||||
    delete(CHANGE_KIND, "topic").to(PutTopic.class);
 | 
			
		||||
    delete(CHANGE_KIND).to(DeleteDraftChange.class);
 | 
			
		||||
    delete(CHANGE_KIND).to(DeleteChange.class);
 | 
			
		||||
    post(CHANGE_KIND, "abandon").to(Abandon.class);
 | 
			
		||||
    post(CHANGE_KIND, "hashtags").to(PostHashtags.class);
 | 
			
		||||
    post(CHANGE_KIND, "publish").to(PublishDraftPatchSet.CurrentRevision.class);
 | 
			
		||||
 
 | 
			
		||||
@@ -261,10 +261,23 @@ public class ChangeControl {
 | 
			
		||||
        && isVisible(db);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Can this user delete this draft change or any draft patch set of this change? */
 | 
			
		||||
  public boolean canDeleteDraft(final ReviewDb db) throws OrmException {
 | 
			
		||||
    return (isOwner() || getRefControl().canDeleteDrafts())
 | 
			
		||||
        && isVisible(db);
 | 
			
		||||
  /** Can this user delete this change or any patch set of this change? */
 | 
			
		||||
  public boolean canDelete(ReviewDb db, Change.Status status)
 | 
			
		||||
      throws OrmException {
 | 
			
		||||
    if (!isVisible(db)) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    switch (status) {
 | 
			
		||||
      case DRAFT:
 | 
			
		||||
        return (isOwner() || getRefControl().canDeleteDrafts());
 | 
			
		||||
      case NEW:
 | 
			
		||||
      case ABANDONED:
 | 
			
		||||
        return isAdmin();
 | 
			
		||||
      case MERGED:
 | 
			
		||||
      default:
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Can this user rebase this change? */
 | 
			
		||||
@@ -377,6 +390,10 @@ public class ChangeControl {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public boolean isAdmin() {
 | 
			
		||||
    return getUser().getCapabilities().canAdministrateServer();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @return true if the user is allowed to remove this reviewer. */
 | 
			
		||||
  public boolean canRemoveReviewer(PatchSetApproval approval) {
 | 
			
		||||
    return canRemoveReviewer(approval.getAccountId(), approval.getValue());
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user