Allow to specify whether to store reflog for new projects

Introduce a new remote.NAME.storeRefLog for configuring the
core.logallrefupdates in the new repository's config file when it is
created by replication.

Release-Notes: Allow configuring the reflog for newly created repositories with remote.NAME.storeRefLog config.
Bug: Issue 464210139
Change-Id: Ia14a4b5829cceb226a537c0651d08f2057fce8aa
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AdminApi.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AdminApi.java
index 77e1836..f3bc7cc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/AdminApi.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AdminApi.java
@@ -17,8 +17,31 @@
 import com.google.gerrit.entities.Project;
 
 public interface AdminApi {
+
+  /**
+   * Create a new project without honouring the reflog configuration.
+   *
+   * @param project the new project to create
+   * @param head initial value for the HEAD
+   * @return true if the project was created
+   * @deprecated this method should not be used as it does not respect the reflog settings
+   *     <p>Use {@link AdminApi#createProject(Project.NameKey,String,boolean)} instead.
+   */
+  @Deprecated(since = "3.14", forRemoval = true)
   boolean createProject(Project.NameKey project, String head);
 
+  /**
+   * Create a new project honouring the reflog configuration.
+   *
+   * @param project the new project to create
+   * @param head initial value for the HEAD
+   * @param enableRefLog true if the reflog tracking should be enabled
+   * @return true if the project was created
+   */
+  default boolean createProject(Project.NameKey project, String head, boolean enableRefLog) {
+    return createProject(project, head);
+  }
+
   boolean deleteProject(Project.NameKey project);
 
   boolean updateHead(Project.NameKey project, String newHead);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/CreateProjectTask.java b/src/main/java/com/googlesource/gerrit/plugins/replication/CreateProjectTask.java
index 8cbb2a2..32903ab 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/CreateProjectTask.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/CreateProjectTask.java
@@ -53,15 +53,16 @@
   public boolean create() {
     return destinations
         .getURIs(Optional.of(config.getName()), project, FilterType.PROJECT_CREATION)
-        .values()
+        .entries()
         .stream()
-        .map(u -> createProject(u, project, head))
+        .map(entry -> createProject(entry.getValue(), project, head, entry.getKey().storeRefLog()))
         .reduce(true, (a, b) -> a && b);
   }
 
-  private boolean createProject(URIish replicateURI, Project.NameKey projectName, String head) {
+  private boolean createProject(
+      URIish replicateURI, Project.NameKey projectName, String head, boolean storeRefLog) {
     Optional<AdminApi> adminApi = adminApiFactory.get().create(replicateURI);
-    if (adminApi.isPresent() && adminApi.get().createProject(projectName, head)) {
+    if (adminApi.isPresent() && adminApi.get().createProject(projectName, head, storeRefLog)) {
       return true;
     }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
index 41a9a49..f0b26f9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -894,6 +894,10 @@
     return config.excludedRefsPattern();
   }
 
+  boolean storeRefLog() {
+    return config.storeRefLog();
+  }
+
   private static boolean matches(URIish uri, String urlMatch) {
     if (urlMatch == null || urlMatch.equals("") || urlMatch.equals("*")) {
       return true;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java
index 41ab4c4..b9e1be9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java
@@ -56,6 +56,7 @@
   private final int slowLatencyThreshold;
   private final Supplier<Integer> pushBatchSize;
   private final ImmutableList<Pattern> excludedRefsPattern;
+  private final boolean storeRefLog;
 
   protected DestinationConfiguration(RemoteConfig remoteConfig, Config cfg) {
     this.remoteConfig = remoteConfig;
@@ -122,6 +123,7 @@
               return 0;
             });
     excludedRefsPattern = getExcludedRefsPattern(cfg, name);
+    storeRefLog = cfg.getBoolean("remote", name, "storeRefLog", false);
   }
 
   @Override
@@ -227,6 +229,11 @@
     return excludedRefsPattern;
   }
 
+  @Override
+  public boolean storeRefLog() {
+    return storeRefLog;
+  }
+
   private ImmutableList<Pattern> getExcludedRefsPattern(Config cfg, String name) {
     List<Pattern> patterns = new ArrayList<>();
     for (String regex : cfg.getStringList("remote", name, "excludedRefsPattern")) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/LocalFS.java b/src/main/java/com/googlesource/gerrit/plugins/replication/LocalFS.java
index b092363..49c97f0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/LocalFS.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/LocalFS.java
@@ -20,9 +20,11 @@
 import java.io.File;
 import java.io.IOException;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.transport.URIish;
 
 public class LocalFS implements AdminApi {
@@ -35,6 +37,11 @@
 
   @Override
   public boolean createProject(Project.NameKey project, String head) {
+    return createProject(project, head, false);
+  }
+
+  @Override
+  public boolean createProject(Project.NameKey project, String head, boolean enableRefLog) {
     try (Repository repo = new FileRepository(uri.getPath())) {
       repo.create(true /* bare */);
 
@@ -43,6 +50,15 @@
         u.disableRefLog();
         u.link(head);
       }
+
+      StoredConfig config = repo.getConfig();
+      config.setBoolean(
+          ConfigConstants.CONFIG_CORE_SECTION,
+          null,
+          ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES,
+          enableRefLog);
+      config.save();
+
       repLog.atInfo().log("Created local repository: %s", uri);
     } catch (IOException e) {
       repLog.atSevere().withCause(e).log("Error creating local repository %s", uri.getPath());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteConfiguration.java
index 67d23b0..c725d40 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteConfiguration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteConfiguration.java
@@ -152,4 +152,11 @@
   default ImmutableList<Pattern> excludedRefsPattern() {
     return ImmutableList.of();
   }
+
+  /**
+   * reflog storage flag for newly created repositories
+   *
+   * @return true if new repositories should store ref-updates in their reflog
+   */
+  boolean storeRefLog();
 }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index f2954f5..150810e 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -344,6 +344,17 @@
 
 	Defaults to `git-receive-pack`.
 
+remote.NAME.storeRefLog
+:	`true` if the remote repositories should be enabled for storing
+	ref updates in the reflog once they are created by replication.
+
+	NOTE: Enabling the reflog would prevent the unreferenced objects
+	from being garbage collected, potentially impacting the post-gc
+	repository size. Also, the ref updates may take additional time
+	because of the need to store the old and new SHA1s in the reflog.
+
+	Defaults to `false`.
+
 remote.NAME.uploadpack
 :	Path of the `git-upload-pack` executable on the remote system,
 	if using the SSH transport.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java
index e25a5d6..45cf5a7 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
+import java.io.IOException;
 import java.time.Duration;
 import java.util.Arrays;
 import java.util.List;
@@ -38,13 +39,16 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Test;
@@ -72,18 +76,53 @@
   }
 
   @Test
-  public void shouldReplicateNewProject() throws Exception {
+  public void shouldReplicateNewProjectWithoutRefLog() throws Exception {
     setReplicationDestination("foo", "replica", ALL_PROJECTS);
     reloadConfig();
 
-    Project.NameKey sourceProject = createTestProject("foo");
+    Project.NameKey sourceProject = createTestProject("no_reflog_project");
+    Project.NameKey replicaProject = Project.nameKey(sourceProject + "replica.git");
 
-    WaitUtil.waitUntil(
-        () -> nonEmptyProjectExists(Project.nameKey(sourceProject + "replica.git")),
-        TEST_NEW_PROJECT_TIMEOUT);
+    waitForProjectCreated(replicaProject);
 
-    ProjectInfo replicaProject = gApi.projects().name(sourceProject + "replica").get();
-    assertThat(replicaProject).isNotNull();
+    ProjectInfo replicaProjectInfo = gApi.projects().name(replicaProject.get()).get();
+    assertThat(replicaProjectInfo).isNotNull();
+    applyToRepositoryConfig(replicaProject, assertStoreRefLog(false));
+  }
+
+  @Test
+  public void shouldCreateNewProjectWithRefLog() throws Exception {
+    config.setBoolean("remote", "foo", "storeRefLog", true);
+    setReplicationDestination("foo", "replica", ALL_PROJECTS);
+    reloadConfig();
+
+    Project.NameKey sourceProject = createTestProject("reflog_project");
+    Project.NameKey replicaProject = Project.nameKey(sourceProject + "replica.git");
+
+    waitForProjectCreated(replicaProject);
+
+    applyToRepositoryConfig(replicaProject, assertStoreRefLog(true));
+  }
+
+  private void waitForProjectCreated(Project.NameKey replicaProject) throws InterruptedException {
+    WaitUtil.waitUntil(() -> nonEmptyProjectExists(replicaProject), TEST_NEW_PROJECT_TIMEOUT);
+  }
+
+  private static Consumer<StoredConfig> assertStoreRefLog(boolean expectedValue) {
+    return conf ->
+        assertThat(
+                conf.getBoolean(
+                    ConfigConstants.CONFIG_CORE_SECTION,
+                    null,
+                    ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES))
+            .isEqualTo(expectedValue);
+  }
+
+  private void applyToRepositoryConfig(
+      Project.NameKey projectName, Consumer<StoredConfig> functionToApply) throws IOException {
+    try (Repository repo = repoManager.openRepository(projectName)) {
+      functionToApply.accept(repo.getConfig());
+    }
   }
 
   @Test