Merge "Move LoadingStatus to types"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 6b1dea5..d91590e 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -828,8 +828,7 @@
 
 [[cache.name.maxAge]]cache.<name>.maxAge::
 +
-Maximum age to keep an entry in the cache. Entries are removed from
-the cache and refreshed from source data every maxAge interval.
+Maximum age to keep an entry in the cache.
 Values should use common unit suffixes to express their setting:
 +
 * s, sec, second, seconds
@@ -924,20 +923,6 @@
 +
 If 0 or negative, disk storage for the cache is disabled.
 
-[[cache.name.expireAfterWrite]]cache.<name>.expireAfterWrite::
-+
-Duration after which a cached value will be evicted and not
-read anymore.
-+
-Values should use common unit suffixes to express their setting:
-+
-* ms, milliseconds
-* s, sec, second, seconds
-* m, min, minute, minutes
-* h, hr, hour, hours
-+
-Disabled by default.
-
 [[cache.name.refreshAfterWrite]]cache.<name>.refreshAfterWrite::
 +
 Duration after which we asynchronously refresh the cached value.
@@ -2618,6 +2603,10 @@
 same `serverId` (i.e.: a Gerrit cluster).
 Unlike `instanceName` this value is not available in the email templates.
 
+The instance ID can also be configured by setting the Java system property
+`gerrit.instanceId` on startup. This will override the configuration in the
+gerrit.config.
+
 [[gerrit.instanceName]]gerrit.instanceName::
 +
 Short identifier for this Gerrit instance.
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index a76282e..f4238d1 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -82,9 +82,10 @@
 link:dev-build-plugins.html#_bundle_custom_plugin_in_release_war[bundling in release.war]
 and run `tools/eclipse/project.py`.
 
-If a plugin requires additional test dependencies (not available in the Gerrit), then in order to
-execute tests directly from Eclipse, that plugin must be also added to `CUSTOM_PLUGINS_TEST_DEPS`
-list in `tools/bzl/plugins.bzl` (note that `tools/eclipse/project.py` has to be run again).
+If a plugin requires additional test dependencies (not available in the Gerrit),
+then in order to execute tests directly from Eclipse, that plugin must be also
+added to `CUSTOM_PLUGINS_TEST_DEPS` list in `tools/bzl/plugins.bzl` and Eclipse
+project configuration needs to be updated by running `tools/eclipse/project.py`.
 
 == Java Versions
 
diff --git a/Documentation/pgm-reindex.txt b/Documentation/pgm-reindex.txt
index b74829d..183c132 100644
--- a/Documentation/pgm-reindex.txt
+++ b/Documentation/pgm-reindex.txt
@@ -39,6 +39,12 @@
 --show-cache-stats::
 	Show cache statistics at the end of program.
 
+--build-bloom-filter::
+	Whether to build bloom filters for H2 disk caches. When using fully
+	populated disk caches on large Gerrit sites, it is recommended that
+	bloom filters are disabled to improve performance.
+
+
 == CONTEXT
 The secondary index must be enabled. See
 link:config-gerrit.html#index.type[index.type].
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index 39929e1..3f50afe 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -280,9 +280,14 @@
 
 ** [[cherry-pick]]`Cherry-Pick`:
 +
-Allows to cherry-pick the change to another branch. The destination
-branch can be selected from a dialog. Cherry-picking a change creates a
-new open change on the selected destination branch.
+Allows to cherry-pick the change to another branch. The destination branch
+can be selected from a dialog. Cherry-picking a change creates a new open
+change on the selected destination branch. 'Cherry-pick committer email'
+drop-down is visible for single change cherry-picks when user has more than
+one email registered to their account. It is possible to select any of the
+registered emails to be used as the cherry-pick committer email. It defaults
+to source commit's committer email if it is a registered email of the calling
+user, else defaults to calling user's preferred email.
 +
 It is also possible to cherry-pick a change to the same branch. This is
 effectively the same as rebasing it to the current tip of the
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index a6ba8df..0539582 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -51,6 +51,8 @@
 import com.google.common.testing.FakeTicker;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.config.ConfigAnnotationParser;
+import com.google.gerrit.acceptance.config.GerritSystemProperty;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -437,6 +439,14 @@
         GerritServer.Description.forTestMethod(description, configName);
     testMethodDescription = methodDesc;
 
+    if (methodDesc.systemProperties() != null) {
+      ConfigAnnotationParser.parse(methodDesc.systemProperties());
+    }
+
+    if (methodDesc.systemProperty() != null) {
+      ConfigAnnotationParser.parse(methodDesc.systemProperty());
+    }
+
     testRequiresSsh = classDesc.useSshAnnotation() || methodDesc.useSshAnnotation();
     if (!testRequiresSsh) {
       baseConfig.setString("sshd", null, "listenAddress", "off");
@@ -701,6 +711,19 @@
       server.close();
       server = null;
     }
+
+    GerritServer.Description methodDesc =
+        GerritServer.Description.forTestMethod(description, configName);
+    if (methodDesc.systemProperties() != null) {
+      for (GerritSystemProperty sysProp : methodDesc.systemProperties().value()) {
+        System.clearProperty(sysProp.name());
+      }
+    }
+
+    if (methodDesc.systemProperty() != null) {
+      System.clearProperty(methodDesc.systemProperty().name());
+    }
+
     SystemReader.setInstance(oldSystemReader);
     oldSystemReader = null;
     // Set useDefaultTicker in afterTest, so the next beforeTest will use the default ticker
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 7c6df1e..73631e9 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -31,6 +31,8 @@
 import com.google.gerrit.acceptance.config.ConfigAnnotationParser;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.config.GerritConfigs;
+import com.google.gerrit.acceptance.config.GerritSystemProperties;
+import com.google.gerrit.acceptance.config.GerritSystemProperty;
 import com.google.gerrit.acceptance.config.GlobalPluginConfig;
 import com.google.gerrit.acceptance.config.GlobalPluginConfigs;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
@@ -137,6 +139,8 @@
           false, // @UseSystemTime is only valid on methods.
           get(UseClockStep.class, testDesc.getTestClass()),
           get(UseTimezone.class, testDesc.getTestClass()),
+          null, // @GerritSystemProperty is only valid on methods.
+          null, // @GerritSystemProperties is only valid on methods.
           null, // @GerritConfig is only valid on methods.
           null, // @GerritConfigs is only valid on methods.
           null, // @GlobalPluginConfig is only valid on methods.
@@ -179,6 +183,8 @@
           testDesc.getAnnotation(UseTimezone.class) != null
               ? testDesc.getAnnotation(UseTimezone.class)
               : get(UseTimezone.class, testDesc.getTestClass()),
+          testDesc.getAnnotation(GerritSystemProperty.class),
+          testDesc.getAnnotation(GerritSystemProperties.class),
           testDesc.getAnnotation(GerritConfig.class),
           testDesc.getAnnotation(GerritConfigs.class),
           testDesc.getAnnotation(GlobalPluginConfig.class),
@@ -234,6 +240,12 @@
     abstract UseTimezone useTimezone();
 
     @Nullable
+    abstract GerritSystemProperty systemProperty();
+
+    @Nullable
+    abstract GerritSystemProperties systemProperties();
+
+    @Nullable
     abstract GerritConfig config();
 
     @Nullable
@@ -249,6 +261,10 @@
       if (useClockStep() != null && useSystemTime()) {
         throw new IllegalStateException("Use either @UseClockStep or @UseSystemTime, not both");
       }
+      if (systemProperties() != null && systemProperty() != null) {
+        throw new IllegalStateException(
+            "Use either @GerritSystemProperties or @GerritSystemProperty, not both");
+      }
       if (configs() != null && config() != null) {
         throw new IllegalStateException("Use either @GerritConfigs or @GerritConfig, not both");
       }
diff --git a/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java b/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java
index 27ce857..fc6be03 100644
--- a/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java
+++ b/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java
@@ -47,6 +47,16 @@
     return cfg;
   }
 
