Add REST endpoint to delete a label definition

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I7b2d642e93298fb5e2aac1d4ac08b7182e1a5149
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index a09075d..67f8592 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -3253,6 +3253,37 @@
   }
 ----
 
+[[delete-label]]
+=== Delete Label
+--
+'DELETE /projects/link:#project-name[\{project-name\}]/labels/link:#label-name[\{label-name\}]'
+--
+
+Deletes the definition of a label that is defined in this project.
+
+The calling user must have write access to the `refs/meta/config` branch of the
+project.
+
+The request body does not need to include a link:#delete-label-input[
+DeleteLabelInput] entity if no commit message is specified.
+
+.Request
+----
+  Delete /projects/My-Project/labels/Foo-Review HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "commit_message": "Delete Foo-Review label",
+  }
+----
+
+If a label was deleted the response is "`204 No Content`".
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
 
 [[ids]]
 == IDs
@@ -3709,6 +3740,19 @@
 Tokens such as `${project}` are not resolved.
 |===========================
 
+[[delete-label-input]]
+=== DeleteLabelInput
+The `DeleteLabelInput` entity contains information for deleting a label
+definition in a project.
+
+[options="header",cols="1,^2,4"]
+|=============================
+|Field Name      ||Description
+|`commit_message`|optional|
+Message that should be used to commit the deletion of the label in the
+`project.config` file to the `refs/meta/config` branch.
+|=============================
+
 [[delete-branches-input]]
 === DeleteBranchesInput
 The `DeleteBranchesInput` entity contains information about branches that should
diff --git a/java/com/google/gerrit/extensions/api/projects/LabelApi.java b/java/com/google/gerrit/extensions/api/projects/LabelApi.java
index bee9e53..975a57e 100644
--- a/java/com/google/gerrit/extensions/api/projects/LabelApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/LabelApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.LabelDefinitionInfo;
 import com.google.gerrit.extensions.common.LabelDefinitionInput;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -26,6 +27,12 @@
 
   LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException;
 
+  default void delete() throws RestApiException {
+    delete(null);
+  }
+
+  void delete(@Nullable String commitMessage) throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -45,5 +52,10 @@
     public LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public void delete(@Nullable String commitMessage) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/InputWithCommitMessage.java b/java/com/google/gerrit/extensions/common/InputWithCommitMessage.java
new file mode 100644
index 0000000..3e96b30
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/InputWithCommitMessage.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2019 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.extensions.common;
+
+import com.google.gerrit.common.Nullable;
+
+/** A generic input with a commit message only. */
+public class InputWithCommitMessage {
+  public String commitMessage;
+
+  public InputWithCommitMessage() {
+    this(null);
+  }
+
+  public InputWithCommitMessage(@Nullable String commitMessage) {
+    this.commitMessage = commitMessage;
+  }
+}
diff --git a/java/com/google/gerrit/server/api/projects/LabelApiImpl.java b/java/com/google/gerrit/server/api/projects/LabelApiImpl.java
index 887f5ae..ad7ec31 100644
--- a/java/com/google/gerrit/server/api/projects/LabelApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/LabelApiImpl.java
@@ -16,7 +16,9 @@
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.projects.LabelApi;
+import com.google.gerrit.extensions.common.InputWithCommitMessage;
 import com.google.gerrit.extensions.common.LabelDefinitionInfo;
 import com.google.gerrit.extensions.common.LabelDefinitionInput;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -26,6 +28,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.restapi.project.CreateLabel;
+import com.google.gerrit.server.restapi.project.DeleteLabel;
 import com.google.gerrit.server.restapi.project.GetLabel;
 import com.google.gerrit.server.restapi.project.LabelsCollection;
 import com.google.gerrit.server.restapi.project.SetLabel;
@@ -41,6 +44,7 @@
   private final CreateLabel createLabel;
   private final GetLabel getLabel;
   private final SetLabel setLabel;
+  private final DeleteLabel deleteLabel;
   private final ProjectCache projectCache;
   private final String label;
 
@@ -52,6 +56,7 @@
       CreateLabel createLabel,
       GetLabel getLabel,
       SetLabel setLabel,
+      DeleteLabel deleteLabel,
       ProjectCache projectCache,
       @Assisted ProjectResource project,
       @Assisted String label) {
@@ -59,6 +64,7 @@
     this.createLabel = createLabel;
     this.getLabel = getLabel;
     this.setLabel = setLabel;
+    this.deleteLabel = deleteLabel;
     this.projectCache = projectCache;
     this.project = project;
     this.label = label;
@@ -97,6 +103,15 @@
     }
   }
 
+  @Override
+  public void delete(@Nullable String commitMessage) throws RestApiException {
+    try {
+      deleteLabel.apply(resource(), new InputWithCommitMessage(commitMessage));
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete label", e);
+    }
+  }
+
   private LabelResource resource() throws RestApiException, PermissionBackendException {
     return labels.parse(project, IdString.fromDecoded(label));
   }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteLabel.java b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
