Merge "Add REST API endpoint to delete multiple tags"

This commit is contained in:
Hugo Arès 2016-12-08 17:57:57 +00:00 committed by Gerrit Code Review
commit f55bf18420
8 changed files with 311 additions and 3 deletions

View File

@ -1904,6 +1904,38 @@ Deletes a tag.
HTTP/1.1 204 No Content
----
[[delete-tags]]
=== Delete Tags
--
'POST /projects/link:#project-name[\{project-name\}]/tags:delete'
--
Delete one or more tags.
The tags to be deleted must be provided in the request body as a
link:#delete-tags-input[DeleteTagsInput] entity.
.Request
----
POST /projects/MyProject/tags:delete HTTP/1.0
Content-Type: application/json;charset=UTF-8
{
"tags": [
"v1.0",
"v2.0"
]
}
----
.Response
----
HTTP/1.1 204 No Content
----
If some tags could not be deleted, the response is "`409 Conflict`" and the
error message is contained in the response body.
[[commit-endpoints]]
== Commit Endpoints
@ -2539,6 +2571,18 @@ be deleted.
deleted.
|==========================
[[delete-tags-input]]
=== DeleteTagsInput
The `DeleteTagsInput` entity contains information about tags that should
be deleted.
[options="header",width="50%",cols="1,6"]
|==========================
|Field Name |Description
|`tags` |A list of tag names that identify the tags that should be
deleted.
|==========================
[[gc-input]]
=== GCInput
The `GCInput` entity contains information to run the Git garbage

View File

@ -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 org.junit.Assert.fail;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
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.TagInput;
import com.google.gerrit.extensions.api.projects.DeleteTagsInput;
import com.google.gerrit.extensions.api.projects.ProjectApi;
import com.google.gerrit.extensions.api.projects.TagInfo;
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 DeleteTagsIT extends AbstractDaemonTest {
private static final List<String> TAGS = ImmutableList.of(
"refs/tags/test-1", "refs/tags/test-2", "refs/tags/test-3");
@Before
public void setUp() throws Exception {
for (String name : TAGS) {
project().tag(name).create(new TagInput());
}
assertTags(TAGS);
}
@Test
public void deleteTags() throws Exception {
HashMap<String, RevCommit> initialRevisions = initialRevisions(TAGS);
DeleteTagsInput input = new DeleteTagsInput();
input.tags = TAGS;
project().deleteTags(input);
assertTagsDeleted();
assertRefUpdatedEvents(initialRevisions);
}
@Test
public void deleteTagsForbidden() throws Exception {
DeleteTagsInput input = new DeleteTagsInput();
input.tags = TAGS;
setApiUser(user);
try {
project().deleteTags(input);
fail("Expected ResourceConflictException");
} catch (ResourceConflictException e) {
assertThat(e).hasMessage(errorMessageForTags(TAGS));
}
setApiUser(admin);
assertTags(TAGS);
}
@Test
public void deleteTagsNotFound() throws Exception {
DeleteTagsInput input = new DeleteTagsInput();
List<String> tags = Lists.newArrayList(TAGS);
tags.add("refs/tags/does-not-exist");
input.tags = tags;
try {
project().deleteTags(input);
fail("Expected ResourceConflictException");
} catch (ResourceConflictException e) {
assertThat(e).hasMessage(errorMessageForTags(
ImmutableList.of("refs/tags/does-not-exist")));
}
assertTagsDeleted();
}
@Test
public void deleteTagsNotFoundContinue() throws Exception {
// If it fails on the first tag in the input, it should still
// continue to process the remaining tags.
DeleteTagsInput input = new DeleteTagsInput();
List<String> tags = Lists.newArrayList("refs/tags/does-not-exist");
tags.addAll(TAGS);
input.tags = tags;
try {
project().deleteTags(input);
fail("Expected ResourceConflictException");
} catch (ResourceConflictException e) {
assertThat(e).hasMessage(errorMessageForTags(
ImmutableList.of("refs/tags/does-not-exist")));
}
assertTagsDeleted();
}
private String errorMessageForTags(List<String> tags) {
StringBuilder message = new StringBuilder();
for (String tag : tags) {
message.append("Cannot delete ")
.append(tag)
.append(": it doesn't exist or you do not have permission ")
.append("to delete it\n");
}
return message.toString();
}
private HashMap<String, RevCommit> initialRevisions(List<String> tags)
throws Exception {
HashMap<String, RevCommit> result = new HashMap<>();
for (String tag : tags) {
result.put(tag, getRemoteHead(project, tag));
}
return result;
}
private void assertRefUpdatedEvents(HashMap<String, RevCommit> revisions)
throws Exception {
for (String tag : revisions.keySet()) {
RevCommit revision = revisions.get(tag);
eventRecorder.assertRefUpdatedEvents(project.get(), tag,
null, revision,
revision, null);
}
}
private ProjectApi project() throws Exception {
return gApi.projects().name(project.get());
}
private void assertTags(List<String> expected) throws Exception {
List<TagInfo> actualTags = project().tags().get();
Iterable<String> actualNames = Iterables.transform(actualTags, b -> b.ref);
assertThat(actualNames).containsExactlyElementsIn(expected).inOrder();
}
private void assertTagsDeleted() throws Exception {
assertTags(ImmutableList.<String>of());
}
}

