diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java new file mode 100644 index 0000000000..856eefed4a --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java @@ -0,0 +1,153 @@ +// Copyright (C) 2016 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.rest.project.BranchAssert.assertRefNames; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.extensions.api.projects.BranchInput; +import com.google.gerrit.extensions.api.projects.DeleteBranchesInput; +import com.google.gerrit.extensions.api.projects.ProjectApi; +import com.google.gerrit.extensions.restapi.ResourceConflictException; + +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; + +@NoHttpd +public class DeleteBranchesIT extends AbstractDaemonTest { + private static final List BRANCHES = ImmutableList.of( + "refs/heads/test-1", "refs/heads/test-2", "refs/heads/test-3"); + + @Before + public void setUp() throws Exception { + for (String name : BRANCHES) { + project().branch(name).create(new BranchInput()); + } + assertBranches(BRANCHES); + } + + @Test + public void deleteBranches() throws Exception { + HashMap initialRevisions = initialRevisions(BRANCHES); + DeleteBranchesInput input = new DeleteBranchesInput(); + input.branches = BRANCHES; + project().deleteBranches(input); + assertBranchesDeleted(); + assertRefUpdatedEvents(initialRevisions); + } + + @Test + public void deleteBranchesForbidden() throws Exception { + DeleteBranchesInput input = new DeleteBranchesInput(); + input.branches = BRANCHES; + setApiUser(user); + try { + project().deleteBranches(input); + fail("Expected ResourceConflictException"); + } catch (ResourceConflictException e) { + assertThat(e).hasMessage(errorMessageForBranches(BRANCHES)); + } + setApiUser(admin); + assertBranches(BRANCHES); + } + + @Test + public void deleteBranchesNotFound() throws Exception { + DeleteBranchesInput input = new DeleteBranchesInput(); + List branches = Lists.newArrayList(BRANCHES); + branches.add("refs/heads/does-not-exist"); + input.branches = branches; + try { + project().deleteBranches(input); + fail("Expected ResourceConflictException"); + } catch (ResourceConflictException e) { + assertThat(e).hasMessage(errorMessageForBranches( + ImmutableList.of("refs/heads/does-not-exist"))); + } + assertBranchesDeleted(); + } + + @Test + public void deleteBranchesNotFoundContinue() throws Exception { + // If it fails on the first branch in the input, it should still + // continue to process the remaining branches. + DeleteBranchesInput input = new DeleteBranchesInput(); + List branches = Lists.newArrayList("refs/heads/does-not-exist"); + branches.addAll(BRANCHES); + input.branches = branches; + try { + project().deleteBranches(input); + fail("Expected ResourceConflictException"); + } catch (ResourceConflictException e) { + assertThat(e).hasMessage(errorMessageForBranches( + ImmutableList.of("refs/heads/does-not-exist"))); + } + assertBranchesDeleted(); + } + + private String errorMessageForBranches(List branches) { + StringBuilder message = new StringBuilder(); + for (String branch : branches) { + message.append("Cannot delete ") + .append(branch) + .append(": it doesn't exist or you do not have permission ") + .append("to delete it\n"); + } + return message.toString(); + } + + private HashMap initialRevisions(List branches) + throws Exception { + HashMap result = new HashMap<>(); + for (String branch : branches) { + result.put(branch, getRemoteHead(project, branch)); + } + return result; + } + + private void assertRefUpdatedEvents(HashMap revisions) + throws Exception { + for (String branch : revisions.keySet()) { + RevCommit revision = revisions.get(branch); + eventRecorder.assertRefUpdatedEvents(project.get(), branch, + null, revision, + revision, null); + } + } + + private ProjectApi project() throws Exception { + return gApi.projects().name(project.get()); + } + + private void assertBranches(List branches) throws Exception { + List expected = Lists.newArrayList( + "HEAD", "refs/meta/config", "refs/heads/master"); + expected.addAll(branches); + assertRefNames(expected, project().branches().get()); + } + + private void assertBranchesDeleted() throws Exception { + assertBranches(ImmutableList.of()); + } +} diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DeleteBranchesInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DeleteBranchesInput.java new file mode 100644 index 0000000000..e8108a5a9e --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DeleteBranchesInput.java @@ -0,0 +1,21 @@ +// Copyright (C) 2016 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.api.projects; + +import java.util.List; + +public class DeleteBranchesInput { + public List branches; +} \ No newline at end of file diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java index b60d33b818..e111291c78 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java @@ -39,6 +39,8 @@ public interface ProjectApi { ListRefsRequest branches(); ListRefsRequest tags(); + void deleteBranches(DeleteBranchesInput in) throws RestApiException; + abstract class ListRefsRequest { protected int limit; protected int start; @@ -198,5 +200,10 @@ public interface ProjectApi { public TagApi tag(String ref) throws RestApiException { throw new NotImplementedException(); } + + @Override + public void deleteBranches(DeleteBranchesInput in) throws RestApiException { + throw new NotImplementedException(); + } } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java index 6071aeaf74..b28258c136 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java @@ -23,6 +23,7 @@ import com.google.gerrit.extensions.api.projects.BranchInfo; import com.google.gerrit.extensions.api.projects.ChildProjectApi; import com.google.gerrit.extensions.api.projects.ConfigInfo; import com.google.gerrit.extensions.api.projects.ConfigInput; +import com.google.gerrit.extensions.api.projects.DeleteBranchesInput; import com.google.gerrit.extensions.api.projects.DescriptionInput; import com.google.gerrit.extensions.api.projects.ProjectApi; import com.google.gerrit.extensions.api.projects.ProjectInput; @@ -38,6 +39,7 @@ import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.project.ChildProjectsCollection; import com.google.gerrit.server.project.CreateProject; +import com.google.gerrit.server.project.DeleteBranches; import com.google.gerrit.server.project.GetAccess; import com.google.gerrit.server.project.GetConfig; import com.google.gerrit.server.project.GetDescription; @@ -50,6 +52,7 @@ import com.google.gerrit.server.project.ProjectsCollection; import com.google.gerrit.server.project.PutConfig; import com.google.gerrit.server.project.PutDescription; import com.google.gerrit.server.project.SetAccess; +import com.google.gwtorm.server.OrmException; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; @@ -83,6 +86,7 @@ public class ProjectApiImpl implements ProjectApi { private final PutConfig putConfig; private final ListBranches listBranches; private final ListTags listTags; + private final DeleteBranches deleteBranches; @AssistedInject ProjectApiImpl(CurrentUser user, @@ -102,11 +106,12 @@ public class ProjectApiImpl implements ProjectApi { PutConfig putConfig, ListBranches listBranches, ListTags listTags, + DeleteBranches deleteBranches, @Assisted ProjectResource project) { this(user, createProjectFactory, projectApi, projects, getDescription, putDescription, childApi, children, projectJson, branchApiFactory, tagApiFactory, getAccess, setAccess, getConfig, putConfig, listBranches, - listTags, project, null); + listTags, deleteBranches, project, null); } @AssistedInject @@ -127,11 +132,12 @@ public class ProjectApiImpl implements ProjectApi { PutConfig putConfig, ListBranches listBranches, ListTags listTags, + DeleteBranches deleteBranches, @Assisted String name) { this(user, createProjectFactory, projectApi, projects, getDescription, putDescription, childApi, children, projectJson, branchApiFactory, tagApiFactory, getAccess, setAccess, getConfig, putConfig, listBranches, - listTags, null, name); + listTags, deleteBranches, null, name); } private ProjectApiImpl(CurrentUser user, @@ -151,6 +157,7 @@ public class ProjectApiImpl implements ProjectApi { PutConfig putConfig, ListBranches listBranches, ListTags listTags, + DeleteBranches deleteBranches, ProjectResource project, String name) { this.user = user; @@ -172,6 +179,7 @@ public class ProjectApiImpl implements ProjectApi { this.putConfig = putConfig; this.listBranches = listBranches; this.listTags = listTags; + this.deleteBranches = deleteBranches; } @Override @@ -328,6 +336,15 @@ public class ProjectApiImpl implements ProjectApi { return tagApi.create(checkExists(), ref); } + @Override + public void deleteBranches(DeleteBranchesInput in) throws RestApiException { + try { + deleteBranches.apply(checkExists(), in); + } catch (OrmException | IOException e) { + throw new RestApiException("Cannot delete branches", e); + } + } + private ProjectResource checkExists() throws ResourceNotFoundException { if (project == null) { throw new ResourceNotFoundException(name); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java index daecc1d0cf..f819d39272 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java @@ -18,6 +18,7 @@ import static java.lang.String.format; import com.google.common.collect.Lists; import com.google.gerrit.common.ChangeHooks; +import com.google.gerrit.extensions.api.projects.DeleteBranchesInput; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestModifyView; @@ -25,7 +26,6 @@ import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.project.DeleteBranches.Input; import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -44,26 +44,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.List; @Singleton -class DeleteBranches implements RestModifyView { +public class DeleteBranches + implements RestModifyView { private static final Logger log = LoggerFactory.getLogger(DeleteBranches.class); - static class Input { - List branches; - - static Input init(Input in) { - if (in == null) { - in = new Input(); - } - if (in.branches == null) { - in.branches = Lists.newArrayListWithCapacity(1); - } - return in; - } - } - private final Provider identifiedUser; private final GitRepositoryManager repoManager; private final Provider queryProvider; @@ -84,9 +70,16 @@ class DeleteBranches implements RestModifyView { } @Override - public Response apply(ProjectResource project, Input input) + public Response apply(ProjectResource project, DeleteBranchesInput input) throws OrmException, IOException, ResourceConflictException { - input = Input.init(input); + + if (input == null) { + input = new DeleteBranchesInput(); + } + if (input.branches == null) { + input.branches = Lists.newArrayListWithCapacity(1); + } + try (Repository r = repoManager.openRepository(project.getNameKey())) { BatchRefUpdate batchUpdate = r.getRefDatabase().newBatchUpdate(); for (String branch : input.branches) {