new file mode 100644
index 0000000..5464abf
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2019 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.restapi.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.common.InputWithCommitMessage;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.LabelResource;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class DeleteLabel implements RestModifyView<LabelResource, InputWithCommitMessage> {
+  private final Provider<CurrentUser> user;
+  private final PermissionBackend permissionBackend;
+  private final MetaDataUpdate.User updateFactory;
+  private final ProjectConfig.Factory projectConfigFactory;
+  private final ProjectCache projectCache;
+
+  @Inject
+  public DeleteLabel(
+      Provider<CurrentUser> user,
+      PermissionBackend permissionBackend,
+      MetaDataUpdate.User updateFactory,
+      ProjectConfig.Factory projectConfigFactory,
+      ProjectCache projectCache) {
+    this.user = user;
+    this.permissionBackend = permissionBackend;
+    this.updateFactory = updateFactory;
+    this.projectConfigFactory = projectConfigFactory;
+    this.projectCache = projectCache;
+  }
+
+  @Override
+  public Response<?> apply(LabelResource rsrc, InputWithCommitMessage input)
+      throws AuthException, ResourceNotFoundException, PermissionBackendException, IOException,
+          ConfigInvalidException {
+    if (!user.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    permissionBackend
+        .currentUser()
+        .project(rsrc.getProject().getNameKey())
+        .check(ProjectPermission.WRITE_CONFIG);
+
+    if (input == null) {
+      input = new InputWithCommitMessage();
+    }
+
+    try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) {
+      ProjectConfig config = projectConfigFactory.read(md);
+
+      if (!config.getLabelSections().containsKey(rsrc.getLabelType().getName())) {
+        throw new ResourceNotFoundException(IdString.fromDecoded(rsrc.getLabelType().getName()));
+      }
+
+      config.getLabelSections().remove(rsrc.getLabelType().getName());
+
+      if (input.commitMessage != null) {
+        md.setMessage(Strings.emptyToNull(input.commitMessage.trim()));
+      } else {
+        md.setMessage("Delete label");
+      }
+
+      config.commit(md);
+    }
+
+    projectCache.evict(rsrc.getProject().getProjectState().getProject());
+
+    return Response.none();
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java
index 7ec4e6d..4ed21cc 100644
--- a/java/com/google/gerrit/server/restapi/project/Module.java
+++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -71,6 +71,7 @@
     create(LABEL_KIND).to(CreateLabel.class);
     get(LABEL_KIND).to(GetLabel.class);
     put(LABEL_KIND).to(SetLabel.class);
+    delete(LABEL_KIND).to(DeleteLabel.class);
 
     get(PROJECT_KIND, "HEAD").to(GetHead.class);
     put(PROJECT_KIND, "HEAD").to(SetHead.class);
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index ebb918c..02557cc 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -168,7 +168,11 @@
    */
   private static final ImmutableList<RestCall> LABEL_ENDPOINTS =
       ImmutableList.of(
-          RestCall.get("/projects/%s/labels/%s"), RestCall.put("/projects/%s/labels/%s"));
+          RestCall.get("/projects/%s/labels/%s"),
+          RestCall.put("/projects/%s/labels/%s"),
+
+          // Label deletion must be tested last
+          RestCall.delete("/projects/%s/labels/%s"));
 
   private static final String FILENAME = "test.txt";
   @Inject private ProjectOperations projectOperations;
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java
new file mode 100644
index 0000000..c916285
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2019 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.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+public class DeleteLabelIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ProjectOperations projectOperations;
+
+  @Test
+  public void anonymous() throws Exception {
+    requestScopeOperations.setApiUserAnonymous();
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.projects().name(allProjects.get()).label("Code-Review").delete());
+    assertThat(thrown).hasMessageThat().contains("Authentication required");
+  }
+
+  @Test
+  public void notAllowed() throws Exception {
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.projects().name(allProjects.get()).label("Code-Review").delete());
+    assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted");
+  }
+
+  @Test
+  public void nonExisting() throws Exception {
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.projects().name(allProjects.get()).label("Non-Existing-Review").delete());
+    assertThat(thrown).hasMessageThat().contains("Not found: Non-Existing-Review");
+  }
+
+  @Test
+  public void delete() throws Exception {
+    gApi.projects().name(allProjects.get()).label("Code-Review").delete();
+
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.projects().name(project.get()).label("Code-Review").get());
+    assertThat(thrown).hasMessageThat().contains("Not found: Code-Review");
+  }
+
+  @Test
+  public void defaultCommitMessage() throws Exception {
+    gApi.projects().name(allProjects.get()).label("Code-Review").delete();
+    assertThat(
+            projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+        .isEqualTo("Delete label");
+  }
+
+  @Test
+  public void withCommitMessage() throws Exception {
+    gApi.projects().name(allProjects.get()).label("Code-Review").delete("Delete Code-Review label");
+    assertThat(
+            projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+        .isEqualTo("Delete Code-Review label");
+  }
+
+  @Test
+  public void commitMessageIsTrimmed() throws Exception {
+    gApi.projects()
+        .name(allProjects.get())
+        .label("Code-Review")
+        .delete(" Delete Code-Review label ");
+    assertThat(
+            projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+        .isEqualTo("Delete Code-Review label");
+  }
+}