+  public static void parse(GerritSystemProperties annotation) {
+    for (GerritSystemProperty prop : annotation.value()) {
+      parse(prop);
+    }
+  }
+
+  public static void parse(GerritSystemProperty annotation) {
+    System.setProperty(annotation.name(), annotation.value());
+  }
+
   @Nullable
   public static Map<String, Config> parse(GlobalPluginConfigs annotation) {
     if (annotation == null || annotation.value().length < 1) {
diff --git a/java/com/google/gerrit/acceptance/config/GerritSystemProperties.java b/java/com/google/gerrit/acceptance/config/GerritSystemProperties.java
new file mode 100644
index 0000000..cc6389c
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/config/GerritSystemProperties.java
@@ -0,0 +1,27 @@
+// 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.config;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({METHOD})
+@Retention(RUNTIME)
+public @interface GerritSystemProperties {
+  GerritSystemProperty[] value();
+}
diff --git a/java/com/google/gerrit/acceptance/config/GerritSystemProperty.java b/java/com/google/gerrit/acceptance/config/GerritSystemProperty.java
new file mode 100644
index 0000000..a2bf735
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/config/GerritSystemProperty.java
@@ -0,0 +1,33 @@
+// 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.config;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({METHOD})
+@Retention(RUNTIME)
+@Repeatable(GerritSystemProperties.class)
+public @interface GerritSystemProperty {
+  /** System property name. */
+  String name();
+
+  /** Value of the system property. */
+  String value() default "";
+}
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index be833ea..f0a8b89 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -147,8 +147,10 @@
   @Nullable
   String getRemoteDisplayname(HttpServletRequest req) {
     if (displaynameHeader != null) {
-      String raw = req.getHeader(displaynameHeader);
-      return emptyToNull(new String(raw.getBytes(ISO_8859_1), UTF_8));
+      String raw = emptyToNull(req.getHeader(displaynameHeader));
+      if (raw != null) {
+        return new String(raw.getBytes(ISO_8859_1), UTF_8);
+      }
     }
     return null;
   }
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index dcc6af6..5dfcef3 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -61,7 +61,7 @@
   protected static class Metrics {
     final Timer1<String> executionTime;
 
-    Metrics(MetricMaker metricMaker) {
+    protected Metrics(MetricMaker metricMaker) {
       executionTime =
           metricMaker.newTimer(
               "query/query_latency",
@@ -95,14 +95,14 @@
   private Set<String> requestedFields;
 
   protected QueryProcessor(
-      MetricMaker metricMaker,
+      Metrics metrics,
       SchemaDefinitions<T> schemaDef,
       IndexConfig indexConfig,
       IndexCollection<?, T, ? extends Index<?, T>> indexes,
       IndexRewriter<T> rewriter,
       String limitField,
       IntSupplier userQueryLimit) {
-    this.metrics = new Metrics(metricMaker);
+    this.metrics = metrics;
     this.schemaDef = schemaDef;
     this.indexConfig = indexConfig;
     this.indexes = indexes;
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index f22ceb5..bbba28c 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
 import com.google.gerrit.server.index.options.AutoFlush;
+import com.google.gerrit.server.index.options.BuildBloomFilter;
 import com.google.gerrit.server.index.options.IsFirstInsertForEntry;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.util.ReplicaUtil;
@@ -92,6 +93,9 @@
   @Option(name = "--show-cache-stats", usage = "Show cache statistics at the end.")
   private boolean showCacheStats;
 
+  @Option(name = "--build-bloom-filter", usage = "Build bloom filter for H2 disk caches.")
+  private boolean buildBloomFilter;
+
   private Injector dbInjector;
   private Injector sysInjector;
   private Injector cfgInjector;
@@ -209,6 +213,9 @@
             OptionalBinder.newOptionalBinder(binder(), IsFirstInsertForEntry.class)
                 .setBinding()
                 .toInstance(IsFirstInsertForEntry.YES);
+            OptionalBinder.newOptionalBinder(binder(), BuildBloomFilter.class)
+                .setBinding()
+                .toInstance(buildBloomFilter ? BuildBloomFilter.TRUE : BuildBloomFilter.FALSE);
           }
         });
     modules.add(new BatchProgramModule(dbInjector));
diff --git a/java/com/google/gerrit/server/approval/ApprovalsUtil.java b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
index 9b40a57..e64c273 100644
--- a/java/com/google/gerrit/server/approval/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
@@ -377,6 +377,11 @@
     return notes.load().getApprovals().onlyNonCopied();
   }
 
+  public ListMultimap<PatchSet.Id, PatchSetApproval> byChangeIncludingCopiedApprovals(
+      ChangeNotes notes) {
+    return notes.load().getApprovals().all();
+  }
+
   /**
    * Copies approvals to a new patch set.
    *
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 445d8a0..fdd55ac 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -14,11 +14,15 @@
 
 package com.google.gerrit.server.cache.h2;
 
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.server.cache.MemoryCacheFactory;
@@ -26,13 +30,17 @@
 import com.google.gerrit.server.cache.PersistentCacheDef;
 import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore;
 import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
+import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.options.BuildBloomFilter;
+import com.google.gerrit.server.index.options.IsFirstInsertForEntry;
 import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
 import com.google.gerrit.server.logging.LoggingContextAwareScheduledExecutorService;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -57,32 +65,43 @@
   private final ScheduledExecutorService cleanup;
   private final long h2CacheSize;
   private final boolean h2AutoServer;
+  private final boolean isOfflineReindex;
+  private final boolean buildBloomFilter;
 
   @Inject
   H2CacheFactory(
       MemoryCacheFactory memCacheFactory,
       @GerritServerConfig Config cfg,
       SitePaths site,
-      DynamicMap<Cache<?, ?>> cacheMap) {
+      DynamicMap<Cache<?, ?>> cacheMap,
+      @Nullable IsFirstInsertForEntry isFirstInsertForEntry,
+      @Nullable BuildBloomFilter buildBloomFilter) {
     super(memCacheFactory, cfg, site);
     h2CacheSize = cfg.getLong("cache", null, "h2CacheSize", -1);
     h2AutoServer = cfg.getBoolean("cache", null, "h2AutoServer", false);
     caches = new ArrayList<>();
     this.cacheMap = cacheMap;
+    this.isOfflineReindex =
+        isFirstInsertForEntry != null && isFirstInsertForEntry.equals(IsFirstInsertForEntry.YES);
+    this.buildBloomFilter =
+        !(buildBloomFilter != null && buildBloomFilter.equals(BuildBloomFilter.FALSE));
 
     if (diskEnabled) {
       executor =
           new LoggingContextAwareExecutorService(
               Executors.newFixedThreadPool(
                   1, new ThreadFactoryBuilder().setNameFormat("DiskCache-Store-%d").build()));
+
       cleanup =
-          new LoggingContextAwareScheduledExecutorService(
-              Executors.newScheduledThreadPool(
-                  1,
-                  new ThreadFactoryBuilder()
-                      .setNameFormat("DiskCache-Prune-%d")
-                      .setDaemon(true)
-                      .build()));
+          isOfflineReindex
+              ? null
+              : new LoggingContextAwareScheduledExecutorService(
+                  Executors.newScheduledThreadPool(
+                      1,
+                      new ThreadFactoryBuilder()
+                          .setNameFormat("DiskCache-Prune-%d")
+                          .setDaemon(true)
+                          .build()));
     } else {
       executor = null;
       cleanup = null;
@@ -94,9 +113,11 @@
     if (executor != null) {
       for (H2CacheImpl<?, ?> cache : caches) {
         executor.execute(cache::start);
-        @SuppressWarnings("unused")
-        Future<?> possiblyIgnoredError =
-            cleanup.schedule(() -> cache.prune(cleanup), 30, TimeUnit.SECONDS);
+        if (cleanup != null) {
+          @SuppressWarnings("unused")
+          Future<?> possiblyIgnoredError =
+              cleanup.schedule(() -> cache.prune(cleanup), 30, TimeUnit.SECONDS);
+        }
       }
     }
   }
@@ -105,7 +126,9 @@
   public void stop() {
     if (executor != null) {
       try {
-        cleanup.shutdownNow();
+        if (cleanup != null) {
+          cleanup.shutdownNow();
+        }
 
         List<Runnable> pending = executor.shutdownNow();
         if (executor.awaitTermination(15, TimeUnit.MINUTES)) {
@@ -183,6 +206,22 @@
     if (h2AutoServer) {
       url.append(";AUTO_SERVER=TRUE");
     }
+    Duration refreshAfterWrite = def.refreshAfterWrite();
+    if (has(def.configKey(), "refreshAfterWrite")) {
+      long refreshAfterWriteInSec =
+          ConfigUtil.getTimeUnit(config, "cache", def.configKey(), "refreshAfterWrite", 0, SECONDS);
+      if (refreshAfterWriteInSec != 0) {
+        refreshAfterWrite = Duration.ofSeconds(refreshAfterWriteInSec);
+      }
+    }
+    Duration expireAfterWrite = def.expireAfterWrite();
+    if (has(def.configKey(), "maxAge")) {
+      long expireAfterWriteInsec =
+          ConfigUtil.getTimeUnit(config, "cache", def.configKey(), "maxAge", 0, SECONDS);
+      if (expireAfterWriteInsec != 0) {
+        expireAfterWrite = Duration.ofSeconds(expireAfterWriteInsec);
+      }
+    }
     return new SqlStore<>(
         url.toString(),
         def.keyType(),
@@ -190,7 +229,12 @@
         def.valueSerializer(),
         def.version(),
         maxSize,
-        def.expireAfterWrite(),
-        def.expireFromMemoryAfterAccess());
+        expireAfterWrite,
+        refreshAfterWrite,
+        buildBloomFilter);
+  }
+
+  private boolean has(String name, String var) {
+    return !Strings.isNullOrEmpty(config.getString("cache", name, var));
   }
 }
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 8327b88..29bf0e6 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -365,6 +365,7 @@
     private final AtomicLong missCount = new AtomicLong();
     private volatile BloomFilter<K> bloomFilter;
     private int estimatedSize;
+    private boolean buildBloomFilter;
 
     SqlStore(
         String jdbcUrl,
@@ -374,7 +375,8 @@
         int version,
         long maxSize,
         @Nullable Duration expireAfterWrite,
-        @Nullable Duration refreshAfterWrite) {
+        @Nullable Duration refreshAfterWrite,
+        boolean buildBloomFilter) {
       this.url = jdbcUrl;
       this.keyType = createKeyType(keyType, keySerializer);
       this.valueSerializer = valueSerializer;
@@ -382,6 +384,7 @@
       this.maxSize = maxSize;
       this.expireAfterWrite = expireAfterWrite;
       this.refreshAfterWrite = refreshAfterWrite;
+      this.buildBloomFilter = buildBloomFilter;
 
       int cores = Runtime.getRuntime().availableProcessors();
       int keep = Math.min(cores, 16);
@@ -398,7 +401,7 @@
     }
 
     synchronized void open() {
-      if (bloomFilter == null) {
+      if (buildBloomFilter && bloomFilter == null) {
         bloomFilter = buildBloomFilter();
       }
     }
@@ -412,7 +415,7 @@
 
     boolean mightContain(K key) {
       BloomFilter<K> b = bloomFilter;
-      if (b == null) {
+      if (buildBloomFilter && b == null) {
         synchronized (this) {
           b = bloomFilter;
           if (b == null) {
diff --git a/java/com/google/gerrit/server/config/GerritInstanceIdProvider.java b/java/com/google/gerrit/server/config/GerritInstanceIdProvider.java
index 891ca76..6523f18 100644
--- a/java/com/google/gerrit/server/config/GerritInstanceIdProvider.java
+++ b/java/com/google/gerrit/server/config/GerritInstanceIdProvider.java
@@ -22,11 +22,14 @@
 /** Provides {@link GerritInstanceId} from {@code gerrit.instanceId}. */
 @Singleton
 public class GerritInstanceIdProvider implements Provider<String> {
+  public static final String INSTANCE_ID_SYSTEM_PROPERTY = "gerrit.instanceId";
   private final String instanceId;
 
   @Inject
   public GerritInstanceIdProvider(@GerritServerConfig Config cfg) {
-    instanceId = cfg.getString("gerrit", null, "instanceId");
+    instanceId =
+        System.getProperty(
+            INSTANCE_ID_SYSTEM_PROPERTY, cfg.getString("gerrit", null, "instanceId"));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/index/IndexModule.java b/java/com/google/gerrit/server/index/IndexModule.java
index fbe18ca..c0bd62f 100644
--- a/java/com/google/gerrit/server/index/IndexModule.java
+++ b/java/com/google/gerrit/server/index/IndexModule.java
@@ -53,6 +53,7 @@
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.gerrit.server.index.group.GroupIndexerImpl;
 import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
+import com.google.gerrit.server.index.options.BuildBloomFilter;
 import com.google.gerrit.server.index.options.IsFirstInsertForEntry;
 import com.google.gerrit.server.index.project.ProjectIndexDefinition;
 import com.google.gerrit.server.index.project.ProjectIndexerImpl;
@@ -155,6 +156,9 @@
     OptionalBinder.newOptionalBinder(binder(), IsFirstInsertForEntry.class)
         .setDefault()
         .toInstance(IsFirstInsertForEntry.NO);
+    OptionalBinder.newOptionalBinder(binder(), BuildBloomFilter.class)
+        .setDefault()
+        .toInstance(BuildBloomFilter.TRUE);
   }
 
   @Provides
diff --git a/java/com/google/gerrit/server/index/options/BuildBloomFilter.java b/java/com/google/gerrit/server/index/options/BuildBloomFilter.java
new file mode 100644
index 0000000..021f0fe
--- /dev/null
+++ b/java/com/google/gerrit/server/index/options/BuildBloomFilter.java
@@ -0,0 +1,21 @@
+// 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.server.index.options;
+
+/** This enum can be used to decide if bloom filters for H2 disk caches should be built. */
+public enum BuildBloomFilter {
+  TRUE,
+  FALSE
+}
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 0e37049..3d35321 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -156,7 +156,7 @@
               Permission.EDIT_TOPIC_NAME) // user can edit topic on a specific ref
           || getProjectControl().isAdmin();
     }
-    return refControl.canForceEditTopicName();
+    return refControl.canForceEditTopicName(isOwner());
   }
 
   /** Can this user toggle WorkInProgress state? */
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index eebaa8f..a706951 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -46,6 +46,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -65,6 +66,27 @@
     DefaultRefFilter create(ProjectControl projectControl);
   }
 
+  @Singleton
+  private static class Metrics {
+    final Counter0 fullFilterCount;
+    final Counter0 skipFilterCount;
+
+    @Inject
+    Metrics(MetricMaker metricMaker) {
+      fullFilterCount =
+          metricMaker.newCounter(
+              "permissions/ref_filter/full_filter_count",
+              new Description("Rate of full ref filter operations").setRate());
+      skipFilterCount =
+          metricMaker.newCounter(
+              "permissions/ref_filter/skip_filter_count",
+              new Description(
+                      "Rate of ref filter operations where we skip full evaluation"
+                          + " because the user can read all refs")
+                  .setRate());
+    }
+  }
+
   private final TagCache tagCache;
   private final PermissionBackend permissionBackend;
   private final RefVisibilityControl refVisibilityControl;
@@ -75,8 +97,7 @@
   private final @Nullable SearchingChangeCacheImpl searchingChangeDataProvider;
   private final ChangeData.Factory changeDataFactory;
   private final ChangeNotes.Factory changeNotesFactory;
-  private final Counter0 fullFilterCount;
-  private final Counter0 skipFilterCount;
+  private final Metrics metrics;
   private final boolean skipFullRefEvaluationIfAllRefsAreVisible;
 
   @Inject
@@ -85,7 +106,7 @@
       PermissionBackend permissionBackend,
       RefVisibilityControl refVisibilityControl,
       @GerritServerConfig Config config,
-      MetricMaker metricMaker,
+      Metrics metrics,
       @Nullable SearchingChangeCacheImpl searchingChangeDataProvider,
       ChangeData.Factory changeDataFactory,
       ChangeNotes.Factory changeNotesFactory,
@@ -104,17 +125,7 @@
     this.projectState = projectControl.getProjectState();
     this.permissionBackendForProject =
         permissionBackend.user(user).project(projectState.getNameKey());
-    this.fullFilterCount =
-        metricMaker.newCounter(
-            "permissions/ref_filter/full_filter_count",
-            new Description("Rate of full ref filter operations").setRate());
-    this.skipFilterCount =
-        metricMaker.newCounter(
-            "permissions/ref_filter/skip_filter_count",
-            new Description(
-                    "Rate of ref filter operations where we skip full evaluation"
-                        + " because the user can read all refs")
-                .setRate());
+    this.metrics = metrics;
   }
 
   /** Filters given refs and tags by visibility. */
@@ -202,13 +213,13 @@
     logger.atFinest().log("User has READ on refs/* = %s", hasReadOnRefsStar);
     if (skipFullRefEvaluationIfAllRefsAreVisible && !projectState.isAllUsers()) {
       if (hasReadOnRefsStar) {
-        skipFilterCount.increment();
+        metrics.skipFilterCount.increment();
         logger.atFinest().log(
             "Fast path, all refs are visible because user has READ on refs/*: %s", refs);
         return new AutoValue_DefaultRefFilter_Result(
             ImmutableList.copyOf(refs), ImmutableList.of());
       } else if (projectControl.allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG))) {
-        skipFilterCount.increment();
+        metrics.skipFilterCount.increment();
         refs = fastHideRefsMetaConfig(refs);
         logger.atFinest().log(
             "Fast path, all refs except %s are visible: %s", RefNames.REFS_CONFIG, refs);
@@ -217,7 +228,7 @@
       }
     }
     logger.atFinest().log("Doing full ref filtering");
-    fullFilterCount.increment();
+    metrics.fullFilterCount.increment();
 
     boolean hasAccessDatabase =
         permissionBackend
diff --git a/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java b/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
index 0e5ff48..ae72b06 100644
--- a/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
+++ b/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -88,7 +87,11 @@
               try {
                 return forProject.change(cd).test(ChangePermission.READ);
               } catch (PermissionBackendException e) {
-                throw new StorageException(e);
+                // This is almost the same as the message .testOrFalse() would log, but with the
+                // added context of the change and coming from this class
+                logger.atWarning().withCause(e).log(
+                    "Cannot test read permission for %s; assuming not visible", cd);
+                return false;
               }
             })
         .forEach(
@@ -147,7 +150,8 @@
                   })
               .filter(Objects::nonNull);
     } catch (IOException e) {
-      throw new StorageException(e);
+      logger.atWarning().withCause(e).log("Unable to scanChangeIds for %s", projectName);
+      return Stream.empty();
     }
     return cds;
   }
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index a3a041b..aba9522 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -162,8 +162,8 @@
   }
 
   /** Returns true if this user can force edit topic names. */
