diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt index 74ec8d38f3..cbfaf0db6a 100644 --- a/Documentation/rest-api-projects.txt +++ b/Documentation/rest-api-projects.txt @@ -3233,6 +3233,66 @@ If a label was deleted the response is "`204 No Content`". HTTP/1.1 204 No Content ---- +[[batch-update-labels]] +=== Batch Update Labels +-- +'POST /projects/link:#project-name[\{project-name\}]/labels/' +-- + +Creates/updates/deletes multiple label definitions in this project at once. + +The calling user must have write access to the `refs/meta/config` branch of the +project. + +The updates must be specified in the request body as +link:#batch-label-input[BatchLabelInput] entity. + +The updates are processed in the following order: + +1. label deletions +2. label creations +3. label updates + +.Request +---- + POST /projects/My-Project/labels/ HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "commit_message": "Update Labels", + "delete": [ + "Old-Review", + "Unused-Review" + ], + "create": [ + { + "name": "Foo-Review", + "values": { + " 0": "No score", + "-1": "I would prefer this is not merged as is", + "-2": "This shall not be merged", + "+1": "Looks good to me, but someone else must approve", + "+2": "Looks good to me, approved" + } + ], + "update:" { + "Bar-Review": { + "function": "MaxWithBlock" + }, + "Baz-Review": { + "copy_min_score": true + } + } + } +---- + +If the label updates were done successfully the response is "`200 OK`". + +.Response +---- + HTTP/1.1 200 OK +---- + [[ids]] == IDs @@ -3876,9 +3936,14 @@ review label]. |Field Name ||Description |`commit_message`|optional| Message that should be used to commit the change of the label in the -`project.config` file to the `refs/meta/config` branch. +`project.config` file to the `refs/meta/config` branch.+ +Must not be set if this `LabelDefinitionInput` entity is contained in a +link:#batch-label-input[BatchLabelInput] entity. |`name` |optional| -The new link:config-labels.html#label_name[name] of the label. +The new link:config-labels.html#label_name[name] of the label.+ +For label creation the name is required if this `LabelDefinitionInput` entity +is contained in a link:#batch-label-input[BatchLabelInput] +entity. |`function` |optional| The new link:config-labels.html#label_function[function] of the label (can be `MaxWithBlock`, `AnyWithBlock`, `MaxNoBlock`, `NoBlock`, `NoOp` and `PatchSetLock`. @@ -3961,6 +4026,27 @@ the parent project or global config. + Not set if not inherited or overridden. |=============================== +[[batch-label-input]] +=== BatchLabelInput +The `BatchLabelInput` entity contains information for batch updating label +definitions in a project. + +[options="header",cols="1,^2,4"] +|============================= +|Field Name ||Description +|`commit_message`|optional| +Message that should be used to commit the label updates in the +`project.config` file to the `refs/meta/config` branch. +|`delete` |optional| +List of labels that should be deleted. +|`create` |optional| +List of link:#label-definition-input[LabelDefinitionInput] entities that +describe labels that should be created. +|`update` |optional| +Map of label names to link:#label-definition-input[LabelDefinitionInput] +entities that describe the updates that should be done for the labels. +|============================= + [[project-access-input]] === ProjectAccessInput The `ProjectAccessInput` describes changes that should be applied to a project diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java index 6d02ec4e0e..987399501c 100644 --- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java +++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java @@ -18,6 +18,7 @@ import com.google.gerrit.extensions.api.access.ProjectAccessInfo; import com.google.gerrit.extensions.api.access.ProjectAccessInput; import com.google.gerrit.extensions.api.config.AccessCheckInfo; import com.google.gerrit.extensions.api.config.AccessCheckInput; +import com.google.gerrit.extensions.common.BatchLabelInput; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.LabelDefinitionInfo; import com.google.gerrit.extensions.common.ProjectInfo; @@ -220,6 +221,13 @@ public interface ProjectApi { LabelApi label(String labelName) throws RestApiException; + /** + * Adds, updates and deletes label definitions in a batch. + * + * @param input input that describes additions, updates and deletions of label definitions + */ + void labels(BatchLabelInput input) throws RestApiException; + /** * A default implementation which allows source compatibility when adding new methods to the * interface. @@ -404,5 +412,10 @@ public interface ProjectApi { public LabelApi label(String labelName) throws RestApiException { throw new NotImplementedException(); } + + @Override + public void labels(BatchLabelInput input) throws RestApiException { + throw new NotImplementedException(); + } } } diff --git a/java/com/google/gerrit/extensions/common/BatchLabelInput.java b/java/com/google/gerrit/extensions/common/BatchLabelInput.java new file mode 100644 index 0000000000..eb4c581134 --- /dev/null +++ b/java/com/google/gerrit/extensions/common/BatchLabelInput.java @@ -0,0 +1,26 @@ +// 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 java.util.List; +import java.util.Map; + +/** Input for the REST API that describes additions, updates and deletions of label definitions. */ +public class BatchLabelInput { + public String commitMessage; + public List delete; + public List create; + public Map update; +} diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java index d7ab91b71c..6d7fc15d40 100644 --- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java +++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java @@ -43,6 +43,7 @@ import com.google.gerrit.extensions.api.projects.ProjectApi; import com.google.gerrit.extensions.api.projects.ProjectInput; import com.google.gerrit.extensions.api.projects.TagApi; import com.google.gerrit.extensions.api.projects.TagInfo; +import com.google.gerrit.extensions.common.BatchLabelInput; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.Input; import com.google.gerrit.extensions.common.LabelDefinitionInfo; @@ -77,6 +78,7 @@ import com.google.gerrit.server.restapi.project.ListBranches; import com.google.gerrit.server.restapi.project.ListDashboards; import com.google.gerrit.server.restapi.project.ListLabels; import com.google.gerrit.server.restapi.project.ListTags; +import com.google.gerrit.server.restapi.project.PostLabels; import com.google.gerrit.server.restapi.project.ProjectsCollection; import com.google.gerrit.server.restapi.project.PutConfig; import com.google.gerrit.server.restapi.project.PutDescription; @@ -131,6 +133,7 @@ public class ProjectApiImpl implements ProjectApi { private final Index index; private final IndexChanges indexChanges; private final Provider listLabels; + private final PostLabels postLabels; private final LabelApiImpl.Factory labelApi; @AssistedInject @@ -168,6 +171,7 @@ public class ProjectApiImpl implements ProjectApi { Index index, IndexChanges indexChanges, Provider listLabels, + PostLabels postLabels, LabelApiImpl.Factory labelApi, @Assisted ProjectResource project) { this( @@ -205,6 +209,7 @@ public class ProjectApiImpl implements ProjectApi { index, indexChanges, listLabels, + postLabels, labelApi, null); } @@ -244,6 +249,7 @@ public class ProjectApiImpl implements ProjectApi { Index index, IndexChanges indexChanges, Provider listLabels, + PostLabels postLabels, LabelApiImpl.Factory labelApi, @Assisted String name) { this( @@ -281,6 +287,7 @@ public class ProjectApiImpl implements ProjectApi { index, indexChanges, listLabels, + postLabels, labelApi, name); } @@ -320,6 +327,7 @@ public class ProjectApiImpl implements ProjectApi { Index index, IndexChanges indexChanges, Provider listLabels, + PostLabels postLabels, LabelApiImpl.Factory labelApi, String name) { this.permissionBackend = permissionBackend; @@ -357,6 +365,7 @@ public class ProjectApiImpl implements ProjectApi { this.index = index; this.indexChanges = indexChanges; this.listLabels = listLabels; + this.postLabels = postLabels; this.labelApi = labelApi; } @@ -712,4 +721,13 @@ public class ProjectApiImpl implements ProjectApi { throw asRestApiException("Cannot parse label", e); } } + + @Override + public void labels(BatchLabelInput input) throws RestApiException { + try { + postLabels.apply(checkExists(), input); + } catch (Exception e) { + throw asRestApiException("Cannot update labels", e); + } + } } diff --git a/java/com/google/gerrit/server/restapi/project/CreateLabel.java b/java/com/google/gerrit/server/restapi/project/CreateLabel.java index 03b9452951..5d5152791b 100644 --- a/java/com/google/gerrit/server/restapi/project/CreateLabel.java +++ b/java/com/google/gerrit/server/restapi/project/CreateLabel.java @@ -91,85 +91,7 @@ public class CreateLabel try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) { ProjectConfig config = projectConfigFactory.read(md); - if (config.getLabelSections().containsKey(id.get())) { - throw new ResourceConflictException(String.format("label %s already exists", id.get())); - } - - for (String labelName : config.getLabelSections().keySet()) { - if (labelName.equalsIgnoreCase(id.get())) { - throw new ResourceConflictException( - String.format("label %s conflicts with existing label %s", id.get(), labelName)); - } - } - - if (input.values == null || input.values.isEmpty()) { - throw new BadRequestException("values are required"); - } - - List values = LabelDefinitionInputParser.parseValues(input.values); - - LabelType labelType; - try { - labelType = new LabelType(id.get(), values); - } catch (IllegalArgumentException e) { - throw new BadRequestException("invalid name: " + id.get(), e); - } - - if (input.function != null && !input.function.trim().isEmpty()) { - labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function)); - } else { - labelType.setFunction(LabelFunction.MAX_WITH_BLOCK); - } - - if (input.defaultValue != null) { - labelType.setDefaultValue( - LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue)); - } - - if (input.branches != null) { - labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches)); - } - - if (input.canOverride != null) { - labelType.setCanOverride(input.canOverride); - } - - if (input.copyAnyScore != null) { - labelType.setCopyAnyScore(input.copyAnyScore); - } - - if (input.copyMinScore != null) { - labelType.setCopyMinScore(input.copyMinScore); - } - - if (input.copyMaxScore != null) { - labelType.setCopyMaxScore(input.copyMaxScore); - } - - if (input.copyAllScoresIfNoChange != null) { - labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange); - } - - if (input.copyAllScoresIfNoCodeChange != null) { - labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange); - } - - if (input.copyAllScoresOnTrivialRebase != null) { - labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase); - } - - if (input.copyAllScoresOnMergeFirstParentUpdate != null) { - labelType.setCopyAllScoresOnMergeFirstParentUpdate( - input.copyAllScoresOnMergeFirstParentUpdate); - } - - if (input.allowPostSubmit != null) { - labelType.setAllowPostSubmit(input.allowPostSubmit); - } - - if (input.ignoreSelfApproval != null) { - labelType.setIgnoreSelfApproval(input.ignoreSelfApproval); - } + LabelType labelType = createLabel(config, id.get(), input); if (input.commitMessage != null) { md.setMessage(Strings.emptyToNull(input.commitMessage.trim())); @@ -177,7 +99,6 @@ public class CreateLabel md.setMessage("Update label"); } - config.getLabelSections().put(labelType.getName(), labelType); config.commit(md); projectCache.evict(rsrc.getProjectState().getProject()); @@ -185,4 +106,101 @@ public class CreateLabel return Response.created(LabelDefinitionJson.format(rsrc.getNameKey(), labelType)); } } + + /** + * Creates a new label. + * + * @param config the project config + * @param label the name of the new label + * @param input the input that describes the new label + * @return the created label type + * @throws BadRequestException if there was invalid data in the input + * @throws ResourceConflictException if the label cannot be created due to a conflict + */ + public LabelType createLabel(ProjectConfig config, String label, LabelDefinitionInput input) + throws BadRequestException, ResourceConflictException { + if (config.getLabelSections().containsKey(label)) { + throw new ResourceConflictException(String.format("label %s already exists", label)); + } + + for (String labelName : config.getLabelSections().keySet()) { + if (labelName.equalsIgnoreCase(label)) { + throw new ResourceConflictException( + String.format("label %s conflicts with existing label %s", label, labelName)); + } + } + + if (input.values == null || input.values.isEmpty()) { + throw new BadRequestException("values are required"); + } + + List values = LabelDefinitionInputParser.parseValues(input.values); + + LabelType labelType; + try { + labelType = new LabelType(label, values); + } catch (IllegalArgumentException e) { + throw new BadRequestException("invalid name: " + label, e); + } + + if (input.function != null && !input.function.trim().isEmpty()) { + labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function)); + } else { + labelType.setFunction(LabelFunction.MAX_WITH_BLOCK); + } + + if (input.defaultValue != null) { + labelType.setDefaultValue( + LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue)); + } + + if (input.branches != null) { + labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches)); + } + + if (input.canOverride != null) { + labelType.setCanOverride(input.canOverride); + } + + if (input.copyAnyScore != null) { + labelType.setCopyAnyScore(input.copyAnyScore); + } + + if (input.copyMinScore != null) { + labelType.setCopyMinScore(input.copyMinScore); + } + + if (input.copyMaxScore != null) { + labelType.setCopyMaxScore(input.copyMaxScore); + } + + if (input.copyAllScoresIfNoChange != null) { + labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange); + } + + if (input.copyAllScoresIfNoCodeChange != null) { + labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange); + } + + if (input.copyAllScoresOnTrivialRebase != null) { + labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase); + } + + if (input.copyAllScoresOnMergeFirstParentUpdate != null) { + labelType.setCopyAllScoresOnMergeFirstParentUpdate( + input.copyAllScoresOnMergeFirstParentUpdate); + } + + if (input.allowPostSubmit != null) { + labelType.setAllowPostSubmit(input.allowPostSubmit); + } + + if (input.ignoreSelfApproval != null) { + labelType.setIgnoreSelfApproval(input.ignoreSelfApproval); + } + + config.getLabelSections().put(labelType.getName(), labelType); + + return labelType; + } } diff --git a/java/com/google/gerrit/server/restapi/project/DeleteLabel.java b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java index 5464abfdd7..531640cf38 100644 --- a/java/com/google/gerrit/server/restapi/project/DeleteLabel.java +++ b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java @@ -77,12 +77,10 @@ public class DeleteLabel implements RestModifyView { + private final Provider user; + private final PermissionBackend permissionBackend; + private final MetaDataUpdate.User updateFactory; + private final ProjectConfig.Factory projectConfigFactory; + private final DeleteLabel deleteLabel; + private final CreateLabel createLabel; + private final SetLabel setLabel; + private final ProjectCache projectCache; + + @Inject + public PostLabels( + Provider user, + PermissionBackend permissionBackend, + MetaDataUpdate.User updateFactory, + ProjectConfig.Factory projectConfigFactory, + DeleteLabel deleteLabel, + CreateLabel createLabel, + SetLabel setLabel, + ProjectCache projectCache) { + this.user = user; + this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; + this.projectConfigFactory = projectConfigFactory; + this.deleteLabel = deleteLabel; + this.createLabel = createLabel; + this.setLabel = setLabel; + this.projectCache = projectCache; + } + + @Override + public Response apply(ProjectResource rsrc, BatchLabelInput input) + throws AuthException, UnprocessableEntityException, PermissionBackendException, IOException, + ConfigInvalidException, BadRequestException, ResourceConflictException { + if (!user.get().isIdentifiedUser()) { + throw new AuthException("Authentication required"); + } + + permissionBackend + .currentUser() + .project(rsrc.getNameKey()) + .check(ProjectPermission.WRITE_CONFIG); + + if (input == null) { + input = new BatchLabelInput(); + } + + try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) { + boolean dirty = false; + + ProjectConfig config = projectConfigFactory.read(md); + + if (input.delete != null && !input.delete.isEmpty()) { + for (String labelName : input.delete) { + if (!deleteLabel.deleteLabel(config, labelName.trim())) { + throw new UnprocessableEntityException(String.format("label %s not found", labelName)); + } + } + dirty = true; + } + + if (input.create != null && !input.create.isEmpty()) { + for (LabelDefinitionInput labelInput : input.create) { + if (labelInput.name == null || labelInput.name.trim().isEmpty()) { + throw new BadRequestException("label name is required for new label"); + } + if (labelInput.commitMessage != null) { + throw new BadRequestException("commit message on label definition input not supported"); + } + createLabel.createLabel(config, labelInput.name.trim(), labelInput); + } + dirty = true; + } + + if (input.update != null && !input.update.isEmpty()) { + for (Entry e : input.update.entrySet()) { + LabelType labelType = config.getLabelSections().get(e.getKey().trim()); + if (labelType == null) { + throw new UnprocessableEntityException(String.format("label %s not found", e.getKey())); + } + if (e.getValue().commitMessage != null) { + throw new BadRequestException("commit message on label definition input not supported"); + } + setLabel.updateLabel(config, labelType, e.getValue()); + } + dirty = true; + } + + if (input.commitMessage != null) { + md.setMessage(Strings.emptyToNull(input.commitMessage.trim())); + } else { + md.setMessage("Update labels"); + } + + if (dirty) { + config.commit(md); + projectCache.evict(rsrc.getProjectState().getProject()); + } + } + + return Response.ok(""); + } +} diff --git a/java/com/google/gerrit/server/restapi/project/SetLabel.java b/java/com/google/gerrit/server/restapi/project/SetLabel.java index b7cffce393..824b4edaa1 100644 --- a/java/com/google/gerrit/server/restapi/project/SetLabel.java +++ b/java/com/google/gerrit/server/restapi/project/SetLabel.java @@ -80,117 +80,9 @@ public class SetLabel implements RestModifyView gApi.projects().name(allProjects.get()).labels(new BatchLabelInput())); + 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()).labels(new BatchLabelInput())); + assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted"); + } + + @Test + public void deleteNonExistingLabel() throws Exception { + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Foo"); + + UnprocessableEntityException thrown = + assertThrows( + UnprocessableEntityException.class, + () -> gApi.projects().name(allProjects.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Foo not found"); + } + + @Test + public void deleteLabels() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + configLabel("Bar", LabelFunction.NO_OP); + assertThat(gApi.projects().name(project.get()).labels().get()).isNotEmpty(); + + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Foo", "Bar"); + gApi.projects().name(project.get()).labels(input); + assertThat(gApi.projects().name(project.get()).labels().get()).isEmpty(); + } + + @Test + public void deleteLabels_labelNamesAreTrimmed() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + configLabel("Bar", LabelFunction.NO_OP); + assertThat(gApi.projects().name(project.get()).labels().get()).isNotEmpty(); + + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of(" Foo ", " Bar "); + gApi.projects().name(project.get()).labels(input); + assertThat(gApi.projects().name(project.get()).labels().get()).isEmpty(); + } + + @Test + public void cannotDeleteTheSameLabelTwice() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Foo", "Foo"); + + UnprocessableEntityException thrown = + assertThrows( + UnprocessableEntityException.class, + () -> gApi.projects().name(project.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Foo not found"); + } + + @Test + public void cannotCreateLabelWithNameThatIsAlreadyInUse() throws Exception { + LabelDefinitionInput labelInput = new LabelDefinitionInput(); + labelInput.name = "Code-Review"; + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(labelInput); + + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> gApi.projects().name(allProjects.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Code-Review already exists"); + } + + @Test + public void cannotCreateTwoLabelsWithTheSameName() throws Exception { + LabelDefinitionInput fooInput = new LabelDefinitionInput(); + fooInput.name = "Foo"; + fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(fooInput, fooInput); + + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> gApi.projects().name(project.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Foo already exists"); + } + + @Test + public void cannotCreateTwoLabelsWithNamesThatAreTheSameAfterTrim() throws Exception { + LabelDefinitionInput foo1Input = new LabelDefinitionInput(); + foo1Input.name = "Foo"; + foo1Input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInput foo2Input = new LabelDefinitionInput(); + foo2Input.name = " Foo "; + foo2Input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(foo1Input, foo2Input); + + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> gApi.projects().name(project.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Foo already exists"); + } + + @Test + public void cannotCreateTwoLabelsWithConflictingNames() throws Exception { + LabelDefinitionInput foo1Input = new LabelDefinitionInput(); + foo1Input.name = "Foo"; + foo1Input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInput foo2Input = new LabelDefinitionInput(); + foo2Input.name = "foo"; + foo2Input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(foo1Input, foo2Input); + + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> gApi.projects().name(project.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label foo conflicts with existing label Foo"); + } + + @Test + public void createLabels() throws Exception { + LabelDefinitionInput fooInput = new LabelDefinitionInput(); + fooInput.name = "Foo"; + fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInput barInput = new LabelDefinitionInput(); + barInput.name = "Bar"; + barInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(fooInput, barInput); + + gApi.projects().name(allProjects.get()).labels(input); + assertThat(gApi.projects().name(allProjects.get()).label("Foo").get()).isNotNull(); + assertThat(gApi.projects().name(allProjects.get()).label("Bar").get()).isNotNull(); + } + + @Test + public void createLabels_labelNamesAreTrimmed() throws Exception { + LabelDefinitionInput fooInput = new LabelDefinitionInput(); + fooInput.name = " Foo "; + fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInput barInput = new LabelDefinitionInput(); + barInput.name = " Bar "; + barInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(fooInput, barInput); + + gApi.projects().name(allProjects.get()).labels(input); + assertThat(gApi.projects().name(allProjects.get()).label("Foo").get()).isNotNull(); + assertThat(gApi.projects().name(allProjects.get()).label("Bar").get()).isNotNull(); + } + + @Test + public void cannotCreateLabelWithoutName() throws Exception { + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(new LabelDefinitionInput()); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, () -> gApi.projects().name(allProjects.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label name is required for new label"); + } + + @Test + public void cannotSetCommitMessageOnLabelDefinitionInputForCreate() throws Exception { + LabelDefinitionInput labelInput = new LabelDefinitionInput(); + labelInput.name = "Foo"; + labelInput.commitMessage = "Create Label Foo"; + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(labelInput); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, () -> gApi.projects().name(allProjects.get()).labels(input)); + assertThat(thrown) + .hasMessageThat() + .contains("commit message on label definition input not supported"); + } + + @Test + public void updateNonExistingLabel() throws Exception { + BatchLabelInput input = new BatchLabelInput(); + input.update = ImmutableMap.of("Foo", new LabelDefinitionInput()); + + UnprocessableEntityException thrown = + assertThrows( + UnprocessableEntityException.class, + () -> gApi.projects().name(allProjects.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Foo not found"); + } + + @Test + public void updateLabels() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + configLabel("Bar", LabelFunction.NO_OP); + + LabelDefinitionInput fooUpdate = new LabelDefinitionInput(); + fooUpdate.function = LabelFunction.MAX_WITH_BLOCK.getFunctionName(); + LabelDefinitionInput barUpdate = new LabelDefinitionInput(); + barUpdate.name = "Baz"; + + BatchLabelInput input = new BatchLabelInput(); + input.update = ImmutableMap.of("Foo", fooUpdate, "Bar", barUpdate); + + gApi.projects().name(project.get()).labels(input); + + assertThat(gApi.projects().name(project.get()).label("Foo").get().function) + .isEqualTo(fooUpdate.function); + assertThat(gApi.projects().name(project.get()).label("Baz").get()).isNotNull(); + assertThrows( + ResourceNotFoundException.class, + () -> gApi.projects().name(project.get()).label("Bar").get()); + } + + @Test + public void updateLabels_labelNamesAreTrimmed() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + configLabel("Bar", LabelFunction.NO_OP); + + LabelDefinitionInput fooUpdate = new LabelDefinitionInput(); + fooUpdate.function = LabelFunction.MAX_WITH_BLOCK.getFunctionName(); + LabelDefinitionInput barUpdate = new LabelDefinitionInput(); + barUpdate.name = "Baz"; + + BatchLabelInput input = new BatchLabelInput(); + input.update = ImmutableMap.of(" Foo ", fooUpdate, " Bar ", barUpdate); + + gApi.projects().name(project.get()).labels(input); + + assertThat(gApi.projects().name(project.get()).label("Foo").get().function) + .isEqualTo(fooUpdate.function); + assertThat(gApi.projects().name(project.get()).label("Baz").get()).isNotNull(); + assertThrows( + ResourceNotFoundException.class, + () -> gApi.projects().name(project.get()).label("Bar").get()); + } + + @Test + public void cannotSetCommitMessageOnLabelDefinitionInputForUpdate() throws Exception { + LabelDefinitionInput labelInput = new LabelDefinitionInput(); + labelInput.commitMessage = "Update label"; + + BatchLabelInput input = new BatchLabelInput(); + input.update = ImmutableMap.of("Code-Review", labelInput); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, () -> gApi.projects().name(allProjects.get()).labels(input)); + assertThat(thrown) + .hasMessageThat() + .contains("commit message on label definition input not supported"); + } + + @Test + public void deleteAndRecreateLabel() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + + LabelDefinitionInput fooInput = new LabelDefinitionInput(); + fooInput.name = "Foo"; + fooInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName(); + fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Foo"); + input.create = ImmutableList.of(fooInput); + + gApi.projects().name(project.get()).labels(input); + + LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("Foo").get(); + assertThat(fooLabel.function).isEqualTo(fooInput.function); + } + + @Test + public void deleteRecreateAndUpdateLabel() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + + LabelDefinitionInput fooCreateInput = new LabelDefinitionInput(); + fooCreateInput.name = "Foo"; + fooCreateInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName(); + fooCreateInput.values = + ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInput fooUpdateInput = new LabelDefinitionInput(); + fooUpdateInput.function = LabelFunction.ANY_WITH_BLOCK.getFunctionName(); + + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Foo"); + input.create = ImmutableList.of(fooCreateInput); + input.update = ImmutableMap.of("Foo", fooUpdateInput); + + gApi.projects().name(project.get()).labels(input); + + LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("Foo").get(); + assertThat(fooLabel.function).isEqualTo(fooUpdateInput.function); + } + + @Test + public void cannotDeleteAndUpdateLabel() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + + LabelDefinitionInput fooInput = new LabelDefinitionInput(); + fooInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName(); + + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Foo"); + input.update = ImmutableMap.of("Foo", fooInput); + + UnprocessableEntityException thrown = + assertThrows( + UnprocessableEntityException.class, + () -> gApi.projects().name(project.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Foo not found"); + } + + @Test + public void createAndUpdateLabel() throws Exception { + LabelDefinitionInput fooCreateInput = new LabelDefinitionInput(); + fooCreateInput.name = "Foo"; + fooCreateInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName(); + fooCreateInput.values = + ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInput fooUpdateInput = new LabelDefinitionInput(); + fooUpdateInput.function = LabelFunction.ANY_WITH_BLOCK.getFunctionName(); + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(fooCreateInput); + input.update = ImmutableMap.of("Foo", fooUpdateInput); + + gApi.projects().name(project.get()).labels(input); + + LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("Foo").get(); + assertThat(fooLabel.function).isEqualTo(fooUpdateInput.function); + } + + @Test + public void noOpUpdate() throws Exception { + RevCommit refsMetaConfigHead = + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG); + + gApi.projects().name(allProjects.get()).labels(new BatchLabelInput()); + + assertThat(projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG)) + .isEqualTo(refsMetaConfigHead); + } + + @Test + public void defaultCommitMessage() throws Exception { + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Code-Review"); + gApi.projects().name(allProjects.get()).labels(input); + assertThat( + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo("Update labels"); + } + + @Test + public void withCommitMessage() throws Exception { + BatchLabelInput input = new BatchLabelInput(); + input.commitMessage = "Batch Update Labels"; + input.delete = ImmutableList.of("Code-Review"); + gApi.projects().name(allProjects.get()).labels(input); + assertThat( + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo(input.commitMessage); + } + + @Test + public void commitMessageIsTrimmed() throws Exception { + BatchLabelInput input = new BatchLabelInput(); + input.commitMessage = " Batch Update Labels "; + input.delete = ImmutableList.of("Code-Review"); + gApi.projects().name(allProjects.get()).labels(input); + assertThat( + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo("Batch Update Labels"); + } +}