CreateChange: Allow create change on new branch
We currently disallow creating a new change through CreateChange
endpoint against a non-existent branch, but we don't really have a
problem with that (except for preventing typos).
In this change, we split ChangeInfo into ChangeInfo and ChangeInput.
Added a new new_branch field in ChangeInput, default to false to
protect against typos, and when it's set to true, allow new change
against a non-existent branch.
Currently it's only implemented in the API, but not in the UI.
Change-Id: I6e661f036b121a33de183c126b763d29b5f2bc93
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index a5dffd1..7a907ba 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -13,10 +13,8 @@
'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 @@
|`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]]
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 6ac86a5..486ca51 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -48,6 +48,7 @@
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 @@
@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 @@
}
}
+ @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>() {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 1e0dc12..063a795 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -23,6 +23,7 @@
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 @@
@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 @@
@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 @@
@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 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 @@
}
}
- 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 @@
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 @@
return out;
}
- private void assertCreateFails(ChangeInfo in,
+ private void assertCreateFails(ChangeInput in,
Class<? extends RestApiException> errType, String errSubstring)
throws Exception {
exception.expect(errType);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
index da8aeb2..b1381e7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -16,6 +16,7 @@
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 @@
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 @@
}
@Override
- public ChangeApi create(ChangeInfo in) throws RestApiException {
+ public ChangeApi create(ChangeInput in) throws RestApiException {
throw new NotImplementedException();
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index d697502..34b19c2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -43,7 +43,6 @@
public Integer insertions;
public Integer deletions;
- public String baseChange;
public int _number;
public AccountInfo owner;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java
new file mode 100644
index 0000000..9f0df93
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java
@@ -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;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
index bb01dea..5485052 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -23,6 +23,7 @@
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 @@
}
@Override
- public ChangeApi create(ChangeInfo in) throws RestApiException {
+ public ChangeApi create(ChangeInput in) throws RestApiException {
try {
ChangeInfo out = createChange.apply(
TopLevelResource.INSTANCE, in).value();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
index d47eb8b..c273524 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -20,9 +20,11 @@
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.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 @@
@Singleton
public class CreateChange implements
- RestModifyView<TopLevelResource, ChangeInfo> {
+ RestModifyView<TopLevelResource, ChangeInput> {
private final Provider<ReviewDb> db;
private final GitRepositoryManager gitManager;
@@ -117,7 +120,7 @@
}
@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 @@
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 @@
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 @@
inserter.flush();
return id;
}
+
+ private static ObjectId emptyTreeId(ObjectInserter inserter)
+ throws IOException {
+ ObjectId id = inserter.insert(new TreeFormatter());
+ inserter.flush();
+ return id;
+ }
}