-  boolean canForceEditTopicName() {
-    return canPerform(Permission.EDIT_TOPIC_NAME, false, true);
+  boolean canForceEditTopicName(boolean isChangeOwner) {
+    return canPerform(Permission.EDIT_TOPIC_NAME, isChangeOwner, true);
   }
 
   /** Returns true if this user can delete changes. */
diff --git a/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java b/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java
index df2e1cf..47d43b9 100644
--- a/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java
+++ b/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java
@@ -92,6 +92,11 @@
   }
 
   @Override
+  public String toString() {
+    return "Project List Cache Warmer";
+  }
+
+  @Override
   public void run() {
     logger.atFine().log("Loading project_list cache");
     cache.refreshProjectList();
diff --git a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 1fe8641..62cb797 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.rules.PrologSubmitRuleUtil;
 import com.google.gerrit.server.rules.SubmitRule;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import com.google.inject.assistedinject.Assisted;
 import java.util.List;
 import java.util.Optional;
@@ -48,41 +49,51 @@
 public class SubmitRuleEvaluator {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private final ProjectCache projectCache;
-  private final PrologSubmitRuleUtil prologSubmitRuleUtil;
-  private final PluginSetContext<SubmitRule> submitRules;
-  private final Timer0 submitRuleEvaluationLatency;
-  private final Timer0 submitTypeEvaluationLatency;
-  private final SubmitRuleOptions opts;
-  private final CallerFinder callerFinder;
-
   public interface Factory {
     /** Returns a new {@link SubmitRuleEvaluator} with the specified options */
     SubmitRuleEvaluator create(SubmitRuleOptions options);
   }
 
+  @Singleton
+  private static class Metrics {
+    final Timer0 submitRuleEvaluationLatency;
+    final Timer0 submitTypeEvaluationLatency;
+
+    @Inject
+    Metrics(MetricMaker metricMaker) {
+      submitRuleEvaluationLatency =
+          metricMaker.newTimer(
+              "change/submit_rule_evaluation",
+              new Description("Latency for evaluating submit rules on a change.")
+                  .setCumulative()
+                  .setUnit(Units.MILLISECONDS));
+      submitTypeEvaluationLatency =
+          metricMaker.newTimer(
+              "change/submit_type_evaluation",
+              new Description("Latency for evaluating the submit type on a change.")
+                  .setCumulative()
+                  .setUnit(Units.MILLISECONDS));
+    }
+  }
+
+  private final ProjectCache projectCache;
+  private final PrologSubmitRuleUtil prologSubmitRuleUtil;
+  private final PluginSetContext<SubmitRule> submitRules;
+  private final Metrics metrics;
+  private final SubmitRuleOptions opts;
+  private final CallerFinder callerFinder;
+
   @Inject
   private SubmitRuleEvaluator(
       ProjectCache projectCache,
       PrologSubmitRuleUtil prologSubmitRuleUtil,
       PluginSetContext<SubmitRule> submitRules,
-      MetricMaker metricMaker,
+      Metrics metrics,
       @Assisted SubmitRuleOptions options) {
     this.projectCache = projectCache;
     this.prologSubmitRuleUtil = prologSubmitRuleUtil;
     this.submitRules = submitRules;
-    this.submitRuleEvaluationLatency =
-        metricMaker.newTimer(
-            "change/submit_rule_evaluation",
-            new Description("Latency for evaluating submit rules on a change.")
-                .setCumulative()
-                .setUnit(Units.MILLISECONDS));
-    this.submitTypeEvaluationLatency =
-        metricMaker.newTimer(
-            "change/submit_type_evaluation",
-            new Description("Latency for evaluating the submit type on a change.")
-                .setCumulative()
-                .setUnit(Units.MILLISECONDS));
+    this.metrics = metrics;
 
     this.opts = options;
 
@@ -106,7 +117,7 @@
     logger.atFine().log(
         "Evaluate submit rules for change %d (caller: %s)",
         cd.change().getId().get(), callerFinder.findCallerLazy());
-    try (Timer0.Context ignored = submitRuleEvaluationLatency.start()) {
+    try (Timer0.Context ignored = metrics.submitRuleEvaluationLatency.start()) {
       Change change;
       ProjectState projectState;
       try {
@@ -173,7 +184,7 @@
    * @return record from the evaluated rules.
    */
   public SubmitTypeRecord getSubmitType(ChangeData cd) {
-    try (Timer0.Context ignored = submitTypeEvaluationLatency.start()) {
+    try (Timer0.Context ignored = metrics.submitTypeEvaluationLatency.start()) {
       try {
         Project.NameKey name = cd.project();
         Optional<ProjectState> project = projectCache.get(name);
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java b/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
index eef913e..d812eef 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 /**
  * Query processor for the account index.
@@ -46,6 +47,14 @@
   private final Sequences sequences;
   private final IndexConfig indexConfig;
 
+  @Singleton
+  protected static class AccountQueryMetrics extends QueryProcessor.Metrics {
+    @Inject
+    protected AccountQueryMetrics(MetricMaker metricMaker) {
+      super(metricMaker);
+    }
+  }
+
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
     checkState(
@@ -57,14 +66,14 @@
   protected AccountQueryProcessor(
       Provider<CurrentUser> userProvider,
       AccountLimits.Factory limitsFactory,
-      MetricMaker metricMaker,
+      AccountQueryMetrics accountQueryMetrics,
       IndexConfig indexConfig,
       AccountIndexCollection indexes,
       AccountIndexRewriter rewriter,
       AccountControl.Factory accountControlFactory,
       Sequences sequences) {
     super(
-        metricMaker,
+        accountQueryMetrics,
         AccountSchemaDefinitions.INSTANCE,
         indexConfig,
         indexes,
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 81aad7f..c5a8794 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -379,6 +379,8 @@
   private PatchSet currentPatchSet;
   private Collection<PatchSet> patchSets;
   private ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals;
+
+  private ListMultimap<PatchSet.Id, PatchSetApproval> allApprovalsWithCopied;
   private List<PatchSetApproval> currentApprovals;
   private List<String> currentFiles;
   private Optional<DiffSummary> diffSummary;
@@ -868,6 +870,16 @@
     return allApprovals;
   }
 
+  public ListMultimap<PatchSet.Id, PatchSetApproval> conditionallyLoadApprovalsWithCopied() {
+    if (allApprovalsWithCopied == null) {
+      if (!lazyload()) {
+        return ImmutableListMultimap.of();
+      }
+      allApprovalsWithCopied = approvalsUtil.byChangeIncludingCopiedApprovals(notes());
+    }
+    return allApprovalsWithCopied;
+  }
+
   /* @return legacy submit ('SUBM') approval label */
   // TODO(mariasavtchouk): Deprecate legacy submit label,
   // see com.google.gerrit.entities.LabelId.LEGACY_SUBMIT_NAME
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index b7dc127..3097224 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -43,6 +43,7 @@
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -66,6 +67,14 @@
   private final Sequences sequences;
   private final IndexConfig indexConfig;
 
+  @Singleton
+  protected static class ChangeQueryMetrics extends QueryProcessor.Metrics {
+    @Inject
+    protected ChangeQueryMetrics(MetricMaker metricMaker) {
+      super(metricMaker);
+    }
+  }
+
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
     checkState(
@@ -77,7 +86,7 @@
   ChangeQueryProcessor(
       Provider<CurrentUser> userProvider,
       AccountLimits.Factory limitsFactory,
-      MetricMaker metricMaker,
+      ChangeQueryMetrics changeQueryMetrics,
       IndexConfig indexConfig,
       ChangeIndexCollection indexes,
       ChangeIndexRewriter rewriter,
@@ -85,7 +94,7 @@
       ChangeIsVisibleToPredicate.Factory changeIsVisibleToPredicateFactory,
       DynamicSet<ChangePluginDefinedInfoFactory> changePluginDefinedInfoFactories) {
     super(
-        metricMaker,
+        changeQueryMetrics,
         ChangeSchemaDefinitions.INSTANCE,
         indexConfig,
         indexes,
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 961404a..d21f5b6 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -303,7 +303,7 @@
           rw,
           c,
           d.patchSets(),
-          includeApprovals ? d.approvals().asMap() : null,
+          includeApprovals ? d.conditionallyLoadApprovalsWithCopied().asMap() : null,
           includeFiles,
           d.change(),
           labelTypes,
diff --git a/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java b/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
index 344a978..74c8d39 100644
--- a/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 /**
  * Query processor for the group index.
@@ -47,6 +48,14 @@
   private final Sequences sequences;
   private final IndexConfig indexConfig;
 
+  @Singleton
+  protected static class GroupQueryMetrics extends QueryProcessor.Metrics {
+    @Inject
+    protected GroupQueryMetrics(MetricMaker metricMaker) {
+      super(metricMaker);
+    }
+  }
+
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
     checkState(
@@ -58,14 +67,14 @@
   protected GroupQueryProcessor(
       Provider<CurrentUser> userProvider,
       AccountLimits.Factory limitsFactory,
-      MetricMaker metricMaker,
+      GroupQueryMetrics groupQueryMetrics,
       IndexConfig indexConfig,
       GroupIndexCollection indexes,
       GroupIndexRewriter rewriter,
       GroupControl.GenericFactory groupControlFactory,
       Sequences sequences) {
     super(
-        metricMaker,
+        groupQueryMetrics,
         GroupSchemaDefinitions.INSTANCE,
         indexConfig,
         indexes,
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java b/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
index 3877c25..ddc7ccc 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 /**
  * Query processor for the project index.
@@ -49,6 +50,14 @@
   private final ProjectCache projectCache;
   private final IndexConfig indexConfig;
 
+  @Singleton
+  protected static class ProjectQueryMetrics extends QueryProcessor.Metrics {
+    @Inject
+    protected ProjectQueryMetrics(MetricMaker metricMaker) {
+      super(metricMaker);
+    }
+  }
+
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
     checkState(
@@ -60,14 +69,14 @@
   protected ProjectQueryProcessor(
       Provider<CurrentUser> userProvider,
       AccountLimits.Factory limitsFactory,
-      MetricMaker metricMaker,
+      ProjectQueryMetrics projectQueryMetrics,
       IndexConfig indexConfig,
       ProjectIndexCollection indexes,
       ProjectIndexRewriter rewriter,
       PermissionBackend permissionBackend,
       ProjectCache projectCache) {
     super(
-        metricMaker,
+        projectQueryMetrics,
         ProjectSchemaDefinitions.INSTANCE,
         indexConfig,
         indexes,
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 39165d0..9e0eac7 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -52,6 +52,7 @@
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritInstanceId;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.documentation.QueryDocumentationExecutor;
@@ -92,6 +93,7 @@
   private final ProjectCache projectCache;
   private final AgreementJson agreementJson;
   private final SitePaths sitePaths;
+  private final @Nullable @GerritInstanceId String instanceId;
 
   @Inject
   public GetServerInfo(
@@ -113,7 +115,8 @@
       QueryDocumentationExecutor docSearcher,
       ProjectCache projectCache,
       AgreementJson agreementJson,
-      SitePaths sitePaths) {
+      SitePaths sitePaths,
+      @Nullable @GerritInstanceId String instanceId) {
     this.config = config;
     this.accountVisibilityProvider = accountVisibilityProvider;
     this.accountDefaultDisplayName = accountDefaultDisplayName;
@@ -133,6 +136,7 @@
     this.projectCache = projectCache;
     this.agreementJson = agreementJson;
     this.sitePaths = sitePaths;
+    this.instanceId = instanceId;
   }
 
   @Override
@@ -289,7 +293,7 @@
     info.editGpgKeys =
         toBoolean(enableSignedPush && config.getBoolean("gerrit", null, "editGpgKeys", true));
     info.primaryWeblinkName = config.getString("gerrit", null, "primaryWeblinkName");
-    info.instanceId = config.getString("gerrit", null, "instanceId");
+    info.instanceId = instanceId;
     info.defaultBranch = config.getString("gerrit", null, "defaultBranch");
     return info;
   }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index 4fc2b86..388946e 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -117,9 +117,12 @@
           .check(RefPermission.DELETE);
 
       try (Repository repository = repoManager.openRepository(projectState.getNameKey())) {
-        RefUpdate.Result result;
+        Ref refObj = repository.exactRef(ref);
+        if (refObj == null) {
+          throw new ResourceConflictException(String.format("ref %s doesn't exist", ref));
+        }
         RefUpdate u = repository.updateRef(ref);
-        u.setExpectedOldObjectId(repository.exactRef(ref).getObjectId());
+        u.setExpectedOldObjectId(refObj.getObjectId());
         u.setNewObjectId(ObjectId.zeroId());
         u.setForceUpdate(true);
         refDeletionValidator.validateRefOperation(
@@ -127,7 +130,7 @@
             identifiedUser.get(),
             u,
             /* pushOptions */ ImmutableListMultimap.of());
-        result = u.delete();
+        RefUpdate.Result result = u.delete();
 
         switch (result) {
           case NEW:
diff --git a/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java b/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java
index 1086626..2f96915 100644
--- a/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java
+++ b/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java
@@ -39,7 +39,9 @@
           sshDaemon,
           (sshId, sshSession, abstractSession, ioSession) -> {
             CurrentUser sessionUser = sshSession.getUser();
-            if (sessionUser.isIdentifiedUser() && sessionUser.getAccountId().get() == id) {
+            if (sessionUser != null
+                && sessionUser.isIdentifiedUser()
+                && sessionUser.getAccountId().get() == id) {
               logger.atInfo().log(
                   "Disconnecting SSH session %s because user %s(%d) got deactivated",
                   abstractSession, sessionUser.getLoggableName(), id);
diff --git a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 4f23d1d..f42eb5c 100644
--- a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -27,6 +27,8 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.restapi.change.DeleteReviewer;
 import com.google.gerrit.server.restapi.change.PostReviewers;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryableAction;
 import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -89,6 +91,8 @@
 
   @Inject private ChangeArgumentParser changeArgumentParser;
 
+  @Inject private RetryHelper retryHelper;
+
   private Set<Account.Id> toRemove = new HashSet<>();
 
   private Map<Change.Id, ChangeResource> changes = new LinkedHashMap<>();
@@ -121,7 +125,15 @@
       ReviewerResource rsrc = reviewerFactory.create(changeRsrc, reviewer);
       String error = null;
       try {
-        deleteReviewer.apply(rsrc, new DeleteReviewerInput());
+        retryHelper
+            .action(
+                RetryableAction.ActionType.CHANGE_UPDATE,
+                "removeReviewers",
+                () -> {
+                  deleteReviewer.apply(rsrc, new DeleteReviewerInput());
+                  return null;
+                })
+            .call();
       } catch (ResourceNotFoundException e) {
         error = String.format("could not remove %s: not found", reviewer);
       } catch (Exception e) {
@@ -139,15 +151,26 @@
       ReviewerInput input = new ReviewerInput();
       input.reviewer = reviewer;
       input.confirmed = true;
-      String error;
+      var error =
+          new Object() {
+            String value;
+          };
       try {
-        error = postReviewers.apply(changeRsrc, input).value().error;
+        retryHelper
+            .action(
+                RetryableAction.ActionType.CHANGE_UPDATE,
+                "applyReview",
+                () -> {
+                  error.value = postReviewers.apply(changeRsrc, input).value().error;
+                  return null;
+                })
+            .call();
       } catch (Exception e) {
-        error = String.format("could not add %s: %s", reviewer, e.getMessage());
+        error.value = String.format("could not add %s: %s", reviewer, e.getMessage());
       }
-      if (error != null) {
+      if (error.value != null) {
         ok = false;
-        writeError("error", error);
+        writeError("error", error.value);
       }
     }
 
diff --git a/java/com/google/gerrit/sshd/commands/SetTopicCommand.java b/java/com/google/gerrit/sshd/commands/SetTopicCommand.java
index 244fdbe..b590740 100644
--- a/java/com/google/gerrit/sshd/commands/SetTopicCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetTopicCommand.java
@@ -16,13 +16,13 @@
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.changes.TopicInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.SetTopicOp;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.gerrit.server.restapi.change.PutTopic;
 import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -34,9 +34,8 @@
 
 @CommandMetaData(name = "set-topic", description = "Set the topic for one or more changes")
 public class SetTopicCommand extends SshCommand {
-  private final BatchUpdate.Factory updateFactory;
   private final ChangeArgumentParser changeArgumentParser;
-  private final SetTopicOp.Factory topicOpFactory;
+  private final PutTopic putTopic;
 
   private Map<Change.Id, ChangeResource> changes = new LinkedHashMap<>();
 
@@ -62,17 +61,14 @@
   private String topic;
 
   @Inject
-  SetTopicCommand(
-      BatchUpdate.Factory updateFactory,
-      ChangeArgumentParser changeArgumentParser,
-      SetTopicOp.Factory topicOpFactory) {
-    this.updateFactory = updateFactory;
+  SetTopicCommand(ChangeArgumentParser changeArgumentParser, PutTopic putTopic) {
     this.changeArgumentParser = changeArgumentParser;
-    this.topicOpFactory = topicOpFactory;
+    this.putTopic = putTopic;
   }
 
   @Override
   public void run() throws Exception {
+    boolean ok = true;
     if (topic != null) {
       topic = topic.trim();
     }
@@ -83,11 +79,28 @@
     }
 
     for (ChangeResource r : changes.values()) {
-      SetTopicOp op = topicOpFactory.create(topic);
-      try (BatchUpdate u = updateFactory.create(r.getChange().getProject(), user, TimeUtil.now())) {
-        u.addOp(r.getId(), op);
-        u.execute();
+      TopicInput input = new TopicInput();
+      input.topic = topic;
+      try {
+        putTopic.apply(r, input);
+      } catch (ResourceNotFoundException e) {
+        ok = false;
+        writeError(
+            "error",
+            String.format(
+                "could not add topic to change %d: not found", r.getChange().getChangeId()));
+      } catch (Exception e) {
+        ok = false;
+        writeError(
+            "error",
+            String.format(
+                "could not add topic to change %d: %s",
+                r.getChange().getChangeId(), e.getMessage()));
       }
     }
+
+    if (!ok) {
+      throw die("one or more updates failed");
+    }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/config/GerritInstanceIdIT.java b/javatests/com/google/gerrit/acceptance/config/GerritInstanceIdIT.java
index 0dd6a83..d5e9351 100644
--- a/javatests/com/google/gerrit/acceptance/config/GerritInstanceIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/config/GerritInstanceIdIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.config.GerritInstanceIdProvider.INSTANCE_ID_SYSTEM_PROPERTY;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import org.junit.Test;
@@ -28,6 +29,13 @@
   }
 
   @Test
+  @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
+  @GerritSystemProperty(name = INSTANCE_ID_SYSTEM_PROPERTY, value = "sysPropInstanceId")
+  public void instanceIdSystemPropertyOverridesConfig() {
+    assertThat(instanceId).isEqualTo("sysPropInstanceId");
+  }
+
+  @Test
   public void shouldReturnNullWhenNotDefined() {
     assertThat(instanceId).isNull();
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 364ce84..b8b63e6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -15,12 +15,14 @@
 package com.google.gerrit.acceptance.rest.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.config.GerritInstanceIdProvider.INSTANCE_ID_SYSTEM_PROPERTY;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.config.GerritSystemProperty;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.extensions.api.plugins.InstallPluginInput;
 import com.google.gerrit.extensions.client.AccountFieldName;
@@ -225,4 +227,11 @@
     ServerInfo i = gApi.config().server().getInfo();
     assertThat(i.download.schemes).isEmpty();
   }
+
+  @Test
+  @GerritSystemProperty(name = INSTANCE_ID_SYSTEM_PROPERTY, value = "sysPropInstanceId")
+  public void instanceIdFromSystemProperty() throws Exception {
+    ServerInfo i = gApi.config().server().getInfo();
+    assertThat(i.gerrit.instanceId).isEqualTo("sysPropInstanceId");
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
index 912c464..8f4458d 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
@@ -24,6 +24,8 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
@@ -325,6 +327,33 @@
     }
   }
 
+  @Test
+  public void allApprovalsAllPatchSetsOptionsWithCopyConditionJSON() throws Exception {
+    // Copy min Code-Review votes
+    try (ProjectConfigUpdate u = updateProject(Project.NameKey.parse("All-Projects"))) {
+      u.getConfig().updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyCondition("is:MIN"));
+      u.save();
+    }
+
+    // Create a change and add Code-Review -2 on first patch-set
+    String changeId = createChange().getChangeId();
+    gApi.changes().id(changeId).current().review(ReviewInput.reject());
+
+    // Create second patch-set
+    amendChange(changeId);
+
+    // Assert that second patch-set has Code-Review -2 vote
+    List<ChangeAttribute> changes =
+        executeSuccessfulQuery("--all-approvals --patch-sets " + changeId);
+    assertThat(changes).hasSize(1);
+    assertThat(changes.get(0).patchSets).hasSize(2);
+    assertThat(changes.get(0).patchSets.get(1).approvals).isNotNull();
+    assertThat(changes.get(0).patchSets.get(1).approvals).hasSize(1);
+    assertThat(changes.get(0).patchSets.get(1).approvals.get(0).type)
+        .isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(changes.get(0).patchSets.get(1).approvals.get(0).value).isEqualTo("-2");
+  }
+
   protected static class SamplePluginModule extends AbstractModule {
     @Override
     public void configure() {
diff --git a/javatests/com/google/gerrit/httpd/auth/container/HttpAuthFilterTest.java b/javatests/com/google/gerrit/httpd/auth/container/HttpAuthFilterTest.java
new file mode 100644
index 0000000..a5f8349
--- /dev/null
+++ b/javatests/com/google/gerrit/httpd/auth/container/HttpAuthFilterTest.java
@@ -0,0 +1,78 @@
+// 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.httpd.auth.container;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doReturn;
+
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.StrictStubs.class)
+public class HttpAuthFilterTest {
+
+  private static String DISPLAYNAME_HEADER = "displaynameHeader";
+  private static String DISPLAYNAME = "displayname";
+
+  @Mock private DynamicItem<WebSession> webSession;
+  @Mock private ExternalIdKeyFactory externalIdKeyFactory;
+  @Mock private AuthConfig authConfig;
+
+  @Test
+  public void getRemoteDisplaynameShouldReturnDisplaynameHeaderWhenHeaderIsConfiguredAndSet()
+      throws IOException {
+    doReturn(DISPLAYNAME_HEADER).when(authConfig).getHttpDisplaynameHeader();
+    HttpAuthFilter httpAuthFilter =
+        new HttpAuthFilter(webSession, authConfig, externalIdKeyFactory);
+
+    FakeHttpServletRequest req = new FakeHttpServletRequest();
+    req.addHeader(DISPLAYNAME_HEADER, DISPLAYNAME);
+
+    assertThat(httpAuthFilter.getRemoteDisplayname(req)).isEqualTo(DISPLAYNAME);
+  }
+
+  @Test
+  public void getRemoteDisplaynameShouldReturnNullWhenDisplaynameHeaderIsConfiguredAndNotSet()
+      throws IOException {
+    doReturn(DISPLAYNAME_HEADER).when(authConfig).getHttpDisplaynameHeader();
+    HttpAuthFilter httpAuthFilter =
+        new HttpAuthFilter(webSession, authConfig, externalIdKeyFactory);
+
+    FakeHttpServletRequest req = new FakeHttpServletRequest();
+
+    assertThat(httpAuthFilter.getRemoteDisplayname(req)).isNull();
+  }
+
+  @Test
+  public void getRemoteDisplaynameShouldReturnNullWhenDisplaynameHeaderIsConfiguredAndEmpty()
+      throws IOException {
+    doReturn(DISPLAYNAME_HEADER).when(authConfig).getHttpDisplaynameHeader();
+    HttpAuthFilter httpAuthFilter =
+        new HttpAuthFilter(webSession, authConfig, externalIdKeyFactory);
+
+    FakeHttpServletRequest req = new FakeHttpServletRequest();
+    req.addHeader(DISPLAYNAME_HEADER, "");
+
+    assertThat(httpAuthFilter.getRemoteDisplayname(req)).isNull();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
index 16fd4ca..8c4eb08 100644
--- a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -71,7 +71,8 @@
         version,
         1 << 20,
         expireAfterWrite,
-        refreshAfterWrite);
+        refreshAfterWrite,
+        true);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/permissions/RefControlTest.java b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
index c5bef59..43b0eba 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -981,6 +981,20 @@
   }
 
   @Test
+  public void changeOwnerEditTopicName() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(EDIT_TOPIC_NAME).ref("refs/heads/*").group(CHANGE_OWNER).force(true))
+        .update();
+
+    ProjectControl u = user(localKey, DEVS);
+    assertWithMessage("u can edit topic name")
+        .that(u.controlForRef("refs/heads/master").canForceEditTopicName(true))
+        .isTrue();
+  }
+
+  @Test
   public void unblockForceEditTopicName() throws Exception {
     projectOperations
         .project(localKey)
@@ -991,7 +1005,7 @@
 
     ProjectControl u = user(localKey, DEVS);
     assertWithMessage("u can edit topic name")
-        .that(u.controlForRef("refs/heads/master").canForceEditTopicName())
+        .that(u.controlForRef("refs/heads/master").canForceEditTopicName(false))
         .isTrue();
   }
 
@@ -1010,7 +1024,7 @@
 
     ProjectControl u = user(localKey, REGISTERED_USERS);
     assertWithMessage("u can't edit topic name")
-        .that(u.controlForRef("refs/heads/master").canForceEditTopicName())
+        .that(u.controlForRef("refs/heads/master").canForceEditTopicName(false))
         .isFalse();
   }
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 0be80bb..8f835de 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -1610,6 +1610,7 @@
         base: el.baseCommit ? el.baseCommit : null,
         message: el.message,
         allow_conflicts: conflicts,
+        committer_email: el.committerEmail ? el.committerEmail : null,
       }
     );
   }
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index 631d946..15500de 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -1000,6 +1000,7 @@
             base: null,
             message: 'foo message',
             allow_conflicts: false,
+            committer_email: null,
           },
         ]);
       });
@@ -1048,6 +1049,7 @@
             base: null,
             message: 'foo message',
             allow_conflicts: true,
+            committer_email: null,
           },
         ]);
       });
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
index 73521a6..1285664 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
@@ -20,6 +20,7 @@
 import {createSearchUrl} from '../../../models/views/search';
 import {getBrowseCommitWeblink} from '../../../utils/weblink-util';
 import {shorten} from '../../../utils/patch-set-util';
+import {when} from 'lit/directives/when.js';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -31,7 +32,10 @@
 export class GrCommitInfo extends LitElement {
   // TODO(TS): Maybe limit to StandaloneCommitInfo.
   @property({type: Object})
-  commitInfo?: CommitInfo;
+  commitInfo?: Partial<CommitInfo>;
+
+  @property({type: Boolean})
+  showCopyButton = true;
 
   @state() serverConfig?: ServerInfo;
 
@@ -45,6 +49,9 @@
           align-items: center;
           display: flex;
         }
+        gr-weblink {
+          margin-right: 0;
+        }
       `,
     ];
   }
@@ -63,13 +70,18 @@
     if (!commit) return nothing;
     return html` <div class="container">
       <gr-weblink imageAndText .info=${this.getWeblink(commit)}></gr-weblink>
-      <gr-copy-clipboard
-        hastooltip
-        .buttonTitle=${'Copy full SHA to clipboard'}
-        hideinput
-        .text=${commit}
-      >
-      </gr-copy-clipboard>
+      ${when(
+        this.showCopyButton,
+        () => html`
+          <gr-copy-clipboard
+            hastooltip
+            .buttonTitle=${'Copy full SHA to clipboard'}
+            hideinput
+            .text=${commit}
+          >
+          </gr-copy-clipboard>
+        `
+      )}
     </div>`;
   }
 
@@ -86,6 +98,6 @@
       this.serverConfig
     );
     if (primaryLink) return {...primaryLink, name};
-    return {name, url: createSearchUrl({query: name})};
+    return {name, url: createSearchUrl({query: commit})};
   }
 }
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
index 7bc4e60..3fed767 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
@@ -79,7 +79,11 @@
     assert.shadowDom.equal(
       weblink,
       /* HTML */ `
-        <a href="/q/sha4567" rel="noopener noreferrer" target="_blank">
+        <a
+          href="/q/sha45678901234567890"
+          rel="noopener noreferrer"
+          target="_blank"
+        >
           <gr-tooltip-content>
             <span> sha4567 </span>
           </gr-tooltip-content>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
index b66b2cf..bb76a3e 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
@@ -18,6 +18,8 @@
   ChangeInfoId,
   TopicName,
   ChangeActionDialog,
+  EmailInfo,
+  GitPersonInfo,
 } from '../../../types/common';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {
@@ -30,6 +32,7 @@
   ChangeStatus,
   ProgressStatus,
 } from '../../../constants/constants';
