Add REST endpoint to update the project configuration

By PUT on /projects/*/config it is now possible to update the
configuration of a project.

We already have REST endpoints to get and set the project description
(GET and PUT on /projects/*/description), however on the
ProjectInfoScreen we have a single save button to save both the project
description and the project configuration settings. Triggering two calls
in parallel with a callback group, one to update the project description
and one to update the project configuration, is likely failing because
both requests need to update the project.config file. If it happens in
parallel one request will get a LOCK_FAILURE. Also it would be bad to
create two commits for a single save action. This is why it makes sense
to also allow to get and set the project description via the GetConfig
and PutConfig REST endpoints.

Change-Id: I2109264d75dd50d103e58e8a9e73492e2aa5807c
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin 2013-07-17 09:10:54 +02:00 committed by Edwin Kempin
parent 3e05f81f8c
commit a23eb10619
7 changed files with 384 additions and 84 deletions

View File

@ -436,6 +436,7 @@ read access to `refs/meta/config`.
)]}'
{
"kind": "gerritcodereview#project_config",
"description": "demo project",
"use_contributor_agreements": {
"value": true,
"configured_value": "TRUE",
@ -467,6 +468,77 @@ read access to `refs/meta/config`.
}
----
[[set-config]]
Set Config
~~~~~~~~~~
[verse]
'PUT /projects/link:#project-name[\{project-name\}]/config'
Sets the configuration of a project.
The new configuration must be provided in the request body as a
link:#config-input[ConfigInput] entity.
.Request
----
PUT /projects/myproject/config HTTP/1.0
Content-Type: application/json;charset=UTF-8
{
"description": "demo project",
"use_contributor_agreements": "FALSE",
"use_content_merge": "INHERIT",
"use_signed_off_by": "INHERIT",
"require_change_id": "TRUE",
"max_object_size_limit": "10m",
"submit_type": "REBASE_IF_NECESSARY",
"state": "ACTIVE"
}
----
As response the new configuration is returned as a link:#config-info[
ConfigInfo] entity.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json;charset=UTF-8
)]}'
{
"kind": "gerritcodereview#project_config",
"use_contributor_agreements": {
"value": false,
"configured_value": "FALSE",
"inherited_value": false
},
"use_content_merge": {
"value": true,
"configured_value": "INHERIT",
"inherited_value": true
},
"use_signed_off_by": {
"value": false,
"configured_value": "INHERIT",
"inherited_value": false
},
"require_change_id": {
"value": true,
"configured_value": "TRUE",
"inherited_value": true
},
"max_object_size_limit": {
"value": "10m",
"configured_value": "10m",
"inherited_value": "20m"
},
"submit_type": "REBASE_IF_NECESSARY",
"state": "ACTIVE",
"commentlinks": {}
}
----
[[run-gc]]
Run GC
~~~~~~
@ -1074,6 +1146,8 @@ configuration.
[options="header",width="50%",cols="1,^2,4"]
|=========================================
|Field Name ||Description
|`description` |optional|
The description of the project.
|`use_contributor_agreements`|optional|
link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
authors must complete a contributor agreement on the site before
@ -1115,6 +1189,57 @@ The theme that is configured for the project as a link:#theme-info[
ThemeInfo] entity.
|=========================================
[[config-input]]
ConfigInput
~~~~~~~~~~~
The `ConfigInput` entity describes a new project configuration.
[options="header",width="50%",cols="1,^2,4"]
|=========================================
|Field Name ||Description
|`description` |optional|
The new description of the project. +
If not set, the description is removed.
|`use_contributor_agreements`|optional|
Whether authors must complete a contributor agreement on the site
before pushing any commits or changes to this project. +
Can be `TRUE`, `FALSE` or `INHERIT`. +
If not set, this setting is not updated.
|`use_content_merge` |optional|
Whether Gerrit will try to perform a 3-way merge of text file content
when a file has been modified by both the destination branch and the
change being submitted. This option only takes effect if submit type is
not FAST_FORWARD_ONLY. +
Can be `TRUE`, `FALSE` or `INHERIT`. +
If not set, this setting is not updated.
|`use_signed_off_by` |optional|
Whether each change must contain a Signed-off-by line from either the
author or the uploader in the commit message. +
Can be `TRUE`, `FALSE` or `INHERIT`. +
If not set, this setting is not updated.
|`require_change_id` |optional|
Whether a valid link:user-changeid.html[Change-Id] footer in any commit
uploaded for review is required. This does not apply to commits pushed
directly to a branch or tag. +
Can be `TRUE`, `FALSE` or `INHERIT`. +
If not set, this setting is not updated.
|`max_object_size_limit` |optional|
The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
limit] of this project as a link:#max-object-size-limit-info[
MaxObjectSizeLimitInfo] entity. +
If set to `0`, the max object size limit is removed. +
If not set, this setting is not updated.
|`submit_type` |optional|
The default submit type of the project, can be `MERGE_IF_NECESSARY`,
`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
`CHERRY_PICK`. +
If not set, the submit type is not updated.
|`state` |optional|
The state of the project, can be `ACTIVE`, `READ_ONLY` or `HIDDEN`. +
Not set if the project state is `ACTIVE`. +
If not set, the project state is not updated.
|=========================================
[[dashboard-info]]
DashboardInfo
~~~~~~~~~~~~~

View File

@ -29,4 +29,9 @@ public class ResourceConflictException extends RestApiException {
public ResourceConflictException(String msg) {
super(msg);
}
/** @param msg message to return to the client describing the error. */
public ResourceConflictException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@ -27,6 +27,11 @@ public class ResourceNotFoundException extends RestApiException {
super(id);
}
/** @param id portion of the resource URI that does not exist. */
public ResourceNotFoundException(String id, Throwable cause) {
super(id, cause);
}
/** @param id portion of the resource URI that does not exist. */
public ResourceNotFoundException(IdString id) {
super(id.get());

View File

@ -0,0 +1,109 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.project;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gerrit.server.git.TransferConfig;
import java.util.Map;
public class ConfigInfo {
public final String kind = "gerritcodereview#project_config";
public String description;
public InheritedBooleanInfo useContributorAgreements;
public InheritedBooleanInfo useContentMerge;
public InheritedBooleanInfo useSignedOffBy;
public InheritedBooleanInfo requireChangeId;
public MaxObjectSizeLimitInfo maxObjectSizeLimit;
public SubmitType submitType;
public Project.State state;
public Map<String, CommentLinkInfo> commentlinks;
public ThemeInfo theme;
public ConfigInfo(ProjectState state, TransferConfig config) {
Project p = state.getProject();
this.description = Strings.emptyToNull(p.getDescription());
InheritedBooleanInfo useContributorAgreements =
new InheritedBooleanInfo();
InheritedBooleanInfo useSignedOffBy = new InheritedBooleanInfo();
InheritedBooleanInfo useContentMerge = new InheritedBooleanInfo();
InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
useContributorAgreements.value = state.isUseContributorAgreements();
useSignedOffBy.value = state.isUseSignedOffBy();
useContentMerge.value = state.isUseContentMerge();
requireChangeId.value = state.isRequireChangeID();
useContributorAgreements.configuredValue =
p.getUseContributorAgreements();
useSignedOffBy.configuredValue = p.getUseSignedOffBy();
useContentMerge.configuredValue = p.getUseContentMerge();
requireChangeId.configuredValue = p.getRequireChangeID();
ProjectState parentState = Iterables.getFirst(state.parents(), null);
if (parentState != null) {
useContributorAgreements.inheritedValue =
parentState.isUseContributorAgreements();
useSignedOffBy.inheritedValue = parentState.isUseSignedOffBy();
useContentMerge.inheritedValue = parentState.isUseContentMerge();
requireChangeId.inheritedValue = parentState.isRequireChangeID();
}
this.useContributorAgreements = useContributorAgreements;
this.useSignedOffBy = useSignedOffBy;
this.useContentMerge = useContentMerge;
this.requireChangeId = requireChangeId;
MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
maxObjectSizeLimit.value =
config.getEffectiveMaxObjectSizeLimit(state) == config
.getMaxObjectSizeLimit() ? config
.getFormattedMaxObjectSizeLimit() : p.getMaxObjectSizeLimit();
maxObjectSizeLimit.configuredValue = p.getMaxObjectSizeLimit();
maxObjectSizeLimit.inheritedValue =
config.getFormattedMaxObjectSizeLimit();
this.maxObjectSizeLimit = maxObjectSizeLimit;
this.submitType = p.getSubmitType();
this.state = p.getState() != Project.State.ACTIVE ? p.getState() : null;
this.commentlinks = Maps.newLinkedHashMap();
for (CommentLinkInfo cl : state.getCommentLinks()) {
this.commentlinks.put(cl.name, cl);
}
this.theme = state.getTheme();
}
public static class InheritedBooleanInfo {
public Boolean value;
public InheritableBoolean configuredValue;
public Boolean inheritedValue;
}
public static class MaxObjectSizeLimitInfo {
public String value;
public String configuredValue;
public String inheritedValue;
}
}

View File

@ -14,17 +14,10 @@
package com.google.gerrit.server.project;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gerrit.server.git.TransferConfig;
import com.google.inject.Inject;
import java.util.Map;
public class GetConfig implements RestReadView<ProjectResource> {
private final TransferConfig config;
@ -36,82 +29,6 @@ public class GetConfig implements RestReadView<ProjectResource> {
@Override
public ConfigInfo apply(ProjectResource resource) {
ConfigInfo result = new ConfigInfo();
ProjectState state = resource.getControl().getProjectState();
Project p = state.getProject();
InheritedBooleanInfo useContributorAgreements = new InheritedBooleanInfo();
InheritedBooleanInfo useSignedOffBy = new InheritedBooleanInfo();
InheritedBooleanInfo useContentMerge = new InheritedBooleanInfo();
InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
useContributorAgreements.value = state.isUseContributorAgreements();
useSignedOffBy.value = state.isUseSignedOffBy();
useContentMerge.value = state.isUseContentMerge();
requireChangeId.value = state.isRequireChangeID();
useContributorAgreements.configuredValue = p.getUseContributorAgreements();
useSignedOffBy.configuredValue = p.getUseSignedOffBy();
useContentMerge.configuredValue = p.getUseContentMerge();
requireChangeId.configuredValue = p.getRequireChangeID();
ProjectState parentState = Iterables.getFirst(state.parents(), null);
if (parentState != null) {
useContributorAgreements.inheritedValue = parentState.isUseContributorAgreements();
useSignedOffBy.inheritedValue = parentState.isUseSignedOffBy();
useContentMerge.inheritedValue = parentState.isUseContentMerge();
requireChangeId.inheritedValue = parentState.isRequireChangeID();
}
result.useContributorAgreements = useContributorAgreements;
result.useSignedOffBy = useSignedOffBy;
result.useContentMerge = useContentMerge;
result.requireChangeId = requireChangeId;
MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
maxObjectSizeLimit.value =
config.getEffectiveMaxObjectSizeLimit(state) == config.getMaxObjectSizeLimit()
? config.getFormattedMaxObjectSizeLimit()
: p.getMaxObjectSizeLimit();
maxObjectSizeLimit.configuredValue = p.getMaxObjectSizeLimit();
maxObjectSizeLimit.inheritedValue = config.getFormattedMaxObjectSizeLimit();
result.maxObjectSizeLimit = maxObjectSizeLimit;
result.submitType = p.getSubmitType();
result.state = p.getState() != Project.State.ACTIVE ? p.getState() : null;
result.commentlinks = Maps.newLinkedHashMap();
for (CommentLinkInfo cl : state.getCommentLinks()) {
result.commentlinks.put(cl.name, cl);
}
result.theme = state.getTheme();
return result;
}
public static class ConfigInfo {
public final String kind = "gerritcodereview#project_config";
public InheritedBooleanInfo useContributorAgreements;
public InheritedBooleanInfo useContentMerge;
public InheritedBooleanInfo useSignedOffBy;
public InheritedBooleanInfo requireChangeId;
public MaxObjectSizeLimitInfo maxObjectSizeLimit;
public SubmitType submitType;
public Project.State state;
public Map<String, CommentLinkInfo> commentlinks;
public ThemeInfo theme;
}
public static class InheritedBooleanInfo {
public Boolean value;
public InheritableBoolean configuredValue;
public Boolean inheritedValue;
}
public static class MaxObjectSizeLimitInfo {
public String value;
public String configuredValue;
public String inheritedValue;
return new ConfigInfo(resource.getControl().getProjectState(), config);
}
}

View File

@ -65,5 +65,6 @@ public class Module extends RestApiModule {
install(new FactoryModuleBuilder().build(CreateProject.Factory.class));
get(PROJECT_KIND, "config").to(GetConfig.class);
put(PROJECT_KIND, "config").to(PutConfig.class);
}
}

View File

@ -0,0 +1,138 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.project;
import com.google.common.base.Strings;
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.RestModifyView;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.project.PutConfig.Input;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import java.io.IOException;
public class PutConfig implements RestModifyView<ProjectResource, Input> {
public static class Input {
public String description;
public InheritableBoolean useContributorAgreements;
public InheritableBoolean useContentMerge;
public InheritableBoolean useSignedOffBy;
public InheritableBoolean requireChangeId;
public String maxObjectSizeLimit;
public SubmitType submitType;
public Project.State state;
}
private final MetaDataUpdate.User metaDataUpdateFactory;
private final ProjectCache projectCache;
private final Provider<CurrentUser> self;
private final ProjectState.Factory projectStateFactory;
private final TransferConfig config;
@Inject
PutConfig(MetaDataUpdate.User metaDataUpdateFactory,
ProjectCache projectCache,
Provider<CurrentUser> self,
ProjectState.Factory projectStateFactory,
TransferConfig config) {
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.projectCache = projectCache;
this.self = self;
this.projectStateFactory = projectStateFactory;
this.config = config;
}
@Override
public ConfigInfo apply(ProjectResource rsrc, Input input)
throws ResourceNotFoundException, BadRequestException,
ResourceConflictException {
Project.NameKey projectName = rsrc.getNameKey();
if (!rsrc.getControl().isOwner()) {
throw new ResourceNotFoundException(projectName.get());
}
if (input == null) {
throw new BadRequestException("config is required");
}
final MetaDataUpdate md;
try {
md = metaDataUpdateFactory.create(projectName);
} catch (RepositoryNotFoundException notFound) {
throw new ResourceNotFoundException(projectName.get());
} catch (IOException e) {
throw new ResourceNotFoundException(projectName.get(), e);
}
try {
ProjectConfig projectConfig = ProjectConfig.read(md);
Project p = projectConfig.getProject();
p.setDescription(Strings.emptyToNull(input.description));
if (input.useContributorAgreements != null) {
p.setUseContributorAgreements(input.useContributorAgreements);
}
if (input.useContentMerge != null) {
p.setUseContentMerge(input.useContentMerge);
}
if (input.useSignedOffBy != null) {
p.setUseSignedOffBy(input.useSignedOffBy);
}
if (input.requireChangeId != null) {
p.setRequireChangeID(input.requireChangeId);
}
if (input.maxObjectSizeLimit != null) {
p.setMaxObjectSizeLimit(input.maxObjectSizeLimit);
}
if (input.submitType != null) {
p.setSubmitType(input.submitType);
}
if (input.state != null) {
p.setState(input.state);
}
md.setMessage("Modified project settings\n");
try {
projectConfig.commit(md);
(new PerRequestProjectControlCache(projectCache, self.get()))
.evict(projectConfig.getProject());
} catch (IOException e) {
throw new ResourceConflictException("Cannot update " + projectName);
}
return new ConfigInfo(projectStateFactory.create(projectConfig), config);
} catch (ConfigInvalidException err) {
throw new ResourceConflictException("Cannot read project " + projectName, err);
} catch (IOException err) {
throw new ResourceConflictException("Cannot update project " + projectName, err);
} finally {
md.close();
}
}
}