Merge "Always show `Download` action" into stable-3.8
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 7b1baba..25fe9f3 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -129,9 +129,10 @@
 
 - `Hidden`:
 +
-The project is hidden and only visible to project owners. Other users
-are not able to see the project even if they have read permissions
-granted on the project.
+The project is hidden; It will not appear in any searches and is only visible
+to project owners by going directly to the repository admin page. Other users
+are not able to see the project even if they have read permissions granted on
+the project.
 
 
 [[receive-section]]
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index e812cb7..57ac919 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -61,6 +61,7 @@
 import com.google.gerrit.server.experiments.ConfigExperimentFeatures.ConfigExperimentFeaturesModule;
 import com.google.gerrit.server.git.receive.AsyncReceiveCommits.AsyncReceiveCommitsModule;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.index.AbstractIndexModule;
 import com.google.gerrit.server.index.options.AutoFlush;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
 import com.google.gerrit.server.ssh.NoSshModule;
@@ -425,7 +426,6 @@
     if (testSshModule != null) {
       daemon.addAdditionalSshModuleForTesting(testSshModule);
     }
-    daemon.setEnableSshd(desc.useSsh());
     daemon.addAdditionalSysModuleForTesting(
         new AbstractModule() {
           @Override
@@ -461,7 +461,11 @@
 
     if (desc.memory()) {
       checkArgument(additionalArgs.length == 0, "cannot pass args to in-memory server");
-      return startInMemory(desc, site, baseConfig, daemon, inMemoryRepoManager);
+      AbstractIndexModule testIndexModule =
+          (testSysModule instanceof AbstractIndexModule)
+              ? (AbstractIndexModule) testSysModule
+              : null;
+      return startInMemory(desc, site, baseConfig, daemon, inMemoryRepoManager, testIndexModule);
     }
     return startOnDisk(desc, site, daemon, serverStarted, additionalArgs);
   }
@@ -471,7 +475,8 @@
       Path site,
       Config baseConfig,
       Daemon daemon,
-      @Nullable InMemoryRepositoryManager inMemoryRepoManager)
+      @Nullable InMemoryRepositoryManager inMemoryRepoManager,
+      @Nullable AbstractIndexModule testIndexModule)
       throws Exception {
     Config cfg = desc.buildConfig(baseConfig);
     mergeTestConfig(cfg);
@@ -487,24 +492,11 @@
         "accountPatchReviewDb", null, "url", JdbcAccountPatchReviewStore.TEST_IN_MEMORY_URL);
 
     String configuredIndexBackend = cfg.getString("index", null, "type");
-    IndexType indexType;
-    if (configuredIndexBackend != null) {
-      // Explicitly configured index backend from gerrit.config trumps any other ways to configure
-      // index backends so that Reindex tests can be explicit about the backend they want to test
-      // against.
-      indexType = new IndexType(configuredIndexBackend);
-    } else {
-      // Allow configuring the index backend based on sys/env variables so that integration tests
-      // can be run against different index backends.
-      indexType = IndexType.fromEnvironment().orElse(new IndexType("fake"));
-    }
-    if (indexType.isLucene()) {
-      daemon.setIndexModule(
-          LuceneIndexModule.singleVersionAllLatest(
-              0, ReplicaUtil.isReplica(baseConfig), AutoFlush.ENABLED));
-    } else {
-      daemon.setIndexModule(FakeIndexModule.latestVersion(false));
-    }
+    IndexType indexType =
+        (configuredIndexBackend != null)
+            ? new IndexType(configuredIndexBackend)
+            : IndexType.fromEnvironment().orElse(new IndexType("fake"));
+    daemon.setIndexModule(createIndexModule(indexType, baseConfig, testIndexModule));
 
     daemon.setEnableHttpd(desc.httpd());
     daemon.setInMemory(true);
@@ -524,6 +516,17 @@
     return new GerritServer(desc, null, createTestInjector(daemon), daemon, null);
   }
 
