Merge branch 'stable-3.7' into stable-3.8

* stable-3.7:
  Update plugin-manager submodule to ba74d4969
  Fix project name's based submetric names collision
  Fix GerritServer replica mode when used with @UseLocalDisk
  Format bazel files using buildifier
  Format sources using gjf
  Add jgit-ssh-jsch as licensed under JGit's license

Release-Notes: skip
Change-Id: Id2c598910edd3663fc28d9a535a94fac6f7a51b2
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 6c9dfef..93e0eb4 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -375,7 +375,14 @@
 * `cache_used_per_repository` : Bytes of memory retained per repository for the
   top N repositories having most data in the cache. The number N of reported
   repositories is limited to 1000.
-** `repository_name`: The name of the repository.
+** `repository_name`: The name of the repository. Note that it is a subject of
+   sanitization in order to avoid collision between repository names. Rules
+   are:
+*** any character outside `[a-zA-Z0-9_-]+([a-zA-Z0-9_-]+)*` pattern is replaced
+    with `\_0x[HEX CODE]_` (code is capitalized) string
+*** for instance `repo/name` is sanitized to `repo_0x2F_name`
+*** if repository name contains the replacement prefix (`_0x`) it is prefixed
+    with another `_0x` e.g. `repo_0x2F_name` becomes `repo_0x_0x2F_name`
 
 === Git
 
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 36bc3c4..e812cb7 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -452,6 +452,12 @@
             bind(TestTicker.class).toInstance(testTicker);
           }
         });
+    daemon.setEnableHttpd(desc.httpd());
+    // Assure that SSHD is enabled if HTTPD is not required, otherwise the Gerrit server would not
+    // even start.
+    daemon.setEnableSshd(!desc.httpd() || desc.useSsh());
+    daemon.setReplica(
+        ReplicaUtil.isReplica(baseConfig) || ReplicaUtil.isReplica(desc.buildConfig(baseConfig)));
 
     if (desc.memory()) {
       checkArgument(additionalArgs.length == 0, "cannot pass args to in-memory server");
@@ -468,7 +474,6 @@
       @Nullable InMemoryRepositoryManager inMemoryRepoManager)
       throws Exception {
     Config cfg = desc.buildConfig(baseConfig);
-    daemon.setReplica(ReplicaUtil.isReplica(baseConfig) || ReplicaUtil.isReplica(cfg));
     mergeTestConfig(cfg);
     // Set the log4j configuration to an invalid one to prevent system logs
     // from getting configured and creating log files.
@@ -745,4 +750,8 @@
   public String toString() {
     return MoreObjects.toStringHelper(this).addValue(desc).toString();
   }
+
+  public boolean isReplica() {
+    return daemon.isReplica();
+  }
 }
diff --git a/java/com/google/gerrit/metrics/Field.java b/java/com/google/gerrit/metrics/Field.java
index 5508819..01b89cf 100644
--- a/java/com/google/gerrit/metrics/Field.java
+++ b/java/com/google/gerrit/metrics/Field.java
@@ -21,6 +21,8 @@
 import java.util.Optional;
 import java.util.function.BiConsumer;
 import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * Describes a bucketing field used by a metric.
@@ -102,6 +104,21 @@
         .metadataMapper(metadataMapper);
   }
 
+  /**
+   * A dedicated field to be used with metrics based on {@link Metadata#projectName()}. It was
+   * introduced to sanitize the project name to avoid sub-metric name's collision.
+   *
+   * @param fieldName name of the field that contains a project name as value
+   * @return builder for the project name field
+   */
+  public static Field.Builder<String> ofProjectName(String fieldName) {
+    return new AutoValue_Field.Builder<String>()
+        .valueType(String.class)
+        .formatter(Field::sanitizeProjectName)
+        .name(fieldName)
+        .metadataMapper(Metadata.Builder::projectName);
+  }
+
   /** Returns name of this field within the metric. */
   public abstract String name();
 
@@ -137,4 +154,33 @@
       return field;
     }
   }
