Merge "Implement host-level default for 'HEAD' when new projects are created."
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index 9e3d70b..0575eb9 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -53,9 +53,10 @@
-b::
Name of the initial branch(es) in the newly created project.
Several branches can be specified on the command line.
- If several branches are specified then the first one becomes HEAD
- of the project. If none branches are specified then default value
- ('master') is used.
+ If several branches are specified then the first one becomes
+ link:project-configuration.html#default-branch[HEAD] of the project.
+ If none branches are specified then link:config-gerrit.html#gerrit.defaultBranch[host-level default]
+ is used.
--owner::
-o::
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 23720460..c5c9dce 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2122,6 +2122,13 @@
+
Defaults to `All-Projects` if not set.
+[[gerrit.defaultBranch]]gerrit.defaultBranch::
++
+Name of the link:project-configuration.html#default-branch[default branch]
+to use on the project creation, if no other branches were specified in the input.
++
+Defaults to `refs/heads/master` if not set.
+
[[gerrit.allUsers]]gerrit.allUsers::
+
Name of the project in which meta data of all users is stored.
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt
index 23030a4..e583f45 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -108,6 +108,9 @@
=== Default Branch
The default branch of a remote repository is defined by its `HEAD`.
+The default branch is selected from the initial branches of the newly created project,
+or set to link:config-gerrit.html#gerrit.defaultBranch[host-level default],
+if the project was created with empty branches.
For convenience reasons, when the repository is cloned Git creates a
local branch for this default branch and checks it out.
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 49bc7e5..e30ce3a 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -4211,8 +4211,10 @@
|`branches` |optional|
A list of branches that should be initially created. +
For the branch names the `refs/heads/` prefix can be omitted. +
-The first entry of the list will be the default branch (ie. the target
-of the `HEAD` symbolic ref). +
+The first entry of the list will be the
+link:project-configuration.html#default-branch[default branch]. +
+If the list is empty, link:config-gerrit.html#gerrit.defaultBranch[host-level default]
+is used. +
Branches in the Gerrit internal ref space are not allowed, such as
refs/groups/, refs/changes/, etc...
|`owners` |optional|
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 5b27088..aa0c938 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -74,6 +74,7 @@
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.api.projects.ProjectInput;
import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -174,6 +175,7 @@
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
@@ -583,6 +585,7 @@
ProjectInput in = new ProjectInput();
TestProjectInput ann = description.getAnnotation(TestProjectInput.class);
in.name = name("project");
+ in.branches = ImmutableList.of(Constants.R_HEADS + Constants.MASTER);
if (ann != null) {
in.parent = Strings.emptyToNull(ann.parent());
in.description = Strings.emptyToNull(ann.description());
@@ -1349,6 +1352,16 @@
assertThat(rule.getMax()).isEqualTo(expectedMax);
}
+ protected void assertHead(String projectName, String expectedRef) throws Exception {
+ // Assert gerrit's project head points to the correct branch
+ assertThat(getProjectBranches(projectName).get(Constants.HEAD).revision)
+ .isEqualTo(RefNames.shortName(expectedRef));
+ // Assert git head points to the correct branch
+ try (Repository repo = repoManager.openRepository(Project.nameKey(projectName))) {
+ assertThat(repo.exactRef(Constants.HEAD).getTarget().getName()).isEqualTo(expectedRef);
+ }
+ }
+
protected InternalGroup group(AccountGroup.UUID groupUuid) {
InternalGroup group = groupCache.get(groupUuid).orElse(null);
assertWithMessage(groupUuid.get()).that(group).isNotNull();
@@ -1622,6 +1635,12 @@
return comments;
}
+ protected ImmutableMap<String, BranchInfo> getProjectBranches(String projectName)
+ throws RestApiException {
+ return gApi.projects().name(projectName).branches().get().stream()
+ .collect(ImmutableMap.toImmutableMap(branch -> branch.ref, branch -> branch));
+ }
+
protected AutoCloseable installPlugin(String pluginName, Class<? extends Module> sysModuleClass)
throws Exception {
return installPlugin(pluginName, sysModuleClass, null, null);
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index faab241..f3b2bad 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -37,6 +37,7 @@
import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.ProjectOwnerGroupsProvider;
import com.google.gerrit.server.group.GroupResolver;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -60,6 +61,7 @@
import java.util.List;
import java.util.concurrent.locks.Lock;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
@@ -79,6 +81,8 @@
private final PluginItemContext<ProjectNameLockManager> lockManager;
private final ProjectCreator projectCreator;
+ private final Config gerritConfig;
+
@Inject
CreateProject(
ProjectCreator projectCreator,
@@ -90,7 +94,8 @@
Provider<PutConfig> putConfig,
AllProjectsName allProjects,
AllUsersName allUsers,
- PluginItemContext<ProjectNameLockManager> lockManager) {
+ PluginItemContext<ProjectNameLockManager> lockManager,
+ @GerritServerConfig Config gerritConfig) {
this.projectsCollection = projectsCollection;
this.projectCreator = projectCreator;
this.groupResolver = groupResolver;
@@ -101,6 +106,7 @@
this.allProjects = allProjects;
this.allUsers = allUsers;
this.lockManager = lockManager;
+ this.gerritConfig = gerritConfig;
}
@Override
@@ -190,18 +196,18 @@
private List<String> normalizeBranchNames(List<String> branches) throws BadRequestException {
if (branches == null || branches.isEmpty()) {
- return Collections.singletonList(Constants.R_HEADS + Constants.MASTER);
+ // Use host-level default for HEAD or fall back to 'master' if nothing else was specified in
+ // the input.
+ String defaultBranch = gerritConfig.getString("gerrit", null, "defaultBranch");
+ defaultBranch =
+ defaultBranch != null
+ ? normalizeAndValidateBranch(defaultBranch)
+ : Constants.R_HEADS + Constants.MASTER;
+ return Collections.singletonList(defaultBranch);
}
-
List<String> normalizedBranches = new ArrayList<>();
for (String branch : branches) {
- while (branch.startsWith("/")) {
- branch = branch.substring(1);
- }
- branch = RefNames.fullName(branch);
- if (!Repository.isValidRefName(branch)) {
- throw new BadRequestException(String.format("Branch \"%s\" is not a valid name.", branch));
- }
+ branch = normalizeAndValidateBranch(branch);
if (!normalizedBranches.contains(branch)) {
normalizedBranches.add(branch);
}
@@ -209,6 +215,17 @@
return normalizedBranches;
}
+ private String normalizeAndValidateBranch(String branch) throws BadRequestException {
+ while (branch.startsWith("/")) {
+ branch = branch.substring(1);
+ }
+ branch = RefNames.fullName(branch);
+ if (!Repository.isValidRefName(branch)) {
+ throw new BadRequestException(String.format("Branch \"%s\" is not a valid name.", branch));
+ }
+ return branch;
+ }
+
static class ValidBranchListener implements ProjectCreationValidationListener {
@Override
public void validateNewProject(CreateProjectArgs args) throws ValidationException {
diff --git a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index f2ab4e8..e2d554d 100644
--- a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -143,7 +143,7 @@
name = "--branch",
aliases = {"-b"},
metaVar = "BRANCH",
- usage = "initial branch name\n(default: master)")
+ usage = "initial branch name\n(default: gerrit.defaultProject)")
private List<String> branch;
@Option(name = "--empty-commit", usage = "to create initial empty commit")
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 6e19ac2..dd70d4a 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -116,7 +116,7 @@
extensionRegistry.newRegistration().add(projectIndexedCounter)) {
String name = name("foo");
assertThat(gApi.projects().create(name).get().name).isEqualTo(name);
-
+ assertHead(name, "refs/heads/master");
RevCommit head = getRemoteHead(name, RefNames.REFS_CONFIG);
eventRecorder.assertRefUpdatedEvents(name, RefNames.REFS_CONFIG, null, head);
@@ -126,6 +126,23 @@
}
@Test
+ @GerritConfig(name = "gerrit.defaultBranch", value = "main")
+ public void createProject_WhenDefaultBranchIsSetInConfig() throws Exception {
+ ProjectIndexedCounter projectIndexedCounter = new ProjectIndexedCounter();
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectIndexedCounter)) {
+ String name = name("foo");
+ assertThat(gApi.projects().create(name).get().name).isEqualTo(name);
+ assertHead(name, "refs/heads/main");
+ RevCommit head = getRemoteHead(name, RefNames.REFS_CONFIG);
+ eventRecorder.assertRefUpdatedEvents(name, RefNames.REFS_CONFIG, null, head);
+
+ eventRecorder.assertRefUpdatedEvents(name, "refs/heads/main", new String[] {});
+ projectIndexedCounter.assertReindexOf(name);
+ }
+ }
+
+ @Test
public void createProjectWithInitialBranches() throws Exception {
ProjectIndexedCounter projectIndexedCounter = new ProjectIndexedCounter();
try (Registration registration =
@@ -135,12 +152,13 @@
ProjectInput input = new ProjectInput();
input.name = name;
input.createEmptyCommit = true;
- input.branches = ImmutableList.of("master", "foo");
+ input.branches = ImmutableList.of("foo", "master");
assertThat(gApi.projects().create(input).get().name).isEqualTo(name);
assertThat(
gApi.projects().name(name).branches().get().stream().map(b -> b.ref).collect(toSet()))
.containsExactly("refs/heads/foo", "refs/heads/master", "HEAD", RefNames.REFS_CONFIG);
+ assertHead(name, "refs/heads/foo");
RevCommit head = getRemoteHead(name, RefNames.REFS_CONFIG);
eventRecorder.assertRefUpdatedEvents(name, RefNames.REFS_CONFIG, null, head);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index e5c5952..7090074 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -26,6 +26,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.net.HttpHeaders;
@@ -40,6 +41,7 @@
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
import com.google.gerrit.extensions.api.projects.ConfigInfo;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -47,6 +49,7 @@
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -304,13 +307,15 @@
}
@Test
- public void createProjectWithEmptyCommit() throws Exception {
+ @GerritConfig(name = "gerrit.defaultBranch", value = "main")
+ public void createPermissionOnlyProject_WhenDefaultBranchIsSet() throws Exception {
String newProjectName = name("newProject");
ProjectInput in = new ProjectInput();
in.name = newProjectName;
- in.createEmptyCommit = true;
+ in.permissionsOnly = true;
gApi.projects().create(in);
- assertEmptyCommit(newProjectName, "refs/heads/master");
+ // For permissionOnly, don't use host-level default branch.
+ assertHead(newProjectName, RefNames.REFS_CONFIG);
}
@Test
@@ -344,6 +349,72 @@
}
@Test
+ @GerritConfig(name = "gerrit.defaultBranch", value = "main")
+ public void createProject_WhenDefaultBranchIsSet() throws Exception {
+ String newProjectName = name("newProject");
+ gApi.projects().create(newProjectName).get();
+ ImmutableMap<String, BranchInfo> branches = getProjectBranches(newProjectName);
+ // HEAD symbolic ref is set to the default, but the actual ref is not created.
+ assertThat(branches.keySet()).containsExactly("HEAD", "refs/meta/config");
+ assertHead(newProjectName, "refs/heads/main");
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.defaultBranch", value = "main")
+ public void createProjectWithEmptyCommit_WhenDefaultBranchIsSet() throws Exception {
+ String newProjectName = name("newProject");
+ ProjectInput in = new ProjectInput();
+ in.name = newProjectName;
+ in.createEmptyCommit = true;
+ gApi.projects().create(in);
+ ImmutableMap<String, BranchInfo> branches = getProjectBranches(newProjectName);
+ // HEAD symbolic ref is set to the default, and the actual ref is created.
+ assertThat(branches.keySet()).containsExactly("HEAD", "refs/meta/config", "refs/heads/main");
+ assertHead(newProjectName, "refs/heads/main");
+ assertEmptyCommit(newProjectName, "HEAD", "refs/heads/main");
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.defaultBranch", value = "refs/heads/main")
+ public void createProject_WhenDefaultBranchIsSet_WithBranches() throws Exception {
+ // Host-level default only applies if no branches were passed in the input
+ String newProjectName = name("newProject");
+ ProjectInput in = new ProjectInput();
+ in.name = newProjectName;
+ in.createEmptyCommit = true;
+ in.branches = ImmutableList.of("refs/heads/test", "release");
+ gApi.projects().create(in);
+ ImmutableMap<String, BranchInfo> branches = getProjectBranches(newProjectName);
+ assertThat(branches.keySet())
+ .containsExactly("HEAD", "refs/meta/config", "refs/heads/test", "refs/heads/release");
+ assertHead(newProjectName, "refs/heads/test");
+ assertEmptyCommit(newProjectName, "refs/heads/test", "refs/heads/release");
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.defaultBranch", value = "refs/users/self")
+ public void createProject_WhenDefaultBranchIsSet_ToGerritRef() throws Exception {
+ String newProjectName = name("newProject");
+ Throwable thrown =
+ assertThrows(ResourceConflictException.class, () -> gApi.projects().create(newProjectName));
+ assertThat(thrown).hasCauseThat().isInstanceOf(ValidationException.class);
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("Cannot create a project with branch refs/users/self");
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.defaultBranch", value = "refs~main")
+ public void createProject_WhenDefaultBranchIsSet_ToInvalidBranch() throws Exception {
+ String newProjectName = name("newProject");
+ Throwable thrown =
+ assertThrows(BadRequestException.class, () -> gApi.projects().create(newProjectName));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo("Branch \"refs/heads/refs~main\" is not a valid name.");
+ }
+
+ @Test
public void createProjectWithCapability() throws Exception {
projectOperations
.allProjectsForUpdate()
@@ -494,12 +565,6 @@
assertThat(cfg.defaultSubmitType.inheritedValue).isEqualTo(SubmitType.MERGE_ALWAYS);
}
- private void assertHead(String projectName, String expectedRef) throws Exception {
- try (Repository repo = repoManager.openRepository(Project.nameKey(projectName))) {
- assertThat(repo.exactRef(Constants.HEAD).getTarget().getName()).isEqualTo(expectedRef);
- }
- }
-
private void assertEmptyCommit(String projectName, String... refs) throws Exception {
Project.NameKey projectKey = Project.nameKey(projectName);
try (Repository repo = repoManager.openRepository(projectKey);
diff --git a/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
index 01b8eae..5429131 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
@@ -19,6 +19,7 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.project.ProjectState;
import java.util.Optional;
@@ -85,4 +86,35 @@
assertThat(projectState).isPresent();
assertThat(projectState.get().getName()).isEqualTo(newProjectName);
}
+
+ @Test
+ public void withEmptyBranches() throws Exception {
+ String newProjectName = name("newProject");
+ adminSshSession.exec("gerrit create-project " + newProjectName);
+ adminSshSession.assertSuccess();
+ Optional<ProjectState> projectState = projectCache.get(Project.nameKey(newProjectName));
+ assertThat(projectState).isPresent();
+ assertHead(newProjectName, "refs/heads/master");
+ }
+
+ @Test
+ public void withInitBranches() throws Exception {
+ String newProjectName = name("newProject");
+ adminSshSession.exec("gerrit create-project --branch init-branch " + newProjectName);
+ adminSshSession.assertSuccess();
+ Optional<ProjectState> projectState = projectCache.get(Project.nameKey(newProjectName));
+ assertThat(projectState).isPresent();
+ assertHead(newProjectName, "refs/heads/init-branch");
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.defaultBranch", value = "refs/heads/main")
+ public void withEmptyBranches_WhenDefaultBranchIsSet() throws Exception {
+ String newProjectName = name("newProject");
+ adminSshSession.exec("gerrit create-project " + newProjectName);
+ adminSshSession.assertSuccess();
+ Optional<ProjectState> projectState = projectCache.get(Project.nameKey(newProjectName));
+ assertThat(projectState).isPresent();
+ assertHead(newProjectName, "refs/heads/main");
+ }
}