Let JGit read system and user-level config from $SITE/etc/jgit.config

A support for system-level (or user-level) JGit configuration was
suboptimal. Gerrit admins had to use either $HOME/.gitconfig of the user
owning Gerrit process or a system-wide location for global JGit
configuration for all repositories served by a Gerrit site.

Replace the system-wide and user-global JGit configuration to be read from
$SITE/etc/jgit.config.

Change-Id: I5e60e2d4d686b605673fe14665479c3f8cc2a1e2
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 7e9f1e1..67cd0f9 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1577,6 +1577,10 @@
 [[core]]
 === Section core
 
+[NOTE]
+The link:#jgitConfig[etc/jgit.config] file supports configuration of all JGit
+options.
+
 [[core.packedGitWindowSize]]core.packedGitWindowSize::
 +
 Number of bytes of a pack file to load into memory in a single
@@ -4924,6 +4928,19 @@
 +
 * link:config-themes.html[Themes]
 
+[[jgitConfig]]
+== File `etc/jgit.config`
+
+Gerrit uses the `$site_path/etc/jgit.config` file instead of the
+system-wide and user-global Git configuration for its runtime JGit
+configuration.
+
+Sample `etc/jgit.config` file:
+----
+[core]
+  trustFolderStat = false
+----
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 1fd28cc..f406acb 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.httpd.raw.StaticModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
 import com.google.gerrit.pgm.util.LogFileCompressor;
@@ -70,6 +71,7 @@
 import com.google.gerrit.server.git.GarbageCollectionModule;
 import com.google.gerrit.server.git.GitRepositoryManagerModule;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
+import com.google.gerrit.server.git.SystemReaderInstaller;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.index.IndexModule.IndexType;
@@ -263,6 +265,13 @@
     } else {
       modules.add(new GerritServerConfigModule());
     }
+    modules.add(
+        new LifecycleModule() {
+          @Override
+          protected void configure() {
+            listener().to(SystemReaderInstaller.class);
+          }
+        });
     modules.add(new DropWizardMetricMaker.ApiModule());
     return Guice.createInjector(
         PRODUCTION, LibModuleLoader.loadModules(cfgInjector, LibModuleType.DB_MODULE));
diff --git a/java/com/google/gerrit/pgm/util/SiteProgram.java b/java/com/google/gerrit/pgm/util/SiteProgram.java
index eed307f..98558fb 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -18,6 +18,7 @@
 import static com.google.inject.Stage.PRODUCTION;
 
 import com.google.gerrit.common.Die;
+import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
@@ -28,6 +29,7 @@
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.git.GitRepositoryManagerModule;
+import com.google.gerrit.server.git.SystemReaderInstaller;
 import com.google.gerrit.server.schema.SchemaModule;
 import com.google.gerrit.server.securestore.SecureStoreClassName;
 import com.google.inject.AbstractModule;
@@ -106,6 +108,13 @@
           });
     }
 
+    modules.add(
+        new LifecycleModule() {
+          @Override
+          protected void configure() {
+            listener().to(SystemReaderInstaller.class);
+          }
+        });
     Module configModule = new GerritServerConfigModule();
     modules.add(configModule);
     modules.add(
diff --git a/java/com/google/gerrit/server/config/SitePaths.java b/java/com/google/gerrit/server/config/SitePaths.java
index 47b6336..ee95c6f 100644
--- a/java/com/google/gerrit/server/config/SitePaths.java
+++ b/java/com/google/gerrit/server/config/SitePaths.java
@@ -54,6 +54,8 @@
   public final Path secure_config;
   public final Path notedb_config;
 
+  public final Path jgit_config;
+
   public final Path ssl_keystore;
   public final Path ssh_key;
   public final Path ssh_rsa;
@@ -99,6 +101,8 @@
     secure_config = etc_dir.resolve("secure.config");
     notedb_config = etc_dir.resolve("notedb.config");
 
+    jgit_config = etc_dir.resolve("jgit.config");
+
     ssl_keystore = etc_dir.resolve("keystore");
     ssh_key = etc_dir.resolve("ssh_host_key");
     ssh_rsa = etc_dir.resolve("ssh_host_rsa_key");
diff --git a/java/com/google/gerrit/server/git/SystemReaderInstaller.java b/java/com/google/gerrit/server/git/SystemReaderInstaller.java
new file mode 100644
index 0000000..520ede4
--- /dev/null
+++ b/java/com/google/gerrit/server/git/SystemReaderInstaller.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2017 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.server.git;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
+
+@Singleton
+public class SystemReaderInstaller implements LifecycleListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final SitePaths site;
+
+  @Inject
+  SystemReaderInstaller(SitePaths site) {
+    this.site = site;
+  }
+
+  @Override
+  public void start() {
+    SystemReader.setInstance(customReader());
+    logger.atInfo().log("Set JGit's SystemReader to read system config from %s", site.jgit_config);
+  }
+
+  @Override
+  public void stop() {}
+
+  private SystemReader customReader() {
+    SystemReader current = SystemReader.getInstance();
+
+    FileBasedConfig jgitConfig = new FileBasedConfig(site.jgit_config.toFile(), FS.DETECTED);
+
+    return new SystemReader() {
+      @Override
+      public String getHostname() {
+        return current.getHostname();
+      }
+
+      @Override
+      public String getenv(String variable) {
+        return current.getenv(variable);
+      }
+
+      @Override
+      public String getProperty(String key) {
+        return current.getProperty(key);
+      }
+
+      @Override
+      public FileBasedConfig openUserConfig(Config parent, FS fs) {
+        return current.openSystemConfig(parent, fs);
+      }
+
+      @Override
+      public FileBasedConfig openSystemConfig(Config parent, FS fs) {
+        return jgitConfig;
+      }
+
+      @Override
+      public long getCurrentTime() {
+        return current.getCurrentTime();
+      }
+
+      @Override
+      public int getTimezone(long when) {
+        return current.getTimezone(when);
+      }
+    };
+  }
+}
diff --git a/javatests/com/google/gerrit/server/git/JGitConfigTest.java b/javatests/com/google/gerrit/server/git/JGitConfigTest.java
new file mode 100644
index 0000000..7cb5a98
--- /dev/null
+++ b/javatests/com/google/gerrit/server/git/JGitConfigTest.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2019 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.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.server.config.SitePaths;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class JGitConfigTest {
+
+  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  private SitePaths site;
+  private Path gitPath;
+
+  @Before
+  public void setUp() throws IOException {
+    site = new SitePaths(temporaryFolder.newFolder().toPath());
+    Files.createDirectories(site.etc_dir);
+    gitPath = Files.createDirectories(site.resolve("git"));
+
+    Files.write(
+        site.jgit_config, "[core]\n  trustFolderStat = false\n".getBytes(StandardCharsets.UTF_8));
+    new SystemReaderInstaller(site).start();
+  }
+
+  @Test
+  public void test() throws IOException {
+    try (Repository repo = new FileRepository(gitPath.resolve("foo").toFile())) {
+      assertThat(repo.getConfig().getString("core", null, "trustFolderStat")).isEqualTo("false");
+    }
+  }
+}