Merge "CreateChange: Allow create change on new branch"

This commit is contained in:
Edwin Kempin 2016-02-15 08:21:14 +00:00 committed by Gerrit Code Review
commit 611e6847ff
8 changed files with 143 additions and 34 deletions
Documentation
gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance
gerrit-extension-api/src/main/java/com/google/gerrit/extensions
gerrit-server/src/main/java/com/google/gerrit/server

@ -13,10 +13,8 @@ link:rest-api.html[REST API].
'POST /changes'
--
The change info link:#change-info[ChangeInfo] entity must be provided in the
request body. Only the following attributes are honored: `project`,
`branch`, `subject`, `status` and `topic`. The first three attributes are
mandatory. Valid values for status are: `DRAFT` and `NEW`.
The change input link:#change-input[ChangeInput] entity must be provided in the
request body.
.Request
----
@ -4016,9 +4014,29 @@ Only set on the last change that is returned.
|`problems` |optional|
A list of link:#problem-info[ProblemInfo] entities describing potential
problems with this change. Only set if link:#check[CHECK] is set.
|==================================
[[change-input]]
=== ChangeInput
The `ChangeInput` entity contains information about creating a new change.
[options="header",cols="1,^1,5"]
|==================================
|Field Name ||Description
|`project` ||The name of the project.
|`branch` ||
The name of the target branch. +
The `refs/heads/` prefix is omitted.
|`subject` ||
The subject of the change (header line of the commit message).
|`topic` |optional|The topic to which this change belongs.
|`status` |optional, default to `NEW`|
The status of the change (only `NEW` and `DRAFT` accepted here).
|`base_change` |optional|
A link:#change-id[\{change-id\}] that identifies the base change for a create
change operation. Only used for the link:#create-change[CreateChange] endpoint.
change operation.
|`new_branch` |optional, default to `false`|
Allow creating a new branch when set to `true`.
|==================================
[[change-message-info]]