+import {subscribe} from '../../lit/subscription-controller';
 import {fire, fireNoBubble} from '../../../utils/event-util';
 import {css, html, LitElement, PropertyValues} from 'lit';
 import {sharedStyles} from '../../../styles/shared-styles';
@@ -43,6 +46,7 @@
 import {ParsedChangeInfo} from '../../../types/types';
 import {formStyles} from '../../../styles/form-styles';
 import {branchName} from '../../../utils/patch-set-util';
+import {changeModelToken} from '../../../models/change/change-model';
 
 const SUGGESTIONS_LIMIT = 15;
 const CHANGE_SUBJECT_LIMIT = 50;
@@ -127,13 +131,24 @@
   @state()
   private invalidBranch = false;
 
+  @state()
+  emails: EmailInfo[] = [];
+
   @query('#branchInput')
   branchInput!: GrTypedAutocomplete<BranchName>;
 
+  @state()
+  committerEmail?: string;
+
+  @state()
+  latestCommitter?: GitPersonInfo;
+
   private selectedChangeIds = new Set<ChangeInfoId>();
 
   private readonly restApiService = getAppContext().restApiService;
 
+  private readonly getChangeModel = resolve(this, changeModelToken);
+
   private readonly reporting = getAppContext().reportingService;
 
   private readonly getNavigation = resolve(this, navigationToken);
