Merge "Add REST endpoint to batch update label definitions"
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 13dcf64..c373b96 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -3233,6 +3233,66 @@
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
@@ -3874,9 +3934,14 @@
|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`.
@@ -3959,6 +4024,27 @@
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 6d02ec4..9873995 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.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;
@@ -221,6 +222,13 @@
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 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 0000000..eb4c581
--- /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<String> delete;
+ public List<LabelDefinitionInput> create;
+ public Map<String, LabelDefinitionInput> update;
+}
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index d7ab91b..6d7fc15 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.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.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 @@
private final Index index;
private final IndexChanges indexChanges;
private final Provider<ListLabels> listLabels;
+ private final PostLabels postLabels;
private final LabelApiImpl.Factory labelApi;
@AssistedInject
@@ -168,6 +171,7 @@
Index index,
IndexChanges indexChanges,
Provider<ListLabels> listLabels,
+ PostLabels postLabels,
LabelApiImpl.Factory labelApi,
@Assisted ProjectResource project) {
this(
@@ -205,6 +209,7 @@
index,
indexChanges,
listLabels,
+ postLabels,
labelApi,
null);
}
@@ -244,6 +249,7 @@
Index index,
IndexChanges indexChanges,
Provider<ListLabels> listLabels,
+ PostLabels postLabels,
LabelApiImpl.Factory labelApi,
@Assisted String name) {
this(
@@ -281,6 +287,7 @@
index,
indexChanges,
listLabels,
+ postLabels,
labelApi,
name);
}
@@ -320,6 +327,7 @@
Index index,
IndexChanges indexChanges,
Provider<ListLabels> listLabels,
+ PostLabels postLabels,
LabelApiImpl.Factory labelApi,
String name) {
this.permissionBackend = permissionBackend;
@@ -357,6 +365,7 @@
this.index = index;
this.indexChanges = indexChanges;
this.listLabels = listLabels;
+ this.postLabels = postLabels;
this.labelApi = labelApi;
}
@@ -712,4 +721,13 @@
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 03b9452..5d51527 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 @@
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<LabelValue> 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 @@
md.setMessage("Update label");
}
- config.getLabelSections().put(labelType.getName(), labelType);
config.commit(md);
projectCache.evict(rsrc.getProjectState().getProject());
@@ -185,4 +106,101 @@
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<LabelValue> 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 5464abf..531640c 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 @@
try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) {
ProjectConfig config = projectConfigFactory.read(md);
- if (!config.getLabelSections().containsKey(rsrc.getLabelType().getName())) {
+ if (!deleteLabel(config, 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 {
@@ -96,4 +94,20 @@
return Response.none();
}
+
+ /**
+ * Delete the given label from the given project config.
+ *
+ * @param config the project config from which the label should be deleted
+ * @param labelName the name of the label that should be deleted
+ * @return {@code true} if the label was deleted, {@code false} if the label was not found
+ */
+ public boolean deleteLabel(ProjectConfig config, String labelName) {
+ if (!config.getLabelSections().containsKey(labelName)) {
+ return false;
+ }
+
+ config.getLabelSections().remove(labelName);
+ return true;
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java
index 2c76cbd..5b3ea30 100644
--- a/java/com/google/gerrit/server/restapi/project/Module.java
+++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -72,6 +72,7 @@
get(LABEL_KIND).to(GetLabel.class);
put(LABEL_KIND).to(SetLabel.class);
delete(LABEL_KIND).to(DeleteLabel.class);
+ postOnCollection(LABEL_KIND).to(PostLabels.class);
get(PROJECT_KIND, "HEAD").to(GetHead.class);
put(PROJECT_KIND, "HEAD").to(SetHead.class);
diff --git a/java/com/google/gerrit/server/restapi/project/PostLabels.java b/java/com/google/gerrit/server/restapi/project/PostLabels.java
new file mode 100644
index 0000000..8835359
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/PostLabels.java
@@ -0,0 +1,148 @@
+// 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.common.data.LabelType;
+import com.google.gerrit.extensions.common.BatchLabelInput;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+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.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Map.Entry;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** REST endpoint that allows to add, update and delete label definitions in a batch. */
+@Singleton
+public class PostLabels
+ implements RestCollectionModifyView<ProjectResource, LabelResource, BatchLabelInput> {
+ private final Provider<CurrentUser> 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<CurrentUser> 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<String, LabelDefinitionInput> 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 b7cffce..824b4ed 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 @@
LabelType labelType = rsrc.getLabelType();
try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) {
- boolean dirty = false;
-
ProjectConfig config = projectConfigFactory.read(md);
- config.getLabelSections().remove(labelType.getName());
- if (input.name != null) {
- String newName = input.name.trim();
- if (newName.isEmpty()) {
- throw new BadRequestException("name cannot be empty");
- }
- if (!newName.equals(labelType.getName())) {
- if (config.getLabelSections().containsKey(newName)) {
- throw new ResourceConflictException(String.format("name %s already in use", newName));
- }
-
- for (String labelName : config.getLabelSections().keySet()) {
- if (labelName.equalsIgnoreCase(newName)) {
- throw new ResourceConflictException(
- String.format("name %s conflicts with existing label %s", newName, labelName));
- }
- }
-
- try {
- labelType.setName(newName);
- } catch (IllegalArgumentException e) {
- throw new BadRequestException("invalid name: " + input.name, e);
- }
- dirty = true;
- }
- }
-
- if (input.function != null) {
- if (input.function.trim().isEmpty()) {
- throw new BadRequestException("function cannot be empty");
- }
- labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function));
- dirty = true;
- }
-
- if (input.values != null) {
- if (input.values.isEmpty()) {
- throw new BadRequestException("values cannot be empty");
- }
- labelType.setValues(LabelDefinitionInputParser.parseValues(input.values));
- dirty = true;
- }
-
- if (input.defaultValue != null) {
- labelType.setDefaultValue(
- LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue));
- dirty = true;
- }
-
- if (input.branches != null) {
- labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches));
- dirty = true;
- }
-
- if (input.canOverride != null) {
- labelType.setCanOverride(input.canOverride);
- dirty = true;
- }
-
- if (input.copyAnyScore != null) {
- labelType.setCopyAnyScore(input.copyAnyScore);
- dirty = true;
- }
-
- if (input.copyMinScore != null) {
- labelType.setCopyMinScore(input.copyMinScore);
- dirty = true;
- }
-
- if (input.copyMaxScore != null) {
- labelType.setCopyMaxScore(input.copyMaxScore);
- dirty = true;
- }
-
- if (input.copyAllScoresIfNoChange != null) {
- labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange);
- }
-
- if (input.copyAllScoresIfNoCodeChange != null) {
- labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange);
- dirty = true;
- }
-
- if (input.copyAllScoresOnTrivialRebase != null) {
- labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase);
- dirty = true;
- }
-
- if (input.copyAllScoresOnMergeFirstParentUpdate != null) {
- labelType.setCopyAllScoresOnMergeFirstParentUpdate(
- input.copyAllScoresOnMergeFirstParentUpdate);
- dirty = true;
- }
-
- if (input.allowPostSubmit != null) {
- labelType.setAllowPostSubmit(input.allowPostSubmit);
- dirty = true;
- }
-
- if (input.ignoreSelfApproval != null) {
- labelType.setIgnoreSelfApproval(input.ignoreSelfApproval);
- dirty = true;
- }
-
- if (dirty) {
- config.getLabelSections().put(labelType.getName(), labelType);
-
+ if (updateLabel(config, labelType, input)) {
if (input.commitMessage != null) {
md.setMessage(Strings.emptyToNull(input.commitMessage.trim()));
} else {
@@ -203,4 +95,128 @@
}
return Response.ok(LabelDefinitionJson.format(rsrc.getProject().getNameKey(), labelType));
}
+
+ /**
+ * Updates the given label.
+ *
+ * @param config the project config
+ * @param labelType the label type that should be updated
+ * @param input the input that describes the label update
+ * @return whether the label type was modified
+ * @throws BadRequestException if there was invalid data in the input
+ * @throws ResourceConflictException if the update cannot be applied due to a conflict
+ */
+ public boolean updateLabel(ProjectConfig config, LabelType labelType, LabelDefinitionInput input)
+ throws BadRequestException, ResourceConflictException {
+ boolean dirty = false;
+
+ config.getLabelSections().remove(labelType.getName());
+
+ if (input.name != null) {
+ String newName = input.name.trim();
+ if (newName.isEmpty()) {
+ throw new BadRequestException("name cannot be empty");
+ }
+ if (!newName.equals(labelType.getName())) {
+ if (config.getLabelSections().containsKey(newName)) {
+ throw new ResourceConflictException(String.format("name %s already in use", newName));
+ }
+
+ for (String labelName : config.getLabelSections().keySet()) {
+ if (labelName.equalsIgnoreCase(newName)) {
+ throw new ResourceConflictException(
+ String.format("name %s conflicts with existing label %s", newName, labelName));
+ }
+ }
+
+ try {
+ labelType.setName(newName);
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException("invalid name: " + input.name, e);
+ }
+ dirty = true;
+ }
+ }
+
+ if (input.function != null) {
+ if (input.function.trim().isEmpty()) {
+ throw new BadRequestException("function cannot be empty");
+ }
+ labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function));
+ dirty = true;
+ }
+
+ if (input.values != null) {
+ if (input.values.isEmpty()) {
+ throw new BadRequestException("values cannot be empty");
+ }
+ labelType.setValues(LabelDefinitionInputParser.parseValues(input.values));
+ dirty = true;
+ }
+
+ if (input.defaultValue != null) {
+ labelType.setDefaultValue(
+ LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue));
+ dirty = true;
+ }
+
+ if (input.branches != null) {
+ labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches));
+ dirty = true;
+ }
+
+ if (input.canOverride != null) {
+ labelType.setCanOverride(input.canOverride);
+ dirty = true;
+ }
+
+ if (input.copyAnyScore != null) {
+ labelType.setCopyAnyScore(input.copyAnyScore);
+ dirty = true;
+ }
+
+ if (input.copyMinScore != null) {
+ labelType.setCopyMinScore(input.copyMinScore);
+ dirty = true;
+ }
+
+ if (input.copyMaxScore != null) {
+ labelType.setCopyMaxScore(input.copyMaxScore);
+ dirty = true;
+ }
+
+ if (input.copyAllScoresIfNoChange != null) {
+ labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange);
+ }
+
+ if (input.copyAllScoresIfNoCodeChange != null) {
+ labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange);
+ dirty = true;
+ }
+
+ if (input.copyAllScoresOnTrivialRebase != null) {
+ labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase);
+ dirty = true;
+ }
+
+ if (input.copyAllScoresOnMergeFirstParentUpdate != null) {
+ labelType.setCopyAllScoresOnMergeFirstParentUpdate(
+ input.copyAllScoresOnMergeFirstParentUpdate);
+ dirty = true;
+ }
+
+ if (input.allowPostSubmit != null) {
+ labelType.setAllowPostSubmit(input.allowPostSubmit);
+ dirty = true;
+ }
+
+ if (input.ignoreSelfApproval != null) {
+ labelType.setIgnoreSelfApproval(input.ignoreSelfApproval);
+ dirty = true;
+ }
+
+ config.getLabelSections().put(labelType.getName(), labelType);
+
+ return dirty;
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index b8ab752..55eeaf4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -83,7 +83,8 @@
.expectedResponseCode(SC_NOT_FOUND)
.build(),
RestCall.get("/projects/%s/dashboards"),
- RestCall.put("/projects/%s/labels/new-label"));
+ RestCall.put("/projects/%s/labels/new-label"),
+ RestCall.post("/projects/%s/labels/"));
/**
* Child project REST endpoints to be tested, each URL contains placeholders for the parent
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/PostLabelsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/PostLabelsIT.java
new file mode 100644
index 0000000..9e6b051
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/PostLabelsIT.java
@@ -0,0 +1,456 @@
+// 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.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+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.LabelFunction;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.common.BatchLabelInput;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.restapi.project.PostLabels;
+import com.google.inject.Inject;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+/** Tests for the {@link PostLabels} REST endpoint. */
+public class PostLabelsIT 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()).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");
+ }
+}