@ -48,6 +48,7 @@ import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.LabelInfo;
@ -585,7 +586,7 @@ public class ChangeIT extends AbstractDaemonTest {
@Test
public void createEmptyChange() throws Exception {
ChangeInfo in = new ChangeInfo();
ChangeInput in = new ChangeInput();
in.branch = Constants.MASTER;
in.subject = "Create a change from the API";
in.project = project.get();
@ -981,6 +982,39 @@ public class ChangeIT extends AbstractDaemonTest {
}
}
@Test
public void createEmptyChangeOnNonExistingBranch() throws Exception {
ChangeInput in = new ChangeInput();
in.branch = "foo";
in.subject = "Create a change on new branch from the API";
in.project = project.get();
in.newBranch = true;
ChangeInfo info = gApi
.changes()
.create(in)
.get();
assertThat(info.project).isEqualTo(in.project);
assertThat(info.branch).isEqualTo(in.branch);
assertThat(info.subject).isEqualTo(in.subject);
assertThat(Iterables.getOnlyElement(info.messages).message)
.isEqualTo("Uploaded patch set 1.");
}
@Test
public void createEmptyChangeOnExistingBranchWithNewBranch() throws Exception {
ChangeInput in = new ChangeInput();
in.branch = Constants.MASTER;
in.subject = "Create a change on new branch from the API";
in.project = project.get();
in.newBranch = true;
exception.expect(ResourceConflictException.class);
ChangeInfo info = gApi
.changes()
.create(in)
.get();
}
private static Iterable<Account.Id> getReviewers(
Collection<AccountInfo> r) {
return Iterables.transform(r, new Function<AccountInfo, Account.Id>() {

@ -23,6 +23,7 @@ import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.RestApiException;
@ -61,7 +62,7 @@ public class CreateChangeIT extends AbstractDaemonTest {
@Test
public void createEmptyChange_MissingBranch() throws Exception {
ChangeInfo ci = new ChangeInfo();
ChangeInput ci = new ChangeInput();
ci.project = project.get();
assertCreateFails(ci, BadRequestException.class,
"branch must be non-empty");
@ -69,7 +70,7 @@ public class CreateChangeIT extends AbstractDaemonTest {
@Test
public void createEmptyChange_MissingMessage() throws Exception {
ChangeInfo ci = new ChangeInfo();
ChangeInput ci = new ChangeInput();
ci.project = project.get();
ci.branch = "master";
assertCreateFails(ci, BadRequestException.class,
@ -78,26 +79,26 @@ public class CreateChangeIT extends AbstractDaemonTest {
@Test
public void createEmptyChange_InvalidStatus() throws Exception {
ChangeInfo ci = newChangeInfo(ChangeStatus.MERGED);
ChangeInput ci = newChangeInput(ChangeStatus.MERGED);
assertCreateFails(ci, BadRequestException.class,
"unsupported change status");
}
@Test
public void createNewChange() throws Exception {
assertCreateSucceeds(newChangeInfo(ChangeStatus.NEW));
assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
}
@Test
public void createDraftChange() throws Exception {
assume().that(isAllowDrafts()).isTrue();
assertCreateSucceeds(newChangeInfo(ChangeStatus.DRAFT));
assertCreateSucceeds(newChangeInput(ChangeStatus.DRAFT));
}
@Test
public void createDraftChangeNotAllowed() throws Exception {
assume().that(isAllowDrafts()).isFalse();
ChangeInfo ci = newChangeInfo(ChangeStatus.DRAFT);
ChangeInput ci = newChangeInput(ChangeStatus.DRAFT);
assertCreateFails(ci, MethodNotAllowedException.class,
"draft workflow is disabled");
}
@ -106,7 +107,7 @@ public class CreateChangeIT extends AbstractDaemonTest {
public void notedbCommit() throws Exception {
assume().that(notesMigration.enabled()).isTrue();
ChangeInfo c = assertCreateSucceeds(newChangeInfo(ChangeStatus.NEW));
ChangeInfo c = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
try (Repository repo = repoManager.openMetadataRepository(project);
RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(
@ -126,8 +127,8 @@ public class CreateChangeIT extends AbstractDaemonTest {
}
}
private ChangeInfo newChangeInfo(ChangeStatus status) {
ChangeInfo in = new ChangeInfo();
private ChangeInput newChangeInput(ChangeStatus status) {
ChangeInput in = new ChangeInput();
in.project = project.get();
in.branch = "master";
in.subject = "Empty change";
@ -136,7 +137,7 @@ public class CreateChangeIT extends AbstractDaemonTest {
return in;
}
private ChangeInfo assertCreateSucceeds(ChangeInfo in) throws Exception {
private ChangeInfo assertCreateSucceeds(ChangeInput in) throws Exception {
ChangeInfo out = gApi.changes().create(in).get();
assertThat(out.branch).isEqualTo(in.branch);
assertThat(out.subject).isEqualTo(in.subject);
@ -149,7 +150,7 @@ public class CreateChangeIT extends AbstractDaemonTest {
return out;
}
private void assertCreateFails(ChangeInfo in,
private void assertCreateFails(ChangeInput in,
Class<? extends RestApiException> errType, String errSubstring)
throws Exception {
exception.expect(errType);

@ -16,6 +16,7 @@ package com.google.gerrit.extensions.api.changes;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
@ -58,7 +59,7 @@ public interface Changes {
ChangeApi id(String project, String branch, String id)
throws RestApiException;
ChangeApi create(ChangeInfo in) throws RestApiException;
ChangeApi create(ChangeInput in) throws RestApiException;
QueryRequest query();
QueryRequest query(String query);
@ -156,7 +157,7 @@ public interface Changes {
}
@Override
public ChangeApi create(ChangeInfo in) throws RestApiException {
public ChangeApi create(ChangeInput in) throws RestApiException {
throw new NotImplementedException();
}

@ -43,7 +43,6 @@ public class ChangeInfo {
public Integer insertions;
public Integer deletions;
public String baseChange;
public int _number;
public AccountInfo owner;

@ -0,0 +1,28 @@
// Copyright (C) 2016 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 com.google.gerrit.extensions.client.ChangeStatus;
public class ChangeInput {
public String project;
public String branch;
public String subject;
public String topic;
public ChangeStatus status;
public String baseChange;
public Boolean newBranch;
}

@ -23,6 +23,7 @@ import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.Changes;
import com.google.gerrit.extensions.client.ListChangesOption;
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.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
@ -90,7 +91,7 @@ class ChangesImpl implements Changes {
}
@Override
public ChangeApi create(ChangeInfo in) throws RestApiException {
public ChangeApi create(ChangeInput in) throws RestApiException {
try {
ChangeInfo out = createChange.apply(
TopLevelResource.INSTANCE, in).value();

@ -20,9 +20,11 @@ import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.extensions.client.ChangeStatus;
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.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
@ -61,6 +63,7 @@ import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TreeFormatter;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.ChangeIdUtil;
@ -74,7 +77,7 @@ import java.util.TimeZone;
@Singleton
public class CreateChange implements
RestModifyView<TopLevelResource, ChangeInfo> {
RestModifyView<TopLevelResource, ChangeInput> {
private final Provider<ReviewDb> db;
private final GitRepositoryManager gitManager;
@ -117,7 +120,7 @@ public class CreateChange implements
}
@Override
public Response<ChangeInfo> apply(TopLevelResource parent, ChangeInfo input)
public Response<ChangeInfo> apply(TopLevelResource parent, ChangeInput input)
throws OrmException, IOException, InvalidChangeOperationException,
RestApiException, UpdateException {
if (Strings.isNullOrEmpty(input.project)) {
@ -178,24 +181,37 @@ public class CreateChange implements
groups = ps.getGroups();
} else {
Ref destRef = git.getRefDatabase().exactRef(refName);
if (destRef == null) {
throw new UnprocessableEntityException(String.format(
"Branch %s does not exist.", refName));
if (destRef != null) {
if (Boolean.TRUE.equals(input.newBranch)) {
throw new ResourceConflictException(String.format(
"Branch %s already exists.", refName));
} else {
parentCommit = destRef.getObjectId();
}
} else {
if (Boolean.TRUE.equals(input.newBranch)) {
parentCommit = null;
} else {
throw new UnprocessableEntityException(String.format(
"Branch %s does not exist.", refName));
}
}
parentCommit = destRef.getObjectId();
groups = Collections.emptyList();
}
RevCommit mergeTip = rw.parseCommit(parentCommit);
RevCommit mergeTip =
parentCommit == null ? null : rw.parseCommit(parentCommit);
Timestamp now = TimeUtil.nowTs();
IdentifiedUser me = user.get().asIdentifiedUser();
PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
ObjectId id = ChangeIdUtil.computeChangeId(mergeTip.getTree(),
mergeTip, author, author, input.subject);
String commitMessage = ChangeIdUtil.insertId(input.subject, id);
try (ObjectInserter oi = git.newObjectInserter()) {
ObjectId treeId =
mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree();
ObjectId id = ChangeIdUtil.computeChangeId(treeId,
mergeTip, author, author, input.subject);
String commitMessage = ChangeIdUtil.insertId(input.subject, id);
RevCommit c = newCommit(oi, rw, author, mergeTip, commitMessage);
Change.Id changeId = new Change.Id(seq.nextChangeId());
@ -227,8 +243,12 @@ public class CreateChange implements
PersonIdent authorIdent, RevCommit mergeTip, String commitMessage)
throws IOException {
CommitBuilder commit = new CommitBuilder();
commit.setTreeId(mergeTip.getTree().getId());
commit.setParentId(mergeTip);
if (mergeTip == null) {
commit.setTreeId(emptyTreeId(oi));
} else {
commit.setTreeId(mergeTip.getTree().getId());
commit.setParentId(mergeTip);
}
commit.setAuthor(authorIdent);
commit.setCommitter(authorIdent);
commit.setMessage(commitMessage);
@ -242,4 +262,11 @@ public class CreateChange implements
inserter.flush();
return id;
}
private static ObjectId emptyTreeId(ObjectInserter inserter)
throws IOException {
ObjectId id = inserter.insert(new TreeFormatter());
inserter.flush();
return id;
}
}