@@ -142,6 +157,16 @@
     super();
     this.statuses = {};
     this.query = (text: string) => this.getProjectBranchesSuggestions(text);
+    subscribe(
+      this,
+      () => this.getChangeModel().latestCommitter$,
+      x => (this.latestCommitter = x)
+    );
+  }
+
+  override connectedCallback() {
+    super.connectedCallback();
+    this.loadEmails();
   }
 
   override willUpdate(changedProperties: PropertyValues) {
@@ -341,6 +366,17 @@
         @bind-value-changed=${(e: BindValueChangeEvent) =>
           (this.message = e.detail.value ?? '')}
       ></iron-autogrow-textarea>
+      ${when(
+        this.canShowEmailDropdown(),
+        () => html`<div id="cherryPickEmailDropdown">Cherry Pick Committer Email
+            <gr-dropdown-list
+                .items=${this.getEmailDropdownItems()}
+                .value=${this.committerEmail}
+                @value-change=${this.setCommitterEmail}
+            >
+            </gr-dropdown-list>
+            <span></div>`
+      )}
     `;
   }
 
@@ -580,6 +616,7 @@
         topic,
         allow_conflicts: true,
         allow_empty: true,
+        committer_email: this.committerEmail ? this.committerEmail : null,
       };
       const handleError = (response?: Response | null) => {
         this.handleCherryPickFailed(change, response);
@@ -654,4 +691,38 @@
         return branches;
       });
   }
+
+  async loadEmails() {
+    const accountEmails: EmailInfo[] =
+      (await this.restApiService.getAccountEmails()) ?? [];
+    let selectedEmail: string | undefined;
+    accountEmails.forEach(e => {
+      if (e.preferred) {
+        selectedEmail = e.email;
+      }
+    });
+
+    if (accountEmails.some(e => e.email === this.latestCommitter?.email)) {
+      selectedEmail = this.latestCommitter?.email;
+    }
+    this.emails = accountEmails;
+    this.committerEmail = selectedEmail;
+  }
+
+  private canShowEmailDropdown() {
+    return this.emails.length > 1;
+  }
+
+  private getEmailDropdownItems() {
+    return this.emails.map(e => {
+      return {
+        text: e.email,
+        value: e.email,
+      };
+    });
+  }
+
+  private setCommitterEmail(e: CustomEvent<{value: string}>) {
+    this.committerEmail = e.detail.value;
+  }
 }
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
index dc8dba9..83e6f9e 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
@@ -5,7 +5,12 @@
  */
 import '../../../test/common-test-setup';
 import './gr-confirm-cherrypick-dialog';
-import {queryAll, queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {
+  query,
+  queryAll,
+  queryAndAssert,
+  stubRestApi,
+} from '../../../test/test-utils';
 import {GrConfirmCherrypickDialog} from './gr-confirm-cherrypick-dialog';
 import {
   BranchName,
@@ -24,11 +29,53 @@
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {ProgressStatus} from '../../../constants/constants';
 import {fixture, html, assert} from '@open-wc/testing';
+import {GrDropdownList} from '../../shared/gr-dropdown-list/gr-dropdown-list';
 
 const CHERRY_PICK_TYPES = {
   SINGLE_CHANGE: 1,
   TOPIC: 2,
 };
+
+const changes: ChangeInfo[] = [
+  {
+    ...createChange(),
+    id: '1234' as ChangeInfoId,
+    change_id: '12345678901234' as ChangeId,
+    topic: 'T' as TopicName,
+    subject: 'random',
+    project: 'A' as RepoName,
+    _number: 1 as NumericChangeId,
+    revisions: {
+      a: createRevision(),
+    },
+    current_revision: 'a' as CommitId,
+  },
+  {
+    ...createChange(),
+    id: '5678' as ChangeInfoId,
+    change_id: '23456' as ChangeId,
+    topic: 'T' as TopicName,
+    subject: 'a'.repeat(100),
+    project: 'B' as RepoName,
+    _number: 2 as NumericChangeId,
+    revisions: {
+      a: createRevision(),
+    },
+    current_revision: 'a' as CommitId,
+  },
+];
+
+const emails = [
+  {
+    email: 'primary@email.com',
+    preferred: true,
+  },
+  {
+    email: 'secondary@email.com',
+    preferred: false,
+  },
+];
+
 suite('gr-confirm-cherrypick-dialog tests', () => {
   let element: GrConfirmCherrypickDialog;
 
@@ -149,34 +196,6 @@
   });
 
   suite('cherry pick topic', () => {
-    const changes: ChangeInfo[] = [
-      {
-        ...createChange(),
-        id: '1234' as ChangeInfoId,
-        change_id: '12345678901234' as ChangeId,
-        topic: 'T' as TopicName,
-        subject: 'random',
-        project: 'A' as RepoName,
-        _number: 1 as NumericChangeId,
-        revisions: {
-          a: createRevision(),
-        },
-        current_revision: 'a' as CommitId,
-      },
-      {
-        ...createChange(),
-        id: '5678' as ChangeInfoId,
-        change_id: '23456' as ChangeId,
-        topic: 'T' as TopicName,
-        subject: 'a'.repeat(100),
-        project: 'B' as RepoName,
-        _number: 2 as NumericChangeId,
-        revisions: {
-          a: createRevision(),
-        },
-        current_revision: 'a' as CommitId,
-      },
-    ];
     setup(async () => {
       element.updateChanges(changes);
       element.cherryPickType = CHERRY_PICK_TYPES.TOPIC;
@@ -290,4 +309,36 @@
     assert.equal(branches.length, 1);
     assert.equal(branches[0].name, 'test-branch');
   });
+
+  suite('cherry pick single change with committer email', () => {
+    test('hide email dropdown when user has one email', async () => {
+      element.emails = emails.slice(0, 1);
+      await element.updateComplete;
+      assert.notExists(query(element, '#cherryPickEmailDropdown'));
+    });
+
+    test('show email dropdown when user has more than one email', async () => {
+      element.emails = emails;
+      await element.updateComplete;
+      const cherryPickEmailDropdown = queryAndAssert(
+        element,
+        '#cherryPickEmailDropdown'
+      );
+      assert.dom.equal(
+        cherryPickEmailDropdown,
+        `<div id="cherryPickEmailDropdown">Cherry Pick Committer Email
+        <gr-dropdown-list></gr-dropdown-list>
+        <span></span>
+        </div>`
+      );
+      const emailDropdown = queryAndAssert<GrDropdownList>(
+        cherryPickEmailDropdown,
+        'gr-dropdown-list'
+      );
+      assert.deepEqual(
+        emailDropdown.items?.map(e => e.value),
+        emails.map(e => e.email)
+      );
+    });
+  });
 });
diff --git a/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents.ts b/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents.ts
index 7cfebf1..72c2e43 100644
--- a/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents.ts
+++ b/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents.ts
@@ -4,21 +4,34 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {customElement, state} from 'lit/decorators.js';
-import {css, html, LitElement} from 'lit';
+import {css, html, HTMLTemplateResult, LitElement} from 'lit';
 import {resolve} from '../../../models/dependency';
 import {subscribe} from '../../lit/subscription-controller';
 import {changeModelToken} from '../../../models/change/change-model';
-import {EDIT, RevisionInfo} from '../../../api/rest-api';
+import {
+  CommitId,
+  EDIT,
+  NumericChangeId,
+  ParentInfo,
+  PatchSetNumber,
+  RepoName,
+  RevisionInfo,
+} from '../../../api/rest-api';
 import {fontStyles} from '../../../styles/gr-font-styles';
-import {branchName, shorten} from '../../../utils/patch-set-util';
+import {branchName} from '../../../utils/patch-set-util';
 import {when} from 'lit/directives/when.js';
+import {createChangeUrl} from '../../../models/views/change';
 
 @customElement('gr-revision-parents')
 export class GrRevisionParents extends LitElement {
+  @state() repo?: RepoName;
+
   @state() revision?: RevisionInfo;
 
   @state() baseRevision?: RevisionInfo;
 
+  @state() showDetails = false;
+
   private readonly getChangeModel = resolve(this, changeModelToken);
 
   constructor() {
@@ -33,6 +46,11 @@
     );
     subscribe(
       this,
+      () => this.getChangeModel().repo$,
+      x => (this.repo = x)
+    );
+    subscribe(
+      this,
       () => this.getChangeModel().baseRevision$,
       x => (this.baseRevision = x)
     );
@@ -50,7 +68,7 @@
           border-top: 1px solid var(--border-color);
           background-color: var(--yellow-50);
         }
-        .flex {
+        .sections {
           display: flex;
         }
         .section {
@@ -63,53 +81,312 @@
         .title {
           font-weight: var(--font-weight-bold);
         }
+        .messageContainer {
+          display: flex;
+          padding: var(--spacing-m) var(--spacing-l);
+          border-top: 1px solid var(--border-color);
+        }
+        .messageContainer.info {
+          background-color: var(--info-background);
+        }
+        .messageContainer.warning {
+          background-color: var(--warning-background);
+        }
+        .messageContainer gr-icon {
+          margin-right: var(--spacing-m);
+        }
+        .messageContainer.info gr-icon {
+          color: var(--info-foreground);
+        }
+        .messageContainer.warning gr-icon {
+          color: var(--warning-foreground);
+        }
+        .messageContainer .text {
+          max-width: 600px;
+        }
+        .messageContainer .text p {
+          margin: 0;
+        }
+        .messageContainer .text gr-button {
+          margin-left: -4px;
+        }
+        gr-commit-info {
+          display: inline-block;
+        }
       `,
     ];
   }
 
   override render() {
-    // TODO(revision-parents): Figure out what to do about multiple parents.
-    const baseParent = this.baseRevision?.parents_data?.[0];
-    const parent = this.revision?.parents_data?.[0];
-    if (!parent || !baseParent) return;
-    // TODO(revision-parents): Design something nicer for the various cases.
+    return html`${this.renderMessage()}${this.renderDetails()}`;
+  }
+
+  private renderMessage() {
+    if (!this.baseRevision || !this.revision) return;
+    // For merges we are only interested in the target branch parent, which is [0].
+    // And for non-merges there is no more than 1 parent, so [0] is the only choice.
+    const parentLeft = this.baseRevision?.parents_data?.[0];
+    const parentRight = this.revision?.parents_data?.[0];
+    // Note that is you diff a patchset against its base, then baseRevision will be
+    // `undefined`. Thus after this line we know that we are dealing with diffs
+    // of the type "patchset x vs patchset y".
+    if (!parentLeft || !parentRight) return;
+
+    const psLeft = this.baseRevision?._number;
+    const psRight = this.revision?._number;
+    const parentCommitLeft = parentLeft.commit_id;
+    const parentCommitRight = parentRight.commit_id;
+    const branchLeft = branchName(parentLeft.branch_name);
+    const branchRight = branchName(parentRight.branch_name);
+    const isMergedLeft = parentLeft.is_merged_in_target_branch;
+    const isMergedRight = parentRight.is_merged_in_target_branch;
+    const changeNumLeft = parentLeft.change_number;
+    const changeNumRight = parentRight.change_number;
+    const changePsLeft = parentLeft.patch_set_number;
+    const changePsRight = parentRight.patch_set_number;
+
+    if (parentCommitLeft === parentCommitRight) return;
+
+    // Subsequently: different commit
+
+    if (branchLeft !== branchRight) {
+      return html`
+        ${this.renderWarning(
+          'warning',
+          html`
+            Patchset ${psLeft} and ${psRight} are targeting different branches.
+          `
+        )}
+      `;
+    }
+
+    // Subsequently: different commit, same target branch
+
+    // Such a situation is really rare and weird. You have to do something like committing to one
+    // branch and then uploading to another. This warning should actually also be shown, if
+    // you are not comparing PS X and PS Y, because it is generally a weird patchset state.
+    const isWeirdLeft = !isMergedLeft && !changeNumLeft;
+    const isWeirdRight = !isMergedRight && !changeNumRight;
+    if (isWeirdLeft || isWeirdRight) {
+      const weirdPs =
+        isWeirdLeft && isWeirdRight
+          ? `${psLeft} and ${psRight} are`
+          : isWeirdLeft
+          ? `${psLeft} is`
+          : `${psRight} is`;
+      return html`
+        ${this.renderWarning(
+          'warning',
+          html`
+            Patchset ${weirdPs} based on a commit that neither exists in its
+            target branch, nor is it a commit of another active change.
+          `
+        )}
+      `;
+    }
+
+    if (
+      changeNumLeft &&
+      changeNumRight &&
+      changeNumLeft === changeNumRight &&
+      // This check is probably redundant, because "same change and ps" should mean "same commit".
+      psLeft !== psRight
+    ) {
+      return html`
+        ${this.renderWarning(
+          'info',
+          html`
+            The change was rebased from patchset
+            ${this.renderPatchsetLink(changeNumLeft, changePsLeft)} onto
+            patchset ${this.renderPatchsetLink(changeNumLeft, changePsRight)} of
+            change ${this.renderChangeLink(changeNumLeft)}
+            ${when(isMergedRight, () => html` (MERGED)`)}.
+          `
+        )}
+      `;
+    }
+
+    // No additional info? Then "different commit" and "same branch" means "standard rebase".
+    if (isMergedLeft && isMergedRight) {
+      return html`
+        ${this.renderWarning(
+          'info',
+          html`
+            The change was rebased from
+            ${this.renderCommitLink(parentCommitLeft, false)} onto
+            ${this.renderCommitLink(parentCommitRight, false)}.
+          `
+        )}
+      `;
+    }
+
+    // By now we know that we have different commit, same target branch, no weird parent,
+    // and not a standard rebase. So let's spell out what the left and right side are based on.
+    return this.renderWarning(
+      'warning',
+      html`${this.renderInfo(this.baseRevision)}<br />${this.renderInfo(
+          this.revision
+        )}`
+    );
+  }
+
+  private renderInfo(rev: RevisionInfo) {
+    const parent = rev.parents_data?.[0];
+    if (!parent) return;
+    const ps = rev._number;
+    const isMerged = parent.is_merged_in_target_branch;
+    const changeNum = parent.change_number;
+
+    if (changeNum && !isMerged) {
+      return html`
+        Patchset ${ps} is based on patchset
+        ${this.renderPatchsetLink(changeNum, parent.patch_set_number)} of change
+        ${this.renderChangeLink(changeNum)}.
+      `;
+    } else {
+      return html`
+        Patchset ${ps} is based on commit
+        ${this.renderCommitLink(parent.commit_id, false)} in the target branch
+        (${branchName(parent.branch_name)}).
+      `;
+    }
+  }
+
+  private renderWarning(icon: string, message: HTMLTemplateResult) {
+    const isWarning = icon === 'warning';
+    return html`
+      <div class="messageContainer ${icon}">
+        <div class="icon">
+          <gr-icon icon=${icon}></gr-icon>
+        </div>
+        <div class="text">
+          <p>
+            ${message}${when(
+              isWarning,
+              () => html`
+                <br />
+                The diff below may not be meaningful and may even be hiding
+                relevant changes.
+              `
+            )}
+          </p>
+          ${when(
+            isWarning,
+            () => html`
+              <p>
+                <gr-button
+                  link
+                  @click=${() => (this.showDetails = !this.showDetails)}
+                  >${this.showDetails ? 'Hide' : 'Show'} details</gr-button
+                >
+              </p>
+            `
+          )}
+        </div>
+      </div>
+    `;
+  }
+
+  private renderDetails() {
+    if (!this.showDetails) return;
+    if (!this.baseRevision || !this.revision) return;
+    const parentLeft = this.baseRevision.parents_data?.[0];
+    const parentRight = this.revision.parents_data?.[0];
+    if (!parentRight || !parentLeft) return;
+
     return html`
       <div class="container">
-        <div class="flex">
-          <div class="section">
-            <h4 class="heading-4">Patchset ${this.baseRevision?._number}</h4>
-            <div>Branch: ${branchName(parent.branch_name)}</div>
-            <div>Commit: ${shorten(parent.commit_id)}</div>
-            <div>Is Merged: ${parent.is_merged_in_target_branch}</div>
-            ${when(
-              !!parent.change_number,
-              () => html` <div>
-                  Change ID: ${parent.change_id?.substring(0, 10)}
-                </div>
-                <div>Change Number: ${parent.change_number}</div>
-                <div>Patchset Number: ${parent.patch_set_number}</div>
-                <div>Change Status: ${parent.change_status}</div>`
-            )}
-          </div>
-          <div class="section">
-            <h4 class="heading-4">Patchset ${this.revision?._number}</h4>
-            <div>Branch: ${branchName(baseParent.branch_name)}</div>
-            <div>Commit: ${shorten(baseParent.commit_id)}</div>
-            <div>Is Merged: ${baseParent.is_merged_in_target_branch}</div>
-            ${when(
-              !!baseParent.change_number,
-              () => html`<div>
-                  Change ID: ${baseParent.change_id?.substring(0, 10)}
-                </div>
-                <div>Change Number: ${baseParent.change_number}</div>
-                <div>Patchset Number: ${baseParent.patch_set_number}</div>
-                <div>Change Status: ${baseParent.change_status}</div>`
-            )}
-          </div>
+        <div class="sections">
+          ${this.renderSection(
+            this.baseRevision,
+            parentLeft,
+            parentLeft.change_number === parentRight.change_number
+          )}
+          ${this.renderSection(
+            this.revision,
+            parentRight,
+            parentLeft.change_number === parentRight.change_number
+          )}
         </div>
       </div>
     `;
   }
