Merge "CreateBranch: Add a source_ref input parameter"
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 3711bf9..0c9d59b 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -4309,6 +4309,12 @@
The base revision of the new branch. +
If not set and `create_empty_commit` is `true` the branch is created with an empty initial commit. +
If not set and `create_empty_commit` is `false` or unset `HEAD` will be used as base revision.
+|`source_ref` |optional|
+The full name of the source ref where `revision` can be found. +
+Used when `revision` is not a ref name in order to check reachability from a
+specific ref. This ref should be visible to the caller. +
+If not set, then all visible refs under `refs/heads/` and `refs/tags/` are
+searched.
|`create_empty_commit`|`false` if not set|
Whether the branch should be created with an empty initial commit. +
Cannot be used in combination with setting a `revision`. +
diff --git a/java/com/google/gerrit/extensions/api/projects/BranchInput.java b/java/com/google/gerrit/extensions/api/projects/BranchInput.java
index 63a118a..b11b588 100644
--- a/java/com/google/gerrit/extensions/api/projects/BranchInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/BranchInput.java
@@ -37,6 +37,11 @@
* initial content, e.g. by merging in another branch, and push the commit for review)..
* <li>{@code ref}: The name of the branch. The prefix refs/heads/ can be omitted. If set, must
* match the branch ID in the URL.
+ * <li>{@code sourceRef}: The full name of the source ref where {@code revision} can be found.
+ * This ref should be visible to the caller. Used when {@code revision} is not a ref name in
+ * order to check reachability from a specific ref. If not set, then all visible refs under
+ * refs/heads/ and refs/tags/ are searched (see {@code CreateRefControl#checkCreateCommit} for
+ * details).
* <li>{@code validationOptions}: Map with key-value pairs that are forwarded as options to the
* ref operation validation listeners (e.g. can be used to skip certain validations). Which
* validation options are supported depends on the installed ref operation validation
@@ -50,5 +55,6 @@
@DefaultInput public String revision;
public boolean createEmptyCommit;
public String ref;
+ public String sourceRef;
public Map<String, String> validationOptions;
}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index 29ab59f..95bb725 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -113,13 +113,24 @@
if (input.revision != null) {
input.revision = input.revision.trim();
}
+ if (input.sourceRef != null) {
+ input.sourceRef = input.sourceRef.trim();
+ }
if (input.createEmptyCommit) {
- if (!Strings.isNullOrEmpty(input.revision)) {
- throw new BadRequestException("create_empty_commit and revision are mutually exclusive");
+ if (!Strings.isNullOrEmpty(input.revision) || !Strings.isNullOrEmpty(input.sourceRef)) {
+ throw new BadRequestException(
+ "create_empty_commit and revision/source_ref are mutually exclusive");
}
} else {
if (Strings.isNullOrEmpty(input.revision)) {
+ if (!Strings.isNullOrEmpty(input.sourceRef)) {
+ throw new BadRequestException("must not provide source_ref if not providing revision");
+ }
input.revision = Constants.HEAD;
+ } else if (!Strings.isNullOrEmpty(input.sourceRef)) {
+ if (input.revision.startsWith(RefNames.REFS)) {
+ throw new BadRequestException("must not provide source_ref if revision is a ref name");
+ }
}
}
@@ -255,7 +266,12 @@
if (input.createEmptyCommit) {
permissionBackend.user(identifiedUser.get()).ref(branchNameKey).check(RefPermission.CREATE);
} else {
- Ref sourceRef = repo.exactRef(input.revision);
+ Ref sourceRef;
+ if (!Strings.isNullOrEmpty(input.sourceRef)) {
+ sourceRef = repo.exactRef(input.sourceRef);
+ } else {
+ sourceRef = repo.exactRef(input.revision);
+ }
if (sourceRef == null) {
createRefControl.checkCreateRef(
identifiedUser, repo, branchNameKey, revObject, /* forPush= */ false);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 643c1a6..d40d551 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -374,6 +374,69 @@
}
@Test
+ public void createWithRevisionAndSourceRef() throws Exception {
+ String sourceRef = "refs/sandbox/master";
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.CREATE).ref(sourceRef).group(REGISTERED_USERS))
+ .add(allow(Permission.PUSH).ref(sourceRef).group(REGISTERED_USERS))
+ .update();
+ // create a new branch outside refs/heads/ to use as the source
+ pushTo(sourceRef);
+ RevCommit revision = projectOperations.project(project).getHead(sourceRef);
+ // update it so that points to a different revision than the revision on which we create the new
+ // branch
+ pushTo(sourceRef);
+ assertThat(projectOperations.project(project).getHead(sourceRef)).isNotEqualTo(revision);
+
+ BranchInput input = new BranchInput();
+ input.revision = revision.name();
+ input.sourceRef = sourceRef;
+ BranchInfo created = branch(testBranch).create(input).get();
+ assertThat(created.ref).isEqualTo(testBranch.branch());
+ assertThat(created.revision).isEqualTo(revision.name());
+ assertThat(projectOperations.project(project).getHead(testBranch.branch())).isEqualTo(revision);
+ }
+
+ @Test
+ public void cannotCreateWithRevisionAndInvisibleSourceRef() throws Exception {
+ String sourceRef = "refs/sandbox/master";
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.CREATE).ref("refs/*").group(REGISTERED_USERS))
+ .add(allow(Permission.PUSH).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ // create a new branch outside refs/heads/ to use as the source
+ pushTo(sourceRef);
+ RevCommit secretCommit = projectOperations.project(project).getHead(sourceRef);
+ // update it so that points to a different revision than the revision on which we create the new
+ // branch
+ pushTo(sourceRef);
+ assertThat(projectOperations.project(project).getHead(sourceRef)).isNotEqualTo(secretCommit);
+
+ // hide the source branch
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.READ).ref(sourceRef).group(REGISTERED_USERS))
+ .update();
+
+ TestAccount unprivileged =
+ accountCreator.create("unprivileged", "unprivileged@example.com", "unprivileged", null);
+ requestScopeOperations.setApiUser(unprivileged.id());
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.projects().name(project.get()).branch(sourceRef).get());
+
+ BranchInput input = new BranchInput();
+ input.revision = secretCommit.name();
+ input.sourceRef = sourceRef;
+ assertThrows(AuthException.class, () -> branch(testBranch).create(input));
+ }
+
+ @Test
public void createEmptyCommitAndRevisionAreMutuallyExclusive() throws Exception {
BranchInput input = new BranchInput();
input.createEmptyCommit = true;
@@ -382,7 +445,7 @@
assertThrows(BadRequestException.class, () -> branch(testBranch).create(input));
assertThat(thrown)
.hasMessageThat()
- .contains("create_empty_commit and revision are mutually exclusive");
+ .contains("create_empty_commit and revision/source_ref are mutually exclusive");
}
@Test