View File

@ -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 DeleteTagsInput {
public List<String> tags;
}

View File

@ -40,6 +40,7 @@ public interface ProjectApi {
ListRefsRequest<TagInfo> tags();
void deleteBranches(DeleteBranchesInput in) throws RestApiException;
void deleteTags(DeleteTagsInput in) throws RestApiException;
abstract class ListRefsRequest<T extends RefInfo> {
protected int limit;
@ -205,5 +206,10 @@ public interface ProjectApi {
public void deleteBranches(DeleteBranchesInput in) throws RestApiException {
throw new NotImplementedException();
}
@Override
public void deleteTags(DeleteTagsInput in) throws RestApiException {
throw new NotImplementedException();
}
}
}

View File

@ -24,6 +24,7 @@ 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.DeleteTagsInput;
import com.google.gerrit.extensions.api.projects.DescriptionInput;
import com.google.gerrit.extensions.api.projects.ProjectApi;
import com.google.gerrit.extensions.api.projects.ProjectInput;
@ -40,6 +41,7 @@ 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.DeleteTags;
import com.google.gerrit.server.project.GetAccess;
import com.google.gerrit.server.project.GetConfig;
import com.google.gerrit.server.project.GetDescription;
@ -87,6 +89,7 @@ public class ProjectApiImpl implements ProjectApi {
private final ListBranches listBranches;
private final ListTags listTags;
private final DeleteBranches deleteBranches;
private final DeleteTags deleteTags;
@AssistedInject
ProjectApiImpl(CurrentUser user,
@ -107,11 +110,12 @@ public class ProjectApiImpl implements ProjectApi {
ListBranches listBranches,
ListTags listTags,
DeleteBranches deleteBranches,
DeleteTags deleteTags,
@Assisted ProjectResource project) {
this(user, createProjectFactory, projectApi, projects, getDescription,
putDescription, childApi, children, projectJson, branchApiFactory,
tagApiFactory, getAccess, setAccess, getConfig, putConfig, listBranches,
listTags, deleteBranches, project, null);
listTags, deleteBranches, deleteTags, project, null);
}
@AssistedInject
@ -133,11 +137,12 @@ public class ProjectApiImpl implements ProjectApi {
ListBranches listBranches,
ListTags listTags,
DeleteBranches deleteBranches,
DeleteTags deleteTags,
@Assisted String name) {
this(user, createProjectFactory, projectApi, projects, getDescription,
putDescription, childApi, children, projectJson, branchApiFactory,
tagApiFactory, getAccess, setAccess, getConfig, putConfig, listBranches,
listTags, deleteBranches, null, name);
listTags, deleteBranches, deleteTags, null, name);
}
private ProjectApiImpl(CurrentUser user,
@ -158,6 +163,7 @@ public class ProjectApiImpl implements ProjectApi {
ListBranches listBranches,
ListTags listTags,
DeleteBranches deleteBranches,
DeleteTags deleteTags,
ProjectResource project,
String name) {
this.user = user;
@ -180,6 +186,7 @@ public class ProjectApiImpl implements ProjectApi {
this.listBranches = listBranches;
this.listTags = listTags;
this.deleteBranches = deleteBranches;
this.deleteTags = deleteTags;
}
@Override
@ -345,6 +352,15 @@ public class ProjectApiImpl implements ProjectApi {
}
}
@Override
public void deleteTags(DeleteTagsInput in) throws RestApiException {
try {
deleteTags.apply(checkExists(), in);
} catch (OrmException | IOException e) {
throw new RestApiException("Cannot delete tags", e);
}
}
private ProjectResource checkExists() throws ResourceNotFoundException {
if (project == null) {
throw new ResourceNotFoundException(name);

View File

@ -45,6 +45,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class DeleteRef {
private static final Logger log = LoggerFactory.getLogger(DeleteRef.class);
@ -59,6 +60,7 @@ public class DeleteRef {
private final Provider<InternalChangeQuery> queryProvider;
private final ProjectResource resource;
private final List<String> refsToDelete;
private String prefix;
public interface Factory {
public DeleteRef create(ProjectResource r);
@ -90,6 +92,11 @@ public class DeleteRef {
return this;
}
public DeleteRef prefix(String prefix) {
this.prefix = prefix;
return this;
}
public void delete()
throws OrmException, IOException, ResourceConflictException {
if (!refsToDelete.isEmpty()) {
@ -106,6 +113,9 @@ public class DeleteRef {
private void deleteSingleRef(Repository r)
throws IOException, ResourceConflictException {
String ref = refsToDelete.get(0);
if (prefix != null && !ref.startsWith(prefix)) {
ref = prefix + ref;
}
RefUpdate.Result result;
RefUpdate u = r.updateRef(ref);
u.setForceUpdate(true);
@ -160,7 +170,13 @@ public class DeleteRef {
private void deleteMultipleRefs(Repository r)
throws OrmException, IOException, ResourceConflictException {
BatchRefUpdate batchUpdate = r.getRefDatabase().newBatchUpdate();
for (String ref : refsToDelete) {
List<String> refs = prefix == null
? refsToDelete
: refsToDelete.stream().map(
ref -> ref.startsWith(prefix)
? ref
: prefix + ref).collect(Collectors.toList());
for (String ref : refs) {
batchUpdate.addCommand(createDeleteCommand(resource, r, ref));
}
try (RevWalk rw = new RevWalk(r)) {

View File

@ -0,0 +1,51 @@
// 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.server.project;
import static org.eclipse.jgit.lib.Constants.R_TAGS;
import com.google.gerrit.extensions.api.projects.DeleteTagsInput;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
@Singleton
public class DeleteTags
implements RestModifyView<ProjectResource, DeleteTagsInput> {
private final DeleteRef.Factory deleteRefFactory;
@Inject
DeleteTags(DeleteRef.Factory deleteRefFactory) {
this.deleteRefFactory = deleteRefFactory;
}
@Override
public Response<?> apply(ProjectResource project, DeleteTagsInput input)
throws OrmException, RestApiException, IOException {
if (input == null || input.tags == null || input.tags.isEmpty()) {
throw new BadRequestException("tags must be specified");
}
deleteRefFactory.create(project).refs(input.tags).prefix(R_TAGS).delete();
return Response.none();
}
}

View File

@ -82,6 +82,7 @@ public class Module extends RestApiModule {
get(TAG_KIND).to(GetTag.class);
put(TAG_KIND).to(PutTag.class);
delete(TAG_KIND).to(DeleteTag.class);
post(PROJECT_KIND, "tags:delete").to(DeleteTags.class);
factory(CreateTag.Factory.class);
child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class);