+
+  private renderCommitLink(commit?: CommitId, showCopyButton = true) {
+    if (!commit) return;
+    return html`<gr-commit-info
+      .commitInfo=${{commit}}
+      .showCopyButton=${showCopyButton}
+    ></gr-commit-info>`;
+  }
+
+  private renderChangeLink(changeNum: NumericChangeId) {
+    return html`
+      <a href=${createChangeUrl({changeNum, repo: this.repo!})}>${changeNum}</a>
+    `;
+  }
+
+  private renderPatchsetLink(
+    changeNum: NumericChangeId,
+    patchNum?: PatchSetNumber
+  ) {
+    if (!patchNum) return;
+    return html`
+      <a
+        href=${createChangeUrl({
+          changeNum,
+          repo: this.repo!,
+          patchNum,
+        })}
+        >${patchNum}</a
+      >
+    `;
+  }
+
+  private renderSection(
+    revision: RevisionInfo,
+    parent: ParentInfo,
+    sameChange: boolean
+  ) {
+    const ps = revision._number;
+    const commit = parent.commit_id;
+    const branch = branchName(parent.branch_name);
+    const isMerged = parent.is_merged_in_target_branch;
+    const changeNum = parent.change_number as NumericChangeId;
+    const changePs = parent.patch_set_number;
+
+    createChangeUrl({changeNum, repo: this.repo!});
+
+    return html`
+      <div class="section">
+        <h4 class="heading-4">Patchset ${ps}</h4>
+        <div>Target branch: ${branch}</div>
+        <div>Base commit: ${this.renderCommitLink(commit)}</div>
+        ${when(
+          !changeNum && !isMerged,
+          () => html`
+            <div>
+              <gr-icon icon="warning"></gr-icon>
+              <span
+                >Warning: The base commit is not known (aka reachable) in the
+                target branch.</span
+              >
+            </div>
+          `
+        )}
+        ${when(
+          changeNum && (sameChange || !isMerged),
+          () => html`
+            <div>
+              Base change: ${this.renderChangeLink(changeNum)}, patchset
+              ${this.renderPatchsetLink(changeNum, changePs)}
+              ${when(isMerged, () => html`(MERGED)`)}
+            </div>
+          `
+        )}
+      </div>
+    `;
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents_test.ts b/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents_test.ts
index a1b9fdd..7b97550 100644
--- a/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents_test.ts
@@ -13,12 +13,68 @@
   ChangeStatus,
   CommitId,
   NumericChangeId,
+  ParentInfo,
   PatchSetNumber,
 } from '../../../api/rest-api';
