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
This commit is contained in:
@@ -1252,6 +1252,57 @@ entity is returned.
|
||||
}
|
||||
----
|
||||
|
||||
[[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.
|
||||
--
|
||||
|
||||
@@ -170,16 +170,28 @@ public class CreateChange
|
||||
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 @@ public class CreateChange
|
||||
*/
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,7 @@ public class Module extends RestApiModule {
|
||||
|
||||
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);
|
||||
|
||||
@@ -69,6 +69,7 @@ public class ProjectsRestApiBindingsIT extends AbstractDaemonTest {
|
||||
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"),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user