restapi: add an alternative CreateChange endpoint
In distributed installations, such as googlesource.com, different
tasks (running on different machines) are responsible for different
projects. This new API endpoint has the project name in the URL, so
load balancers can route requests to the task that has the data in
memory.
It might be more principled to add the whole /changes/ collection as a
child to /projects/ , but this is hard because the changes collection
is hard coded to be a a toplevel resource.
Every other REST operations on changes includes a change identifier
(eg. project~123), and can be routed based on that identifier.
Change-Id: I318603de0418e177f742684567867bf90571ac70
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index c1349aa..b70dfea 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1252,6 +1252,57 @@
}
----
+[[create-change]]
+=== Create Change for review.
+
+This endpoint is functionally equivalent to
+link:rest-api-changes.html#create-change[create change in the change
+API], but it has the project name in the URL, which is easier to route
+in sharded deployments.
+
+.Request
+----
+ POST /projects/myProject/create.change HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "subject" : "Let's support 100% Gerrit workflow direct in browser",
+ "branch" : "master",
+ "topic" : "create-change-in-browser",
+ "status" : "NEW"
+ }
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that describes
+the resulting change.
+
+.Response
+----
+ HTTP/1.1 201 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
+ "project": "myProject",
+ "branch": "master",
+ "topic": "create-change-in-browser",
+ "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941",
+ "subject": "Let's support 100% Gerrit workflow direct in browser",
+ "status": "NEW",
+ "created": "2014-05-05 07:15:44.639000000",
+ "updated": "2014-05-05 07:15:44.639000000",
+ "mergeable": true,
+ "insertions": 0,
+ "deletions": 0,
+ "_number": 4711,
+ "owner": {
+ "name": "John Doe"
+ }
+ }
+----
+
[[create-access-change]]
=== Create Access Rights Change for review.
--
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index acc6465..2986ead 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -170,16 +170,28 @@
BatchUpdate.Factory updateFactory, TopLevelResource parent, ChangeInput input)
throws IOException, InvalidChangeOperationException, RestApiException, UpdateException,
PermissionBackendException, ConfigInvalidException {
+ if (Strings.isNullOrEmpty(input.project)) {
+ throw new BadRequestException("project must be non-empty");
+ }
+
+ return execute(updateFactory, input, projectsCollection.parse(input.project));
+ }
+
+ /** Creates the changes in the given project. This is public for reuse in the project API. */
+ public Response<ChangeInfo> execute(
+ BatchUpdate.Factory updateFactory, ChangeInput input, ProjectResource projectResource)
+ throws IOException, InvalidChangeOperationException, RestApiException, UpdateException,
+ PermissionBackendException, ConfigInvalidException {
if (!user.get().isIdentifiedUser()) {
throw new AuthException("Authentication required");
}
- IdentifiedUser me = user.get().asIdentifiedUser();
- checkAndSanitizeChangeInput(input, me);
- ProjectResource projectResource = projectsCollection.parse(input.project);
ProjectState projectState = projectResource.getProjectState();
projectState.checkStatePermitsWrite();
+ IdentifiedUser me = user.get().asIdentifiedUser();
+ checkAndSanitizeChangeInput(input, me);
+
Project.NameKey project = projectResource.getNameKey();
contributorAgreements.check(project, user.get());
@@ -202,10 +214,6 @@
*/
private void checkAndSanitizeChangeInput(ChangeInput input, IdentifiedUser me)
throws RestApiException, PermissionBackendException, IOException {
- if (Strings.isNullOrEmpty(input.project)) {
- throw new BadRequestException("project must be non-empty");
- }
-
if (Strings.isNullOrEmpty(input.branch)) {
throw new BadRequestException("branch must be non-empty");
}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateChange.java b/java/com/google/gerrit/server/restapi/project/CreateChange.java
new file mode 100644
index 0000000..de11ffe
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/CreateChange.java
@@ -0,0 +1,70 @@
+// 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.exceptions.InvalidNameException;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class CreateChange extends RetryingRestModifyView<ProjectResource, ChangeInput, ChangeInfo> {
+ private final com.google.gerrit.server.restapi.change.CreateChange changeCreateChange;
+ private final Provider<CurrentUser> user;
+
+ @Inject
+ public CreateChange(
+ RetryHelper retryHelper,
+ Provider<CurrentUser> user,
+ com.google.gerrit.server.restapi.change.CreateChange changeCreateChange) {
+ super(retryHelper);
+ this.changeCreateChange = changeCreateChange;
+ this.user = user;
+ }
+
+ @Override
+ public Response<ChangeInfo> applyImpl(
+ BatchUpdate.Factory updateFactory, ProjectResource rsrc, ChangeInput input)
+ throws PermissionBackendException, IOException, ConfigInvalidException,
+ InvalidChangeOperationException, InvalidNameException, UpdateException, RestApiException {
+ if (!user.get().isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+
+ if (!Strings.isNullOrEmpty(input.project)) {
+ throw new BadRequestException("may not specify project");
+ }
+
+ input.project = rsrc.getName();
+ return changeCreateChange.execute(updateFactory, input, rsrc);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java
index de5661d..065facd 100644
--- a/java/com/google/gerrit/server/restapi/project/Module.java
+++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -77,6 +77,7 @@
child(PROJECT_KIND, "branches").to(BranchesCollection.class);
create(BRANCH_KIND).to(CreateBranch.class);
+ post(PROJECT_KIND, "create.change").to(CreateChange.class);
put(BRANCH_KIND).to(PutBranch.class);
get(BRANCH_KIND).to(GetBranch.class);
delete(BRANCH_KIND).to(DeleteBranch.class);
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index f48a603..48dc89f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -69,6 +69,7 @@
RestCall.get("/projects/%s/statistics.git"),
RestCall.post("/projects/%s/index"),
RestCall.post("/projects/%s/gc"),
+ RestCall.post("/projects/%s/create.change"),
RestCall.get("/projects/%s/children"),
RestCall.get("/projects/%s/branches"),
RestCall.post("/projects/%s/branches:delete"),
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
new file mode 100644
index 0000000..ca3707d
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
@@ -0,0 +1,46 @@
+// 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.Truth8.assertThat;
+import static com.google.gerrit.entities.RefNames.REFS_HEADS;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.common.ChangeInput;
+import org.junit.Test;
+
+public class CreateChangeIT extends AbstractDaemonTest {
+
+ // Just a basic test. The real functionality is tested under the restapi.change acceptance tests.
+ @Test
+ public void basic() throws Exception {
+ BranchInput branchInput = new BranchInput();
+ branchInput.ref = "foo";
+ assertThat(gApi.projects().name(project.get()).branches().get().stream().map(i -> i.ref))
+ .doesNotContain(REFS_HEADS + branchInput.ref);
+ RestResponse r =
+ adminRestSession.put(
+ "/projects/" + project.get() + "/branches/" + branchInput.ref, branchInput);
+ r.assertCreated();
+
+ ChangeInput input = new ChangeInput();
+ input.branch = "foo";
+ input.subject = "subject";
+ RestResponse cr = adminRestSession.post("/projects/" + project.get() + "/create.change", input);
+ cr.assertCreated();
+ }
+}