+import {queryAll} from '../../../utils/common-util';
+
+const PARENT_DEFAULT: ParentInfo = {
+  branch_name: 'refs/heads/master',
+  commit_id: '78e52ce873b1c08396422f51ad6aacf77ed95541' as CommitId,
+  is_merged_in_target_branch: true,
+};
+
+const PARENT_REBASED: ParentInfo = {
+  ...PARENT_DEFAULT,
+  commit_id: '00002ce873b1c08396422f51ad6aacf77ed95541' as CommitId,
+};
+
+const PARENT_OTHER_BRANCH: ParentInfo = {
+  ...PARENT_DEFAULT,
+  branch_name: 'refs/heads/otherbranch',
+  commit_id: '11112ce873b1c08396422f51ad6aacf77ed95541' as CommitId,
+};
+
+const PARENT_WEIRD: ParentInfo = {
+  ...PARENT_DEFAULT,
+  commit_id: '22222ce873b1c08396422f51ad6aacf77ed95541' as CommitId,
+  is_merged_in_target_branch: false,
+};
+
+const PARENT_CHANGE_123_1: ParentInfo = {
+  ...PARENT_DEFAULT,
+  commit_id: '12312ce873b1c08396422f51ad6aacf77ed95541' as CommitId,
+  is_merged_in_target_branch: false,
+  change_id: 'Idc69e6d7bba0ce0a9a0bdcd22adb506c0b76e628' as ChangeId,
+  change_number: 123 as NumericChangeId,
+  patch_set_number: 1 as PatchSetNumber,
+  change_status: ChangeStatus.NEW,
+};
+
+const PARENT_CHANGE_123_2: ParentInfo = {
+  ...PARENT_CHANGE_123_1,
+  commit_id: '12322ce873b1c08396422f51ad6aacf77ed95541' as CommitId,
+  patch_set_number: 2 as PatchSetNumber,
+};
 
 suite('gr-revision-parents tests', () => {
   let element: GrRevisionParents;
 
+  const setParents = async (
+    parentLeft: ParentInfo,
+    parentRight: ParentInfo
+  ) => {
+    element.baseRevision = {
+      ...createRevision(1),
+      parents_data: [parentLeft],
+    };
+    element.revision = {
+      ...createRevision(2),
+      parents_data: [parentRight],
+    };
+    await element.updateComplete;
+  };
+
   setup(async () => {
     element = await fixture(html`<gr-revision-parents></gr-revision-parents>`);
     await element.updateComplete;
@@ -28,65 +84,155 @@
     assert.shadowDom.equal(element, '');
   });
 
-  test('render', async () => {
-    element.baseRevision = {
-      ...createRevision(1),
-      parents_data: [
-        {
-          branch_name: 'refs/heads/master',
-          commit_id: '78e52ce873b1c08396422f51ad6aacf77ed95541' as CommitId,
-          is_merged_in_target_branch: false,
-          change_id: 'Idc69e6d7bba0ce0a9a0bdcd22adb506c0b76e628' as ChangeId,
-          change_number: 1500 as NumericChangeId,
-          patch_set_number: 1 as PatchSetNumber,
-          change_status: ChangeStatus.NEW,
-        },
-      ],
-    };
-    element.revision = {
-      ...createRevision(2),
-      parents_data: [
-        {
-          branch_name: 'refs/heads/master',
-          commit_id: '78e52ce873b1c08396422f51ad6aacf77ed95541' as CommitId,
-          is_merged_in_target_branch: false,
-          change_id: 'Idc69e6d7bba0ce0a9a0bdcd22adb506c0b76e628' as ChangeId,
-          change_number: 1500 as NumericChangeId,
-          patch_set_number: 2 as PatchSetNumber,
-          change_status: ChangeStatus.NEW,
-        },
-      ],
-    };
-    await element.updateComplete;
-
-    assert.shadowDom.equal(
-      element,
+  test('render details: PARENT_DEFAULT', async () => {
+    element.showDetails = true;
+    await setParents(PARENT_DEFAULT, PARENT_DEFAULT);
+    assert.dom.equal(
+      queryAll(element, '.section')[0],
       /* HTML */ `
-        <div class="container">
-          <div class="flex">
-            <div class="section">
-              <h4 class="heading-4">Patchset 1</h4>
-              <div>Branch: master</div>
-              <div>Commit: 78e52ce</div>
-              <div>Is Merged: false</div>
-              <div>Change ID: Idc69e6d7b</div>
-              <div>Change Number: 1500</div>
-              <div>Patchset Number: 2</div>
-              <div>Change Status: NEW</div>
-            </div>
-            <div class="section">
-              <h4 class="heading-4">Patchset 2</h4>
-              <div>Branch: master</div>
-              <div>Commit: 78e52ce</div>
-              <div>Is Merged: false</div>
-              <div>Change ID: Idc69e6d7b</div>
-              <div>Change Number: 1500</div>
-              <div>Patchset Number: 1</div>
-              <div>Change Status: NEW</div>
-            </div>
+        <div class="section">
+          <h4 class="heading-4">Patchset 1</h4>
+          <div>Target branch: master</div>
+          <div>
+            Base commit:
+            <gr-commit-info> </gr-commit-info>
           </div>
         </div>
       `
     );
   });
+
+  test('render details: PARENT_WEIRD', async () => {
+    element.showDetails = true;
+    await setParents(PARENT_WEIRD, PARENT_WEIRD);
+    assert.dom.equal(
+      queryAll(element, '.section')[0],
+      `
+        <div class="section">
+          <h4 class="heading-4">Patchset 1</h4>
+          <div>Target branch: master</div>
+          <div>
+            Base commit:
+            <gr-commit-info> </gr-commit-info>
+          </div>
+          <div>
+            <gr-icon icon="warning"> </gr-icon>
+            <span>
+              Warning: The base commit is not known (aka reachable) in the
+                target branch.
+            </span>
+          </div>
+        </div>
+      `
+    );
+  });
+
+  test('render details: PARENT_CHANGE_123_1', async () => {
+    element.showDetails = true;
+    await setParents(PARENT_CHANGE_123_1, PARENT_CHANGE_123_2);
+    assert.dom.equal(
+      queryAll(element, '.section')[0],
+      /* HTML */ `
+        <div class="section">
+          <h4 class="heading-4">Patchset 1</h4>
+          <div>Target branch: master</div>
+          <div>
+            Base commit:
+            <gr-commit-info> </gr-commit-info>
+          </div>
+          <div>
+            Base change:
+            <a href="/c/123"> 123 </a>
+            , patchset
+            <a href="/c/123/1"> 1 </a>
+          </div>
+        </div>
+      `
+    );
+  });
+
+  test('render message PARENT_DEFAULT vs PARENT_DEFAULT', async () => {
+    await setParents(PARENT_DEFAULT, PARENT_DEFAULT);
+    assert.shadowDom.equal(element, '');
+  });
+
+  test('render message PARENT_DEFAULT vs PARENT_OTHER_BRANCH', async () => {
+    await setParents(PARENT_DEFAULT, PARENT_OTHER_BRANCH);
+    assert.shadowDom.equal(
+      element,
+      `<div class="messageContainer warning">
+       <div class="icon"><gr-icon icon="warning"></gr-icon></div>
+       <div class="text"><p>
+       Patchset 1 and 2 are targeting different branches.<br/>
+       The diff below may not be meaningful and may even be hiding
+                relevant changes.
+       </p><p><gr-button link="">Show details</gr-button></p></div></div>`
+    );
+  });
+
+  test('render message PARENT_DEFAULT vs PARENT_WEIRD', async () => {
+    await setParents(PARENT_DEFAULT, PARENT_WEIRD);
+    assert.shadowDom.equal(
+      element,
+      `<div class="messageContainer warning">
+       <div class="icon"><gr-icon icon="warning"></gr-icon></div>
+       <div class="text"><p>
+       Patchset 2 is based on a commit that neither exists in its
+            target branch, nor is it a commit of another active change.<br/>
+       The diff below may not be meaningful and may even be hiding
+                relevant changes.
+       </p><p><gr-button link="">Show details</gr-button></p></div></div>`
+    );
+  });
+
+  test('render message PARENT_DEFAULT vs PARENT_REBASED', async () => {
+    await setParents(PARENT_DEFAULT, PARENT_REBASED);
+    assert.shadowDom.equal(
+      element,
+      `<div class="messageContainer info">
+       <div class="icon"><gr-icon icon="info"></gr-icon></div>
+       <div class="text"><p>
+       The change was rebased from <gr-commit-info></gr-commit-info>
+       onto <gr-commit-info></gr-commit-info>.
+       </p></div></div>`
+    );
+  });
+
+  test('render message PARENT_CHANGE_123_1 vs PARENT_CHANGE_123_2', async () => {
+    await setParents(PARENT_CHANGE_123_1, PARENT_CHANGE_123_2);
+    assert.shadowDom.equal(
+      element,
+      `<div class="messageContainer info">
+       <div class="icon"><gr-icon icon="info"></gr-icon></div>
+       <div class="text"><p>
+       The change was rebased from patchset
+       <a href="/c/123/1">1</a> onto
+            patchset
+       <a href="/c/123/2">2</a> of
+            change
+       <a href="/c/123">123</a>.
+       </p></div></div>`
+    );
+  });
+
+  test('render message PARENT_DEFAULT vs PARENT_CHANGE_123_1', async () => {
+    await setParents(PARENT_DEFAULT, PARENT_CHANGE_123_1);
+    assert.shadowDom.equal(
+      element,
+      `<div class="messageContainer warning">
+       <div class="icon"><gr-icon icon="warning"></gr-icon></div>
+       <div class="text"><p>
+       Patchset 1 is based on commit
+       <gr-commit-info></gr-commit-info>
+       in the target branch
+        (master).<br>
+       Patchset 2 is based on patchset
+       <a href="/c/123/1">1</a>
+       of change
+       <a href="/c/123">123</a>.<br>
+       The diff below may not be meaningful and may even be hiding
+                relevant changes.
+       </p><p><gr-button link="">Show details</gr-button></p></div></div>`
+    );
+  });
 });
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 68a0c5a..ac35264 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -79,7 +79,6 @@
   SettingsViewState,
 } from '../../../models/views/settings';
 import {define} from '../../../models/dependency';
-import {Finalizable} from '../../../services/registry';
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
 import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
 import {
@@ -105,6 +104,7 @@
   noAwait,
   timeoutPromise,
 } from '../../../utils/async-util';
+import {Finalizable} from '../../../types/types';
 
 // TODO: Move all patterns to view model files and use the `Route` interface,
 // which will enforce using `RegExp` in its `urlPattern` property.
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index 6a05d86..6bf1adc 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -18,6 +18,7 @@
   convertToPatchSetNum,
   getParentInfoString,
   shorten,
+  getParentCommit,
 } from '../../../utils/patch-set-util';
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
 import {
@@ -276,7 +277,8 @@
       KnownExperimentId.REVISION_PARENTS_DATA
     );
     dropdownContent.push({
-      text: isMerge ? 'Auto Merge' : 'Base',
+      triggerText: isMerge ? 'Auto Merge' : 'Base',
+      text: isMerge ? 'Auto Merge' : `Base | ${getParentCommit(rev, 0)}`,
       bottomText:
         showParentsData && !isMerge ? getParentInfoString(rev, 0) : undefined,
       value: PARENT,
@@ -286,7 +288,7 @@
       dropdownContent.push({
         disabled: idx >= parentCount,
         triggerText: `Parent ${idx + 1}`,
-        text: `Parent ${idx + 1}`,
+        text: `Parent ${idx + 1} | ${getParentCommit(rev, idx)}`,
         bottomText: showParentsData ? getParentInfoString(rev, idx) : undefined,
         mobileText: `Parent ${idx + 1}`,
         value: -(idx + 1),
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
index 0f2dd1e..e7ed97b 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -180,7 +180,8 @@
         commentThreads: [],
       } as DropdownItem,
       {
-        text: 'Base',
+        text: 'Base | ',
+        triggerText: 'Base',
         value: PARENT,
         bottomText: undefined,
       } as DropdownItem,
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.ts b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
index ec46d8d..45dc3b6 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.ts
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
@@ -22,7 +22,7 @@
   initClickReporter,
   initInteractionReporter,
 } from '../services/gr-reporting/gr-reporting_impl';
-import {Finalizable} from '../services/registry';
+import {Finalizable} from '../types/types';
 
 export function initGlobalVariables(
   appContext: AppContext & Finalizable,
diff --git a/polygerrit-ui/app/elements/gr-app.ts b/polygerrit-ui/app/elements/gr-app.ts
index 40869a9..2ea6983 100644
--- a/polygerrit-ui/app/elements/gr-app.ts
+++ b/polygerrit-ui/app/elements/gr-app.ts
@@ -24,7 +24,6 @@
 
 import {initGerrit, initGlobalVariables} from './gr-app-global-var-init';
 import './gr-app-element';
-import {Finalizable} from '../services/registry';
 import {
   DependencyError,
   DependencyToken,
@@ -46,6 +45,7 @@
 } from '../services/service-worker-installer';
 import {pluginLoaderToken} from './shared/gr-js-api-interface/gr-plugin-loader';
 import {getAppContext} from '../services/app-context';
+import {Finalizable} from '../types/types';
 
 initGlobalVariables(createAppContext(), true);
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index cd73727..6b9c684 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -20,9 +20,8 @@
   ShowRevisionActionsDetail,
 } from './gr-js-api-types';
 import {EventType, TargetElement} from '../../../api/plugin';
-import {ParsedChangeInfo} from '../../../types/types';
+import {Finalizable, ParsedChangeInfo} from '../../../types/types';
 import {MenuLink} from '../../../api/admin';
-import {Finalizable} from '../../../services/registry';
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
 import {Provider} from '../../../models/dependency';
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
index afa16b9..6c180d7 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
@@ -11,9 +11,8 @@
   ReviewInput,
   RevisionInfo,
 } from '../../../types/common';
-import {Finalizable} from '../../../services/registry';
 import {EventType, TargetElement} from '../../../api/plugin';
-import {ParsedChangeInfo} from '../../../types/types';
+import {Finalizable, ParsedChangeInfo} from '../../../types/types';
 import {MenuLink} from '../../../api/admin';
 import {FileRange, PatchRange} from '../../../api/diff';
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
index e5ead0b..b78af2a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
@@ -17,7 +17,6 @@
 import {fireAlert} from '../../../utils/event-util';
 import {JsApiService} from './gr-js-api-types';
 import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
-import {Finalizable} from '../../../services/registry';
 import {PluginsModel} from '../../../models/plugins/plugins-model';
 import {Gerrit} from '../../../api/gerrit';
 import {fontStyles} from '../../../styles/gr-font-styles';
@@ -30,6 +29,7 @@
 import {GrJsApiInterface} from './gr-js-api-interface-element';
 import {define} from '../../../models/dependency';
 import {modalStyles} from '../../../styles/gr-modal-styles';
+import {Finalizable} from '../../../types/types';
 
 enum PluginState {
   /** State that indicates the plugin is pending to be loaded. */
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
index 36ebb9f..bad23cf 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
@@ -3,11 +3,12 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {create, Registry, Finalizable} from '../services/registry';
+import {create, Registry} from '../services/registry';
 import {AppContext} from '../services/app-context';
 import {AuthService} from '../services/gr-auth/gr-auth';
 import {FlagsService} from '../services/flags/flags';
 import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock';
+import {Finalizable} from '../types/types';
 
 class MockFlagsService implements FlagsService {
   isEnabled() {
diff --git a/polygerrit-ui/app/models/base/model.ts b/polygerrit-ui/app/models/base/model.ts
index 4ddef48..4574b31f 100644
--- a/polygerrit-ui/app/models/base/model.ts
+++ b/polygerrit-ui/app/models/base/model.ts
@@ -4,8 +4,8 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {BehaviorSubject, Observable, Subscription} from 'rxjs';
-import {Finalizable} from '../../services/registry';
 import {deepEqual} from '../../utils/deep-util';
+import {Finalizable} from '../../types/types';
 
 /**
  * A Model stores a value <T> and controls changes to that value via `subject$`
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index f1d28db..2414107 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -248,6 +248,11 @@
     change => change?.revisions[change.current_revision]?.uploader
   );
 
+  public readonly latestCommitter$ = select(
+    this.change$,
+    change => change?.revisions[change.current_revision]?.commit?.committer
+  );
+
   /**
    * Emits the current patchset number. If the route does not define the current
    * patchset num, then this selector waits for the change to be defined and
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 8589ae3..87972de 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {AppContext} from './app-context';
-import {create, Finalizable, Registry} from './registry';
+import {create, Registry} from './registry';
 import {DependencyToken} from '../models/dependency';
 import {FlagsServiceImplementation} from './flags/flags_impl';
 import {GrReporting} from './gr-reporting/gr-reporting_impl';
@@ -72,6 +72,7 @@
   RelatedChangesModel,
   relatedChangesModelToken,
 } from '../models/change/related-changes-model';
+import {Finalizable} from '../types/types';
 
 /**
  * The AppContext lazy initializator for all services
diff --git a/polygerrit-ui/app/services/app-context-init_test.ts b/polygerrit-ui/app/services/app-context-init_test.ts
index 7834e53..3572189 100644
--- a/polygerrit-ui/app/services/app-context-init_test.ts
+++ b/polygerrit-ui/app/services/app-context-init_test.ts
@@ -5,9 +5,9 @@
  */
 import '../test/common-test-setup';
 import {AppContext} from './app-context';
-import {Finalizable} from './registry';
 import {createTestAppContext} from '../test/test-app-context-init';
 import {assert} from '@open-wc/testing';
+import {Finalizable} from '../types/types';
 
 suite('app context initializer tests', () => {
   let appContext: AppContext & Finalizable;
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index aa2c032..eeddc8e 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -3,11 +3,11 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {Finalizable} from './registry';
 import {FlagsService} from './flags/flags';
 import {ReportingService} from './gr-reporting/gr-reporting';
 import {AuthService} from './gr-auth/gr-auth';
 import {RestApiService} from './gr-rest-api/gr-rest-api';
+import {Finalizable} from '../types/types';
 
 export interface AppContext {
   flagsService: FlagsService;
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 1ae23cf..b7d36f4 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -3,7 +3,8 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {Finalizable} from '../registry';
+
+import {Finalizable} from '../../types/types';
 
 export interface FlagsService extends Finalizable {
   isEnabled(experimentId: string): boolean;
diff --git a/polygerrit-ui/app/services/flags/flags_impl.ts b/polygerrit-ui/app/services/flags/flags_impl.ts
index 4ef55a2..ca9aa60 100644
--- a/polygerrit-ui/app/services/flags/flags_impl.ts
+++ b/polygerrit-ui/app/services/flags/flags_impl.ts
@@ -3,8 +3,8 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import {Finalizable} from '../../types/types';
 import {FlagsService} from './flags';
-import {Finalizable} from '../registry';
 
 declare global {
   interface Window {
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth.ts b/polygerrit-ui/app/services/gr-auth/gr-auth.ts
index 945d6f9..a6b4fdd 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth.ts
@@ -4,8 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {define} from '../../models/dependency';
-import {AuthRequestInit} from '../../types/types';
-import {Finalizable} from '../registry';
+import {AuthRequestInit, Finalizable} from '../../types/types';
 export enum AuthType {
   XSRF_TOKEN = 'xsrf_token',
   ACCESS_TOKEN = 'access_token',
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
index 2312fc9..fb53d5e 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
@@ -3,10 +3,9 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {AuthRequestInit} from '../../types/types';
+import {AuthRequestInit, Finalizable} from '../../types/types';
 import {fire} from '../../utils/event-util';
 import {getBaseUrl} from '../../utils/url-util';
-import {Finalizable} from '../registry';
 import {
   AuthService,
   AuthStatus,
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
index 9b08a6e..6df2c67 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
@@ -3,7 +3,6 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {Finalizable} from '../registry';
 import {NumericChangeId} from '../../types/common';
 import {EventDetails, ReportingOptions} from '../../api/reporting';
 import {PluginApi} from '../../api/plugin';
@@ -13,6 +12,7 @@
   LifeCycle,
   Timing,
 } from '../../constants/reporting';
+import {Finalizable} from '../../types/types';
 
 export type EventValue = string | number | {error?: Error};
 
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index 61534a4..84b9481 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -9,7 +9,6 @@
 import {NumericChangeId} from '../../types/common';
 import {Deduping, EventDetails, ReportingOptions} from '../../api/reporting';
 import {PluginApi} from '../../api/plugin';
-import {Finalizable} from '../registry';
 import {
   Execution,
   Interaction,
@@ -18,6 +17,7 @@
 } from '../../constants/reporting';
 import {onCLS, onFID, onLCP, Metric, onINP} from 'web-vitals';
 import {getEventPath, isElementTarget} from '../../utils/dom-util';
+import {Finalizable} from '../../types/types';
 
 // Latency reporting constants.
 
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
index fd1b88c..fb1f0c3 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
@@ -7,7 +7,7 @@
 import {EventDetails} from '../../api/reporting';
 import {PluginApi} from '../../api/plugin';
 import {Execution, Interaction} from '../../constants/reporting';
-import {Finalizable} from '../registry';
+import {Finalizable} from '../../types/types';
 
 export class MockTimer implements Timer {
   end(): this {
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index 5339e38..a942cd4 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -20,7 +20,6 @@
 import {GrReviewerUpdatesParser} from '../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
 import {parseDate} from '../../utils/date-util';
 import {getBaseUrl} from '../../utils/url-util';
-import {Finalizable} from '../registry';
 import {getParentIndex, isMergeParent} from '../../utils/patch-set-util';
 import {listChangesOptionsToHex} from '../../utils/change-util';
 import {assertNever, hasOwnProperty} from '../../utils/common-util';
@@ -139,7 +138,11 @@
   ReviewerState,
 } from '../../constants/constants';
 import {firePageError, fireServerError} from '../../utils/event-util';
-import {AuthRequestInit, ParsedChangeInfo} from '../../types/types';
+import {
+  AuthRequestInit,
+  Finalizable,
+  ParsedChangeInfo,
+} from '../../types/types';
 import {ErrorCallback} from '../../api/rest';
 import {addDraftProp} from '../../utils/comment-util';
 import {BaseScheduler, Scheduler} from '../scheduler/scheduler';
@@ -790,11 +793,14 @@
     }) as Promise<AccountDetailInfo | undefined>;
   }
 
-  getAccountEmails() {
-    return this._fetchSharedCacheURL({
-      url: '/accounts/self/emails',
-      reportUrlAsIs: true,
-    }) as Promise<EmailInfo[] | undefined>;
+  async getAccountEmails() {
+    const isloggedIn = await this.getLoggedIn();
+    if (isloggedIn) {
+      return this._fetchSharedCacheURL({
+        url: '/accounts/self/emails',
+        reportUrlAsIs: true,
+      }) as Promise<EmailInfo[] | undefined>;
+    } else return;
   }
 
   addAccountEmail(email: string): Promise<Response> {
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index 3916094..b081ff8 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -4,7 +4,6 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {HttpMethod} from '../../constants/constants';
-import {Finalizable} from '../registry';
 import {
   AccountCapabilityInfo,
   AccountDetailInfo,
@@ -98,7 +97,7 @@
   DiffPreferencesInfo,
   IgnoreWhitespaceType,
 } from '../../types/diff';
-import {ParsedChangeInfo} from '../../types/types';
+import {Finalizable, ParsedChangeInfo} from '../../types/types';
 import {ErrorCallback} from '../../api/rest';
 
 export type CancelConditionCallback = () => boolean;
diff --git a/polygerrit-ui/app/services/highlight/highlight-service.ts b/polygerrit-ui/app/services/highlight/highlight-service.ts
index d10d875..d59431b 100644
--- a/polygerrit-ui/app/services/highlight/highlight-service.ts
+++ b/polygerrit-ui/app/services/highlight/highlight-service.ts
@@ -11,10 +11,10 @@
   SyntaxWorkerMessageType,
   SyntaxLayerLine,
 } from '../../types/syntax-worker-api';
+import {Finalizable} from '../../types/types';
 import {prependOrigin} from '../../utils/url-util';
 import {createWorker} from '../../utils/worker-util';
 import {ReportingService} from '../gr-reporting/gr-reporting';
-import {Finalizable} from '../registry';
 
 const hljsLibUrl = `${
   window.STATIC_RESOURCE_PATH ?? ''
diff --git a/polygerrit-ui/app/services/registry.ts b/polygerrit-ui/app/services/registry.ts
index 48a5241..a88aec3 100644
--- a/polygerrit-ui/app/services/registry.ts
+++ b/polygerrit-ui/app/services/registry.ts
@@ -4,11 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-// A finalizable object has a single method `finalize` that is called when
-// the object is no longer needed and should clean itself up.
-export interface Finalizable {
-  finalize(): void;
-}
+import {Finalizable} from '../types/types';
 
 // A factory can take a partially created TContext and generate a property
 // for a given key on that TContext.
diff --git a/polygerrit-ui/app/services/registry_test.ts b/polygerrit-ui/app/services/registry_test.ts
index 639cd64..15d37d9 100644
--- a/polygerrit-ui/app/services/registry_test.ts
+++ b/polygerrit-ui/app/services/registry_test.ts
@@ -3,9 +3,10 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {create, Finalizable, Registry} from './registry';
+import {create, Registry} from './registry';
 import '../test/common-test-setup';
 import {assert} from '@open-wc/testing';
+import {Finalizable} from '../types/types';
 
 class Foo implements Finalizable {
   constructor(private readonly final: string[]) {}
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
index 756c209..db9d8fe 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -22,10 +22,10 @@
   ShortcutOptions,
 } from '../../utils/dom-util';
 import {ReportingService} from '../gr-reporting/gr-reporting';
-import {Finalizable} from '../registry';
 import {UserModel} from '../../models/user/user-model';
 import {define} from '../../models/dependency';
 import {isCharacterLetter, isUpperCase} from '../../utils/string-util';
+import {Finalizable} from '../../types/types';
 
 export {Shortcut, ShortcutSection};
 
diff --git a/polygerrit-ui/app/services/storage/gr-storage.ts b/polygerrit-ui/app/services/storage/gr-storage.ts
index d7eb09a..0b34614 100644
--- a/polygerrit-ui/app/services/storage/gr-storage.ts
+++ b/polygerrit-ui/app/services/storage/gr-storage.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {NumericChangeId} from '../../types/common';
-import {Finalizable} from '../registry';
+import {Finalizable} from '../../types/types';
 
 export interface StorageObject {
   message?: string;
diff --git a/polygerrit-ui/app/services/storage/gr-storage_impl.ts b/polygerrit-ui/app/services/storage/gr-storage_impl.ts
index 7a47e0e..9ddba01 100644
--- a/polygerrit-ui/app/services/storage/gr-storage_impl.ts
+++ b/polygerrit-ui/app/services/storage/gr-storage_impl.ts
@@ -4,9 +4,9 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {StorageObject, StorageService} from './gr-storage';
-import {Finalizable} from '../registry';
 import {NumericChangeId} from '../../types/common';
 import {define} from '../../models/dependency';
+import {Finalizable} from '../../types/types';
 
 export const DURATION_DAY = 24 * 60 * 60 * 1000;
 
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index ed472cc..82c1bb9 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -7,7 +7,6 @@
 // https://github.com/Polymer/polymer-resin/issues/9 is resolved.
 import '../scripts/bundled-polymer';
 import {getAppContext} from '../services/app-context';
-import {Finalizable} from '../services/registry';
 import {
   createTestAppContext,
   createTestDependencies,
@@ -40,6 +39,7 @@
 import '../styles/themes/app-theme';
 import {Creator} from '../services/app-context-init';
 import {pluginLoaderToken} from '../elements/shared/gr-js-api-interface/gr-plugin-loader';
+import {Finalizable} from '../types/types';
 
 declare global {
   interface Window {
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index da6a92d..6b9f507 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -5,7 +5,7 @@
  */
 
 // Init app context before any other imports
-import {create, Registry, Finalizable} from '../services/registry';
+import {create, Registry} from '../services/registry';
 import {AppContext} from '../services/app-context';
 import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock';
 import {grRestApiMock} from './mocks/gr-rest-api_mock';
@@ -22,6 +22,7 @@
   diffModelToken,
   DiffModel,
 } from '../embed/diff/gr-diff-model/gr-diff-model';
+import {Finalizable} from '../types/types';
 
 export function createTestAppContext(): AppContext & Finalizable {
   const appRegistry: Registry<AppContext> = {
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index 17720fd..43e06a4 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -19,6 +19,12 @@
   Timestamp,
 } from './common';
 
+// A finalizable object has a single method `finalize` that is called when
+// the object is no longer needed and should clean itself up.
+export interface Finalizable {
+  finalize(): void;
+}
+
 export function isDefined<T>(x: T): x is NonNullable<T> {
   return x !== undefined && x !== null;
 }
diff --git a/polygerrit-ui/app/utils/patch-set-util.ts b/polygerrit-ui/app/utils/patch-set-util.ts
index dffc091..7a5cd45 100644
--- a/polygerrit-ui/app/utils/patch-set-util.ts
+++ b/polygerrit-ui/app/utils/patch-set-util.ts
@@ -327,24 +327,32 @@
   return branch as BranchName;
 }
 
-export function getParentInfoString(
+export function getParentCommit(
   rev?: RevisionInfo | EditRevisionInfo,
   index?: number
 ) {
   const parents = rev?.parents_data ?? [];
   const parent = parents[index ?? 0];
   if (!parent) return '';
+  return shorten(parent.commit_id) ?? '';
+}
 
-  let info = '';
-  if (parent.change_number) {
-    info = `${info}Change ${parent.change_number} at patchset ${parent.patch_set_number}\n`;
-  }
+export function getParentInfoString(
+  rev?: RevisionInfo | EditRevisionInfo,
+  index?: number
+) {
+  const parents = rev?.parents_data ?? [];
+  const parent = parents[index ?? 0];
+  if (!parent || parent.is_merged_in_target_branch) return '';
 
-  if (parent.branch_name) {
-    const commit = shorten(parent.commit_id) ?? 'unknown';
-    info = `${info}Branch ${branchName(
-      parent.branch_name
-    )} at commit ${commit} `;
+  if (index === 0) {
+    if (parent.change_number) {
+      return `Patchset ${parent.patch_set_number} of Change ${parent.change_number}`;
+    } else {
+      return 'Warning: The base commit is not known (aka reachable) in the target branch.';
+    }
+  } else {
+    // For merge changes the parents with index > 0 are expected to be from a different branch.
+    return 'Other branch';
   }
-  return info;
 }