+  private static AbstractIndexModule createIndexModule(
+      IndexType indexType, Config baseConfig, @Nullable AbstractIndexModule testIndexModule) {
+    if (testIndexModule != null) {
+      return testIndexModule;
+    }
+    return indexType.isLucene()
+        ? LuceneIndexModule.singleVersionAllLatest(
+            0, ReplicaUtil.isReplica(baseConfig), AutoFlush.ENABLED)
+        : FakeIndexModule.latestVersion(false);
+  }
+
   private static GerritServer startOnDisk(
       Description desc,
       Path site,
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
index 944f956..e4c6745 100644
--- a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -238,7 +238,8 @@
     private final boolean skipMergable;
 
     @Inject
-    FakeChangeIndex(
+    @VisibleForTesting
+    protected FakeChangeIndex(
         SitePaths sitePaths,
         ChangeData.Factory changeDataFactory,
         @Assisted Schema<ChangeData> schema,
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index c3688d6..2b0de12 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -286,6 +286,9 @@
             .setTitle("Move change to a different branch")
             .setVisible(false);
 
+    if (!moveEnabled) {
+      return description;
+    }
     Change change = rsrc.getChange();
     if (!change.isNew()) {
       return description;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index 65182db..458ae4d 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.entities.AccessSection;
@@ -141,7 +142,15 @@
         throw new IllegalStateException(e);
       }
 
-      md.setMessage("Review access change");
+      if (!Strings.isNullOrEmpty(input.message)) {
+        if (!input.message.endsWith("\n")) {
+          input.message += "\n";
+        }
+        md.setMessage(input.message);
+      } else {
+        md.setMessage("Review access change\n");
+      }
+
       md.setInsertChangeId(true);
       Change.Id changeId = Change.id(seq.nextChangeId());
       try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index b7fe46e..4fc2b86 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -252,7 +252,7 @@
 
     RefUpdate u = r.updateRef(refName);
     u.setForceUpdate(true);
-    u.setExpectedOldObjectId(r.exactRef(refName).getObjectId());
+    u.setExpectedOldObjectId(ref.getObjectId());
     u.setNewObjectId(ObjectId.zeroId());
     refDeletionValidator.validateRefOperation(
         projectState.getName(),
diff --git a/java/com/google/gerrit/server/submit/RebaseSorter.java b/java/com/google/gerrit/server/submit/RebaseSorter.java
index e960284..3645d3f 100644
--- a/java/com/google/gerrit/server/submit/RebaseSorter.java
+++ b/java/com/google/gerrit/server/submit/RebaseSorter.java
@@ -40,7 +40,7 @@
   private final CurrentUser caller;
   private final CodeReviewRevWalk rw;
   private final RevFlag canMergeFlag;
-  private final Set<RevCommit> uninterestingBranchTips;
+  private final RevCommit initialTip;
   private final Set<RevCommit> alreadyAccepted;
   private final Provider<InternalChangeQuery> queryProvider;
   private final Set<CodeReviewCommit> incoming;
@@ -48,7 +48,7 @@
   public RebaseSorter(
       CurrentUser caller,
       CodeReviewRevWalk rw,
-      Set<RevCommit> uninterestingBranchTips,
+      RevCommit initialTip,
       Set<RevCommit> alreadyAccepted,
       RevFlag canMergeFlag,
       Provider<InternalChangeQuery> queryProvider,
@@ -56,7 +56,7 @@
     this.caller = caller;
     this.rw = rw;
     this.canMergeFlag = canMergeFlag;
-    this.uninterestingBranchTips = uninterestingBranchTips;
+    this.initialTip = initialTip;
     this.alreadyAccepted = alreadyAccepted;
     this.queryProvider = queryProvider;
     this.incoming = incoming;
@@ -70,8 +70,8 @@
 
       rw.resetRetain(canMergeFlag);
       rw.markStart(n);
-      for (RevCommit uninterestingBranchTip : uninterestingBranchTips) {
-        rw.markUninteresting(uninterestingBranchTip);
+      if (initialTip != null) {
+        rw.markUninteresting(initialTip);
       }
 
       CodeReviewCommit c;
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategy.java b/java/com/google/gerrit/server/submit/SubmitStrategy.java
index c7b322e..bdda3fc5 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategy.java
@@ -22,7 +22,6 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.SubmissionId;
@@ -218,18 +217,11 @@
           projectCache.get(destBranch.project()).orElseThrow(illegalState(destBranch.project()));
       this.mergeSorter =
           new MergeSorter(caller, rw, alreadyAccepted, canMergeFlag, queryProvider, incoming);
-      Set<RevCommit> uninterestingBranchTips;
-      if (project.is(BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET)) {
-        RevCommit initialTip = mergeTip.getInitialTip();
-        uninterestingBranchTips = initialTip == null ? Set.of() : Set.of(initialTip);
-      } else {
-        uninterestingBranchTips = alreadyAccepted;
-      }
       this.rebaseSorter =
           new RebaseSorter(
               caller,
               rw,
-              uninterestingBranchTips,
+              mergeTip.getInitialTip(),
               alreadyAccepted,
               canMergeFlag,
               queryProvider,
diff --git a/javatests/com/google/gerrit/acceptance/api/project/AccessReviewIT.java b/javatests/com/google/gerrit/acceptance/api/project/AccessReviewIT.java
new file mode 100644
index 0000000..553650a
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/project/AccessReviewIT.java
@@ -0,0 +1,102 @@
+// 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.acceptance.api.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.access.AccessSectionInfo;
+import com.google.gerrit.extensions.api.access.PermissionInfo;
+import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
+import com.google.gerrit.extensions.api.access.ProjectAccessInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.inject.Inject;
+import java.util.HashMap;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+public class AccessReviewIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+
+  private Project.NameKey defaultMessageProject;
+  private Project.NameKey customMessageProject;
+
+  @Before
+  public void setUp() throws Exception {
+    defaultMessageProject = projectOperations.newProject().create();
+    customMessageProject = projectOperations.newProject().create();
+  }
+
+  @Test
+  public void createPermissionsChangeWithDefaultMessage() throws Exception {
+    ProjectAccessInput in = new ProjectAccessInput();
+    in.add = new HashMap<>();
+
+    AccessSectionInfo a = new AccessSectionInfo();
+    PermissionInfo p = new PermissionInfo(null, null);
+    p.rules =
+        ImmutableMap.of(
+            SystemGroupBackend.REGISTERED_USERS.get(),
+            new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false));
+    a.permissions = ImmutableMap.of("read", p);
+    in.add = ImmutableMap.of("refs/heads/*", a);
+
+    RestResponse rep =
+        adminRestSession.put("/projects/" + defaultMessageProject.get() + "/access:review", in);
+    rep.assertCreated();
+
+    List<ChangeInfo> result =
+        gApi.changes()
+            .query("project:" + defaultMessageProject.get() + " AND ref:refs/meta/config")
+            .get();
+    assertThat(Iterables.getOnlyElement(result).subject).isEqualTo("Review access change");
+  }
+
+  @Test
+  public void createPermissionsChangeWithCustomMessage() throws Exception {
+    ProjectAccessInput in = new ProjectAccessInput();
+    String customMessage = "UNIT-42: Allow registered users to read 'main' branch";
+    in.add = new HashMap<>();
+
+    AccessSectionInfo a = new AccessSectionInfo();
+    PermissionInfo p = new PermissionInfo(null, null);
+    p.rules =
+        ImmutableMap.of(
+            SystemGroupBackend.REGISTERED_USERS.get(),
+            new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false));
+    a.permissions = ImmutableMap.of("read", p);
+    in.add = ImmutableMap.of("refs/heads/main", a);
+    in.message = customMessage;
+
+    RestResponse rep =
+        adminRestSession.put("/projects/" + customMessageProject.get() + "/access:review", in);
+    rep.assertCreated();
+
+    List<ChangeInfo> result =
+        gApi.changes()
+            .query("project:" + customMessageProject.get() + " AND ref:refs/meta/config")
+            .get();
+
+    assertThat(Iterables.getOnlyElement(result).subject).isEqualTo(customMessage);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java
index 434071f..fee413a 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java
@@ -14,9 +14,29 @@
 
 package com.google.gerrit.acceptance.ssh;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import com.google.gerrit.index.IndexType;
+import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.project.ProjectIndex;
+import com.google.gerrit.index.testing.AbstractFakeIndex;
+import com.google.gerrit.index.testing.FakeIndexVersionManager;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.AbstractIndexModule;
+import com.google.gerrit.server.index.VersionManager;
+import com.google.gerrit.server.index.account.AccountIndex;
+import com.google.gerrit.server.index.change.ChangeIndex;
+import com.google.gerrit.server.index.change.ChangeIndexCollection;
+import com.google.gerrit.server.index.group.GroupIndex;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Map;
 import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
 
 /**
  * Tests for a defaulted custom index configuration. This unknown type is the opposite of {@link
@@ -24,10 +44,70 @@
  */
 public class CustomIndexIT extends AbstractIndexTests {
 
+  @Override
+  public Module createModule() {
+    return CustomIndexModule.latestVersion(false);
+  }
+
   @ConfigSuite.Default
   public static Config customIndexType() {
     Config config = new Config();
     config.setString("index", null, "type", "custom");
     return config;
   }
+
+  @Inject private ChangeIndexCollection changeIndex;
+
+  @Test
+  public void customIndexModuleIsBound() throws Exception {
+    assertThat(changeIndex.getSearchIndex()).isInstanceOf(CustomModuleFakeIndexChange.class);
+  }
+}
+
+class CustomIndexModule extends AbstractIndexModule {
+
+  public static CustomIndexModule latestVersion(boolean secondary) {
+    return new CustomIndexModule(null, -1 /* direct executor */, secondary);
+  }
+
+  private CustomIndexModule(Map<String, Integer> singleVersions, int threads, boolean secondary) {
+    super(singleVersions, threads, secondary);
+  }
+
+  @Override
+  protected Class<? extends AccountIndex> getAccountIndex() {
+    return AbstractFakeIndex.FakeAccountIndex.class;
+  }
+
+  @Override
+  protected Class<? extends ChangeIndex> getChangeIndex() {
+    return CustomModuleFakeIndexChange.class;
+  }
+
+  @Override
+  protected Class<? extends GroupIndex> getGroupIndex() {
+    return AbstractFakeIndex.FakeGroupIndex.class;
+  }
+
+  @Override
+  protected Class<? extends ProjectIndex> getProjectIndex() {
+    return AbstractFakeIndex.FakeProjectIndex.class;
+  }
+
+  @Override
+  protected Class<? extends VersionManager> getVersionManager() {
+    return FakeIndexVersionManager.class;
+  }
+}
+
+class CustomModuleFakeIndexChange extends AbstractFakeIndex.FakeChangeIndex {
+
+  @com.google.inject.Inject
+  CustomModuleFakeIndexChange(
+      SitePaths sitePaths,
+      ChangeData.Factory changeDataFactory,
+      @Assisted Schema<ChangeData> schema,
+      @GerritServerConfig Config cfg) {
+    super(sitePaths, changeDataFactory, schema, cfg);
+  }
 }
diff --git a/javatests/com/google/gerrit/metrics/dropwizard/BUILD b/javatests/com/google/gerrit/metrics/dropwizard/BUILD
index e236f30..42cf111 100644
--- a/javatests/com/google/gerrit/metrics/dropwizard/BUILD
+++ b/javatests/com/google/gerrit/metrics/dropwizard/BUILD
@@ -8,6 +8,7 @@
     deps = [
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/metrics/dropwizard",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib/mockito",
         "//lib/truth",
         "@dropwizard-core//jar",
diff --git a/javatests/com/google/gerrit/metrics/dropwizard/BucketedCallbackTest.java b/javatests/com/google/gerrit/metrics/dropwizard/BucketedCallbackTest.java
new file mode 100644
index 0000000..e87a208
--- /dev/null
+++ b/javatests/com/google/gerrit/metrics/dropwizard/BucketedCallbackTest.java
@@ -0,0 +1,101 @@
+// 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.dropwizard;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.gerrit.metrics.Description;
+import org.junit.Before;
+import org.junit.Test;
+
+public class BucketedCallbackTest {
+
+  private MetricRegistry registry;
+
+  private DropWizardMetricMaker metrics;
+
+  private static final String CODE_NAME = "name";
+  private static final String KEY_NAME = "foo";
+  private static final String OTHER_KEY_NAME = "bar";
+  private static final String COLLIDING_KEY_NAME1 = "foo1";
+  private static final String COLLIDING_KEY_NAME2 = "foo2";
+  private static final String COLLIDING_SUBMETRIC_NAME = "foocollision";
+
+  private String metricName(String fieldValues) {
+    return CODE_NAME + "/" + fieldValues;
+  }
+
+  @Before
+  public void setup() {
+    registry = new MetricRegistry();
+    metrics = new DropWizardMetricMaker(registry, null);
+  }
+
+  @Test
+  public void shouldRegisterMetricWithNewKey() {
+    BucketedCallback<Long> bc = new CallbackMetricTestImpl();
+
+    bc.getOrCreate(KEY_NAME);
+    assertThat(registry.getNames()).containsExactly(metricName(KEY_NAME));
+
+    bc.getOrCreate(OTHER_KEY_NAME);
+    assertThat(registry.getNames())
+        .containsExactly(metricName(KEY_NAME), metricName(OTHER_KEY_NAME));
+  }
+
+  @Test
+  public void shouldNotReRegisterPreviouslyRegisteredMetric() {
+    BucketedCallback<Long> bc = new CallbackMetricTestImpl();
+    bc.getOrCreate(KEY_NAME);
+    bc.getOrCreate(KEY_NAME);
+    assertThat(registry.getNames()).containsExactly(metricName(KEY_NAME));
+  }
+
+  @Test
+  public void shouldStoreKeyValueInCellsAndRegisterSubmetricName() {
+    BucketedCallback<Long> bc = new CallbackMetricTestImpl();
+    bc.getOrCreate(COLLIDING_KEY_NAME1);
+    assertThat(bc.getCells().keySet()).containsExactly(COLLIDING_KEY_NAME1);
+    assertThat(registry.getNames()).containsExactly(metricName(COLLIDING_SUBMETRIC_NAME));
+  }
+
+  @Test
+  public void shouldErrorIfKeyIsDifferentButNameCollides() {
+    BucketedCallback<Long> bc = new CallbackMetricTestImpl();
+    bc.getOrCreate(COLLIDING_KEY_NAME1);
+
+    assertThrows(IllegalArgumentException.class, () -> bc.getOrCreate(COLLIDING_KEY_NAME2));
+    assertThat(bc.getCells().keySet()).containsExactly(COLLIDING_KEY_NAME1);
+    assertThat(registry.getNames()).containsExactly(metricName(COLLIDING_SUBMETRIC_NAME));
+  }
+
+  private class CallbackMetricTestImpl extends BucketedCallback<Long> {
+
+    CallbackMetricTestImpl() {
+      super(metrics, registry, CODE_NAME, Long.class, new Description("description"));
+    }
+
+    @Override
+    String name(Object key) {
+      if (key.equals(COLLIDING_KEY_NAME1) || key.equals(COLLIDING_KEY_NAME2)) {
+        return COLLIDING_SUBMETRIC_NAME;
+      } else {
+        return key.toString();
+      }
+    }
+  }
+}
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
index 57f9ee6..3f99416 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
@@ -117,10 +117,12 @@
       return;
     }
 
-    this.restApiService.getAccountDetails(userId).then(details => {
-      this._accountDetails = details ?? undefined;
-      this._status = details?.status ?? '';
-    });
+    this.restApiService
+      .getAccountDetails(userId, () => {})
+      .then(details => {
+        this._accountDetails = details ?? undefined;
+        this._status = details?.status ?? '';
+      });
   }
 
   _computeDetail(
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index 23c9e7a..a36f17a 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -420,7 +420,7 @@
     return html`
       <gr-main-header
         id="mainHeader"
-        .searchQuery=${(this.params as SearchViewState)?.query ?? ''}
+        .searchQuery=${(this.params as SearchViewState)?.query}
         @mobile-search=${this.mobileSearchToggle}
         @show-keyboard-shortcuts=${this.showKeyboardShortcuts}
         .mobileSearchHidden=${!this.mobileSearch}
diff --git a/polygerrit-ui/app/elements/gr-app_test.ts b/polygerrit-ui/app/elements/gr-app_test.ts
index 6b97c85..15cfda6 100644
--- a/polygerrit-ui/app/elements/gr-app_test.ts
+++ b/polygerrit-ui/app/elements/gr-app_test.ts
@@ -30,7 +30,6 @@
     GrRouter.prototype,
     <any>'dispatchLocationChangeEvent'
   );
-
   setup(async () => {
     await fixture<GrApp>(html`<gr-app id="app"></gr-app>`);
   });
diff --git a/polygerrit-ui/app/utils/url-util.ts b/polygerrit-ui/app/utils/url-util.ts
index 8267638..2f9bc0c 100644
--- a/polygerrit-ui/app/utils/url-util.ts
+++ b/polygerrit-ui/app/utils/url-util.ts
@@ -34,7 +34,7 @@
   ) {
     return customLoginUrl.startsWith('http')
       ? customLoginUrl
-      : baseUrl + customLoginUrl;
+      : baseUrl + sanitizeRelativeUrl(customLoginUrl);
   } else {
     // Strip the canonical path from the path since needing canonical in
     // the path is unneeded and breaks the url.
@@ -73,6 +73,10 @@
   return range;
 }
 
+function sanitizeRelativeUrl(relativeUrl: string): string {
+  return relativeUrl.startsWith('/') ? relativeUrl : `/${relativeUrl}`;
+}
+
 export function prependOrigin(path: string): string {
   if (path.startsWith('http')) return path;
   if (path.startsWith('/')) return window.location.origin + path;
diff --git a/polygerrit-ui/app/utils/url-util_test.ts b/polygerrit-ui/app/utils/url-util_test.ts
index e36719d..e2ca617 100644
--- a/polygerrit-ui/app/utils/url-util_test.ts
+++ b/polygerrit-ui/app/utils/url-util_test.ts
@@ -76,6 +76,12 @@
       authConfig.auth_type = AuthType.HTTP_LDAP;
       assert.deepEqual(loginUrl(authConfig), customLoginUrl);
     });
+
+    test('auth.loginUrl is sanitized when defined as a relative url', () => {
+      authConfig.login_url = 'custom';
+      authConfig.auth_type = AuthType.HTTP;
+      assert.deepEqual(loginUrl(authConfig), '/custom');
+    });
   });
 
   suite('url encoding and decoding tests', () => {