Merge "Add REST endpoint to batch update label definitions"

This commit is contained in:
Patrick Hiesel
2019-11-20 18:58:29 +00:00
committed by Gerrit Code Review
11 changed files with 992 additions and 195 deletions

View File

@@ -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
@@ -3874,9 +3934,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`.
@@ -3959,6 +4024,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

View File

@@ -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();
}
}
}

View File

@@ -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;
}

View File

@@ -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> 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> 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> 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> 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);
}
}
}

View File

@@ -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<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 @@ 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<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;
}
}

View File

@@ -77,12 +77,10 @@ public class DeleteLabel implements RestModifyView<LabelResource, InputWithCommi
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 @@ public class DeleteLabel implements RestModifyView<LabelResource, InputWithCommi
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;
}
}

View File

@@ -72,6 +72,7 @@ public class Module extends RestApiModule {
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);

View File

@@ -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("");
}
}

View File

@@ -80,117 +80,9 @@ public class SetLabel implements RestModifyView<LabelResource, LabelDefinitionIn
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 @@ public class SetLabel implements RestModifyView<LabelResource, LabelDefinitionIn
}
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;
}
}

View File

@@ -83,7 +83,8 @@ public class ProjectsRestApiBindingsIT extends AbstractDaemonTest {
.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

View File

@@ -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");
}
}