+
+  private static final Pattern SUBMETRIC_NAME_PATTERN =
+      Pattern.compile("[a-zA-Z0-9_-]+([a-zA-Z0-9_-]+)*");
+  private static final Pattern INVALID_CHAR_PATTERN = Pattern.compile("[^\\w-]");
+  private static final String REPLACEMENT_PREFIX = "_0x";
+
+  private static String sanitizeProjectName(String projectName) {
+    if (SUBMETRIC_NAME_PATTERN.matcher(projectName).matches()
+        && !projectName.contains(REPLACEMENT_PREFIX)) {
+      return projectName;
+    }
+
+    String replacmentPrefixSanitizedName =
+        projectName.replaceAll(REPLACEMENT_PREFIX, REPLACEMENT_PREFIX + REPLACEMENT_PREFIX);
+    StringBuilder sanitizedName = new StringBuilder();
+    for (int i = 0; i < replacmentPrefixSanitizedName.length(); i++) {
+      Character c = replacmentPrefixSanitizedName.charAt(i);
+      Matcher matcher = INVALID_CHAR_PATTERN.matcher(c.toString());
+      if (matcher.matches()) {
+        sanitizedName.append(REPLACEMENT_PREFIX);
+        sanitizedName.append(Integer.toHexString(c).toUpperCase());
+        sanitizedName.append('_');
+      } else {
+        sanitizedName.append(c);
+      }
+    }
+
+    return sanitizedName.toString();
+  }
 }
diff --git a/java/com/google/gerrit/metrics/proc/JGitMetricModule.java b/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
index d64bd19..8771448 100644
--- a/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
+++ b/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.server.logging.Metadata;
 import java.util.Map;
 import org.eclipse.jgit.storage.file.WindowCacheStats;
 
@@ -184,7 +183,7 @@
                         + "having most data in the cache.")
                 .setGauge()
                 .setUnit("byte"),
-            Field.ofString("repository_name", Metadata.Builder::projectName)
+            Field.ofProjectName("repository_name")
                 .description("The name of the repository.")
                 .build());
     metrics.newTrigger(
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 744f91b..7474709 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -250,6 +250,10 @@
     this.replica = replica;
   }
 
+  public boolean isReplica() {
+    return replica;
+  }
+
   @VisibleForTesting
   public Injector getHttpdInjector() {
     return httpdInjector;
@@ -375,6 +379,7 @@
     }
     cfgInjector = createCfgInjector();
     config = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
+    config.setBoolean("container", null, "replica", replica);
     indexType = IndexModule.getIndexType(cfgInjector);
     sysInjector = createSysInjector();
     sysInjector.getInstance(PluginGuiceEnvironment.class).setDbCfgInjector(dbInjector, cfgInjector);
