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");
+  }
 }