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>
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 57d6290..88b4577 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -436,6 +436,7 @@
)]}'
{
"kind": "gerritcodereview#project_config",
+ "description": "demo project",
"use_contributor_agreements": {
"value": true,
"configured_value": "TRUE",
@@ -467,6 +468,77 @@
}
----
+[[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 @@
[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 @@
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
~~~~~~~~~~~~~
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java
index eb6d811..aa503c1 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java
@@ -29,4 +29,9 @@
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);
+ }
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
index 0e358ec..76942e6 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
@@ -28,6 +28,11 @@
}
/** @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());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
new file mode 100644
index 0000000..bc8d5f0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
@@ -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;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
index 9193fd9..cac9cdb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
@@ -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 @@
@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);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index 82b016c..0ef875e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -65,5 +65,6 @@
install(new FactoryModuleBuilder().build(CreateProject.Factory.class));
get(PROJECT_KIND, "config").to(GetConfig.class);
+ put(PROJECT_KIND, "config").to(PutConfig.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
new file mode 100644
index 0000000..d7cad5d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -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();
+ }
+ }
+}