diff --git a/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java b/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java
index bd07f7d..213cd1c 100644
--- a/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java
+++ b/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.gerrit.server.util.ReplicaUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -26,21 +27,15 @@
  */
 @Singleton
 public final class GerritIsReplicaProvider implements Provider<Boolean> {
-  public static final String CONFIG_SECTION = "container";
-  public static final String REPLICA_KEY = "replica";
-  public static final String DEPRECATED_REPLICA_KEY = "slave";
-
-  public final boolean isReplica;
+  private final Config config;
 
   @Inject
   public GerritIsReplicaProvider(@GerritServerConfig Config config) {
-    this.isReplica =
-        config.getBoolean(CONFIG_SECTION, REPLICA_KEY, false)
-            || config.getBoolean(CONFIG_SECTION, DEPRECATED_REPLICA_KEY, false);
+    this.config = config;
   }
 
   @Override
   public Boolean get() {
-    return isReplica;
+    return ReplicaUtil.isReplica(config);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java b/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java
index d01a81d..b8ec57b 100644
--- a/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java
@@ -18,6 +18,8 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.server.config.GerritIsReplicaProvider;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
@@ -35,6 +37,14 @@
   @Test
   public void isNotReplica() {
     assertThat(isReplicaProvider.get()).isFalse();
+    assertThat(server.isReplica()).isFalse();
+  }
+
+  @Test
+  @UseLocalDisk
+  public void isNotReplicaWithLocalDisk() {
+    assertThat(isReplicaProvider.get()).isFalse();
+    assertThat(server.isReplica()).isFalse();
   }
 
   @Test
@@ -42,5 +52,30 @@
   public void isReplica() throws Exception {
     restartAsSlave();
     assertThat(isReplicaProvider.get()).isTrue();
+    assertThat(server.isReplica()).isTrue();
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void isReplicaFromGerritConfigAnnotation() throws Exception {
+    assertThat(isReplicaProvider.get()).isTrue();
+    assertThat(server.isReplica()).isTrue();
+  }
+
+  @Test
+  @UseLocalDisk
+  @GerritConfig(name = "container.replica", value = "true")
+  public void isReplicaWithLocalDisk() throws Exception {
+    assertThat(isReplicaProvider.get()).isTrue();
+    assertThat(server.isReplica()).isTrue();
+  }
+
+  @Test
+  @Sandboxed
+  @UseLocalDisk
+  public void isReplicaWithLocalDiskAfterRestart() throws Exception {
+    restartAsSlave();
+    assertThat(isReplicaProvider.get()).isTrue();
+    assertThat(server.isReplica()).isTrue();
   }
 }
diff --git a/javatests/com/google/gerrit/metrics/BUILD b/javatests/com/google/gerrit/metrics/BUILD
new file mode 100644
index 0000000..531280e
--- /dev/null
+++ b/javatests/com/google/gerrit/metrics/BUILD
@@ -0,0 +1,13 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+    name = "field_tests",
+    size = "small",
+    srcs = glob(["*.java"]),
+    tags = ["metrics"],
+    deps = [
+        "//java/com/google/gerrit/metrics",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib/truth",
+    ],
+)
diff --git a/javatests/com/google/gerrit/metrics/FieldSanitizeProjectNameTest.java b/javatests/com/google/gerrit/metrics/FieldSanitizeProjectNameTest.java
new file mode 100644
index 0000000..f12b1b3
--- /dev/null
+++ b/javatests/com/google/gerrit/metrics/FieldSanitizeProjectNameTest.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2023 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class FieldSanitizeProjectNameTest {
+  @Parameterized.Parameters
+  public static Collection<Object[]> testData() {
+    return Arrays.asList(
+        new Object[][] {
+          {"repoName", "repoName"},
+          {"repo_name", "repo_name"},
+          {"repo-name", "repo-name"},
+          {"repo/name", "repo_0x2F_name"},
+          {"repo+name", "repo_0x2B_name"},
+          {"repo_0x2F_name", "repo_0x_0x2F_name"},
+        });
+  }
+
+  private final String input;
+  private final String expected;
+
+  public FieldSanitizeProjectNameTest(String input, String expected) {
+    this.input = input;
+    this.expected = expected;
+  }
+
+  @Test
+  public void shouldSanitizeProjectName() {
+    Field<String> projectNameField = Field.ofProjectName("test_name").build();
+    String result = projectNameField.formatter().apply(input);
+    assertThat(result).isEqualTo(expected);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
index b1cd8fb..512a1b1 100644
--- a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
+++ b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
@@ -171,7 +171,7 @@
               new Description("Latency metric for testing"),
               Field.ofInteger("account", Metadata.Builder::accountId).build(),
               Field.ofInteger("change", Metadata.Builder::changeId).build(),
-              Field.ofString("project", Metadata.Builder::projectName).build());
+              Field.ofProjectName("project").build());
       timer3.start(1000000, 123, "foo/bar").close();
 
       assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(4);
@@ -206,14 +206,14 @@
           metricMaker.newTimer(
               "test1/latency",
               new Description("Latency metric for testing"),
-              Field.ofString("project", Metadata.Builder::projectName).build());
+              Field.ofProjectName("project").build());
       timer1.start(null).close();
 
       Timer2<String, String> timer2 =
           metricMaker.newTimer(
               "test2/latency",
               new Description("Latency metric for testing"),
-              Field.ofString("project", Metadata.Builder::projectName).build(),
+              Field.ofProjectName("project").build(),
               Field.ofString("branch", Metadata.Builder::branchName).build());
       timer2.start(null, null).close();
 
@@ -221,7 +221,7 @@
           metricMaker.newTimer(
               "test3/latency",
               new Description("Latency metric for testing"),
-              Field.ofString("project", Metadata.Builder::projectName).build(),
+              Field.ofProjectName("project").build(),
               Field.ofString("branch", Metadata.Builder::branchName).build(),
               Field.ofString("revision", Metadata.Builder::revision).build());
       timer3.start(null, null, null).close();
@@ -270,7 +270,7 @@
             new Description("Latency metric for testing"),
             Field.ofInteger("account", Metadata.Builder::accountId).build(),
             Field.ofInteger("change", Metadata.Builder::changeId).build(),
-            Field.ofString("project", Metadata.Builder::projectName).build());
+            Field.ofProjectName("project").build());
     timer3.start(1000000, 123, "value3").close();
 
     assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
@@ -317,7 +317,7 @@
               new Description("Latency metric for testing"),
               Field.ofInteger("account", Metadata.Builder::accountId).build(),
               Field.ofInteger("change", Metadata.Builder::changeId).build(),
-              Field.ofString("project", Metadata.Builder::projectName).build());
+              Field.ofProjectName("project").build());
       timer3.start(1000000, 123, "foo/bar").close();
 
       assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index dbd6820..ba74d49 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit dbd68200d867513e2c0449798476e275aaf08cfd
+Subproject commit ba74d4969462c2592bcf97868dd76c33041d47b2