Merge "Fix "Related Changes" link in User Guide"
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index dd82f27..967946d 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -43,6 +43,11 @@
 
 The following endpoints are available to plugins.
 
+=== auth-link
+The `auth-link` extension point is located in the top right corner of anonymous
+pages. The purpose is to improve user experience for custom OAuth providers by
+providing custom components and/or visual feedback of authentication progress.
+
 === banner
 The `banner` extension point is located at the top of all pages. The purpose
 is to allow plugins to show outage information and important announcements to
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index 1d9a0af..fb28d30 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.accounts.AccountApi;
 import com.google.gerrit.extensions.api.config.Server;
+import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.json.OutputFormat;
@@ -94,6 +95,8 @@
       case CHANGE:
       case DIFF:
         data.put(
+            "defaultChangeDetailHex", ListOption.toHex(IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS));
+        data.put(
             "changeRequestsPath",
             IndexPreloadingUtil.computeChangeRequestsPath(requestedPath, page).get());
         data.put("changeNum", IndexPreloadingUtil.computeChangeNum(requestedPath, page).get());
@@ -118,6 +121,7 @@
           serializeObject(GSON, accountApi.getEditPreferences()));
       data.put("userIsAuthenticated", true);
       if (page == RequestedPage.DASHBOARD) {
+        data.put("defaultDashboardHex", ListOption.toHex(IndexPreloadingUtil.DASHBOARD_OPTIONS));
         data.put("dashboardQuery", IndexPreloadingUtil.computeDashboardQueryList());
       }
     } catch (AuthException e) {
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index 3ada18d..36fa61b 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -17,10 +17,12 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.UsedAt.Project;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.restapi.Url;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -81,6 +83,25 @@
               NEW_USER)
           .map(query -> query.replaceAll("\\$\\{user}", "self"))
           .collect(toImmutableList());
+  public static final ImmutableSet<ListChangesOption> DASHBOARD_OPTIONS =
+      ImmutableSet.of(
+          ListChangesOption.LABELS,
+          ListChangesOption.DETAILED_ACCOUNTS,
+          ListChangesOption.SUBMIT_REQUIREMENTS,
+          ListChangesOption.STAR);
+
+  public static final ImmutableSet<ListChangesOption> CHANGE_DETAIL_OPTIONS =
+      ImmutableSet.of(
+          ListChangesOption.ALL_COMMITS,
+          ListChangesOption.ALL_REVISIONS,
+          ListChangesOption.CHANGE_ACTIONS,
+          ListChangesOption.DETAILED_LABELS,
+          ListChangesOption.DOWNLOAD_COMMANDS,
+          ListChangesOption.MESSAGES,
+          ListChangesOption.SUBMITTABLE,
+          ListChangesOption.WEB_LINKS,
+          ListChangesOption.SKIP_DIFFSTAT,
+          ListChangesOption.SUBMIT_REQUIREMENTS);
 
   @Nullable
   public static String getPath(@Nullable String requestedURL) throws URISyntaxException {
diff --git a/java/com/google/gerrit/server/config/ConfigUtil.java b/java/com/google/gerrit/server/config/ConfigUtil.java
index 5d94255..22c3d99 100644
--- a/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -17,6 +17,9 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
+import java.io.IOException;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Modifier;
@@ -391,6 +394,41 @@
     return s;
   }
 
+  /**
+   * Update user config by applying the specified delta
+   *
+   * <p>As opposed to {@link com.google.gerrit.server.config.ConfigUtil#storeSection}, this method
+   * does not unset a variable that are set to default, because it is expected that the input {@code
+   * original} is the raw user config value (does not include the defaults)
+   *
+   * <p>To use this method with the proto config (see {@link
+   * CachedPreferences#asUserPreferencesProto()}), the caller can first convert the proto to a java
+   * class usign one of the {@link UserPreferencesConverter} classes.
+   *
+   * <p>Fields marked with final or transient modifiers are skipped.
+   *
+   * @param original the original current user config
+   * @param updateDelta instance of class with config values that need to be uplied to the original
+   *     config
+   */
+  @UsedAt(Project.GOOGLE)
+  public static <T> void updatePreferences(T original, T updateDelta) throws IOException {
+    try {
+      for (Field f : updateDelta.getClass().getDeclaredFields()) {
+        if (skipField(f)) {
+          continue;
+        }
+        f.setAccessible(true);
+        Object c = f.get(updateDelta);
+        if (c != null) {
+          f.set(original, c);
+        }
+      }
+    } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
+      throw new IOException("cannot apply delta the original config", e);
+    }
+  }
+
   public static boolean skipField(Field field) {
     int modifiers = field.getModifiers();
     return Modifier.isFinal(modifiers) || Modifier.isTransient(modifiers);
diff --git a/java/com/google/gerrit/server/config/UserPreferencesConverter.java b/java/com/google/gerrit/server/config/UserPreferencesConverter.java
index bb611ad..2dccabd 100644
--- a/java/com/google/gerrit/server/config/UserPreferencesConverter.java
+++ b/java/com/google/gerrit/server/config/UserPreferencesConverter.java
@@ -31,8 +31,8 @@
  * <p>Upstream, we use java representations of the preference classes. Internally, we store proto
  * equivalents in Spanner.
  */
-final class UserPreferencesConverter {
-  static final class GeneralPreferencesInfoConverter {
+public final class UserPreferencesConverter {
+  public static final class GeneralPreferencesInfoConverter {
     public static UserPreferences.GeneralPreferencesInfo toProto(GeneralPreferencesInfo info) {
       UserPreferences.GeneralPreferencesInfo.Builder builder =
           UserPreferences.GeneralPreferencesInfo.newBuilder();
@@ -197,7 +197,7 @@
     private GeneralPreferencesInfoConverter() {}
   }
 
-  static final class DiffPreferencesInfoConverter {
+  public static final class DiffPreferencesInfoConverter {
     public static UserPreferences.DiffPreferencesInfo toProto(DiffPreferencesInfo info) {
       UserPreferences.DiffPreferencesInfo.Builder builder =
           UserPreferences.DiffPreferencesInfo.newBuilder();
@@ -272,7 +272,7 @@
     private DiffPreferencesInfoConverter() {}
   }
 
-  static final class EditPreferencesInfoConverter {
+  public static final class EditPreferencesInfoConverter {
     public static UserPreferences.EditPreferencesInfo toProto(EditPreferencesInfo info) {
       UserPreferences.EditPreferencesInfo.Builder builder =
           UserPreferences.EditPreferencesInfo.newBuilder();
diff --git a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
index b52b2d1..f6a4ce1 100644
--- a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
+++ b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
@@ -89,18 +89,19 @@
   public void onGitBatchRefUpdate(GitBatchRefUpdateListener.Event event) {
     if (allUsersName.get().equals(event.getProjectName())) {
       for (UpdatedRef ref : event.getUpdatedRefs()) {
-        if (!RefNames.REFS_CONFIG.equals(ref.getRefName())) {
-          if (ref.getRefName().startsWith(RefNames.REFS_STARRED_CHANGES)) {
-            break;
-          }
+        if (RefNames.isRefsUsers(ref.getRefName()) && !RefNames.isRefsEdit(ref.getRefName())) {
           Account.Id accountId = Account.Id.fromRef(ref.getRefName());
           if (accountId != null) {
             indexer.get().index(accountId);
           }
         }
       }
-      // The update is in All-Users and not on refs/meta/config. So it's not a change. Return early.
-      return;
+      if (event.getUpdatedRefs().stream()
+          .noneMatch(ru -> ru.getRefName().equals(RefNames.REFS_CONFIG))) {
+        // The update is in All-Users and not on refs/meta/config. So it's not a change. Return
+        // early.
+        return;
+      }
     }
 
     for (UpdatedRef ref : event.getUpdatedRefs()) {
diff --git a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
index 7562b49..f6faa4e 100644
--- a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
+++ b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
@@ -110,20 +110,22 @@
           InvalidChangeOperationException {
     PatchSet currentPatchset = notes.getCurrentPatchSet();
 
-    PatchSet.Id latestApprovedPatchsetId = getLatestApprovedPatchsetId(notes);
-    if (latestApprovedPatchsetId.get() == currentPatchset.id().get()) {
+    Optional<PatchSet.Id> latestApprovedPatchsetId = getLatestApprovedPatchsetId(notes);
+    if (latestApprovedPatchsetId.isEmpty()
+        || latestApprovedPatchsetId.get().get() == currentPatchset.id().get()) {
       // If the latest approved patchset is the current patchset, no need to return anything.
       return "";
     }
     StringBuilder diff =
         new StringBuilder(
             String.format(
-                "\n\n%d is the latest approved patch-set.\n", latestApprovedPatchsetId.get()));
+                "\n\n%d is the latest approved patch-set.\n",
+                latestApprovedPatchsetId.get().get()));
     Map<String, FileDiffOutput> modifiedFiles =
         listModifiedFiles(
             notes.getProjectName(),
             currentPatchset,
-            notes.getPatchSets().get(latestApprovedPatchsetId));
+            notes.getPatchSets().get(latestApprovedPatchsetId.get()));
 
     // To make the message a bit more concise, we skip the magic files.
     List<FileDiffOutput> modifiedFilesList =
@@ -173,7 +175,7 @@
             getDiffForFile(
                 notes,
                 currentPatchset.id(),
-                latestApprovedPatchsetId,
+                latestApprovedPatchsetId.get(),
                 fileDiff,
                 currentUser,
                 formatterResult,
@@ -290,10 +292,10 @@
     return diffPreferencesInfo;
   }
 
-  private PatchSet.Id getLatestApprovedPatchsetId(ChangeNotes notes) {
+  private Optional<PatchSet.Id> getLatestApprovedPatchsetId(ChangeNotes notes) {
     ProjectState projectState =
         projectCache.get(notes.getProjectName()).orElseThrow(illegalState(notes.getProjectName()));
-    PatchSet.Id maxPatchSetId = PatchSet.id(notes.getChangeId(), 1);
+    Optional<PatchSet.Id> maxPatchSetId = Optional.empty();
     for (PatchSetApproval patchSetApproval : notes.getApprovals().onlyNonCopied().values()) {
       if (!patchSetApproval.label().equals(LabelId.CODE_REVIEW)) {
         continue;
@@ -303,8 +305,9 @@
       if (!lt.isPresent() || !lt.get().isMaxPositive(patchSetApproval)) {
         continue;
       }
-      if (patchSetApproval.patchSetId().get() > maxPatchSetId.get()) {
-        maxPatchSetId = patchSetApproval.patchSetId();
+      if (maxPatchSetId.isEmpty()
+          || patchSetApproval.patchSetId().get() > maxPatchSetId.get().get()) {
+        maxPatchSetId = Optional.of(patchSetApproval.patchSetId());
       }
     }
     return maxPatchSetId;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 7f52d71..8bdd42f 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -160,6 +160,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.git.ObjectIds;
+import com.google.gerrit.httpd.raw.IndexPreloadingUtil;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -238,13 +239,6 @@
   @Inject private AccountControl.Factory accountControlFactory;
   @Inject private ChangeOperations changeOperations;
 
-  public static final ImmutableSet<ListChangesOption> DASHBOARD_OPTIONS =
-      ImmutableSet.of(
-          ListChangesOption.LABELS,
-          ListChangesOption.DETAILED_ACCOUNTS,
-          ListChangesOption.SUBMIT_REQUIREMENTS,
-          ListChangesOption.STAR);
-
   @Inject
   @Named("diff_intraline")
   private Cache<IntraLineDiffKey, IntraLineDiff> intraCache;
@@ -3185,7 +3179,7 @@
               gApi.changes()
                   .query()
                   .withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
-                  .withOptions(DASHBOARD_OPTIONS)
+                  .withOptions(IndexPreloadingUtil.DASHBOARD_OPTIONS)
                   .get())
           .hasSize(2);
     }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
index d1e6bcba..ab2f358 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
@@ -70,6 +70,7 @@
 import com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.httpd.raw.IndexPreloadingUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.testing.TestLabels;
@@ -102,13 +103,6 @@
   @Inject private ExtensionRegistry extensionRegistry;
   @Inject private IndexOperations.Change changeIndexOperations;
 
-  private static final ImmutableSet<ListChangesOption> DASHBOARD_OPTIONS =
-      ImmutableSet.of(
-          ListChangesOption.LABELS,
-          ListChangesOption.DETAILED_ACCOUNTS,
-          ListChangesOption.SUBMIT_REQUIREMENTS,
-          ListChangesOption.STAR);
-
   @Test
   public void submitRecords() throws Exception {
     PushOneCommit.Result r = createChange();
@@ -2958,7 +2952,7 @@
               .withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
               .withOptions(
                   new ImmutableSet.Builder<ListChangesOption>()
-                      .addAll(DASHBOARD_OPTIONS)
+                      .addAll(IndexPreloadingUtil.DASHBOARD_OPTIONS)
                       .add(ListChangesOption.SUBMIT_REQUIREMENTS)
                       .build())
               .get();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
index 651130e..b0d39d5 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
@@ -16,7 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.entities.LabelFunction.NO_BLOCK;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
 import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder;
 import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -589,6 +591,34 @@
   }
 
   @Test
+  public void overriddenSubmitRequirementMissingCodeReviewVote_submitsWithoutDiff()
+      throws Exception {
+    // Set Code-Review to optional
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .upsertLabelType(
+              label(
+                      "Code-Review",
+                      value(1, "Positive"),
+                      value(0, "No score"),
+                      value(-1, "Negative"))
+                  .toBuilder()
+                  .setFunction(NO_BLOCK)
+                  .build());
+      u.save();
+    }
+
+    Change.Id changeId = changeOperations.newChange().project(project).create();
+    changeOperations.change(changeId).newPatchset().create();
+
+    // Submitted without Code-Review approval
+    gApi.changes().id(changeId.get()).current().submit();
+
+    assertThat(Iterables.getLast(gApi.changes().id(changeId.get()).messages()).message)
+        .isEqualTo("Change has been successfully merged");
+  }
+
+  @Test
   public void diffChangeMessageOnSubmitWithStickyVote_noChanges() throws Exception {
     Change.Id changeId = changeOperations.newChange().project(project).create();
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
index 5cb7feb..cc1ee00 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
@@ -115,7 +115,9 @@
     when(gerritApi.config()).thenReturn(configApi);
 
     assertThat(dynamicTemplateData(gerritApi, "/c/project/+/123"))
-        .containsAtLeast("changeRequestsPath", "changes/project~123");
+        .containsAtLeast(
+            "defaultChangeDetailHex", "1916314",
+            "changeRequestsPath", "changes/project~123");
   }
 
   private static SanitizedContent ordain(String s) {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 7987354..b58f2fa 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -1452,7 +1452,9 @@
         this.includeComments = true;
         fireNoBubble(this, 'send', {});
         fireIronAnnounce(this, 'Reply sent');
+        return;
       })
+      .then(result => result)
       .finally(() => {
         this.getNavigation().releaseNavigation('sending review');
         this.disabled = false;
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index be94050..a55173c 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -437,7 +437,9 @@
           ></gr-icon>
         </div>
         ${this.renderRegister()}
-        <a class="loginButton" href=${this.loginUrl}>${this.loginText}</a>
+        <gr-endpoint-decorator name="auth-link">
+          <a class="loginButton" href=${this.loginUrl}>${this.loginText}</a>
+        </gr-endpoint-decorator>
         <a
           class="settingsButton"
           href="${getBaseUrl()}/settings/"
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
index d2b833f..4b9c313 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
@@ -82,7 +82,9 @@
               >
               </gr-icon>
             </div>
-            <a class="loginButton" href="/login"> Sign in </a>
+            <gr-endpoint-decorator name="auth-link">
+              <a class="loginButton" href="/login"> Sign in </a>
+            </gr-endpoint-decorator>
             <a
               aria-label="Settings"
               class="settingsButton"
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
index 4f05cca..e0febdc 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -457,7 +457,14 @@
         <paper-button class=${classMap(leftClasses)} @click=${this.selectBase}>
           Base
         </paper-button>
-        <paper-fab mini icon="gr-icons:swapHoriz" @click=${this.manualBlink}>
+        <paper-fab
+          mini
+          icon="gr-icons:swapHoriz"
+          title=${this.baseSelected
+            ? 'switch to Revision version'
+            : 'switch to Base version'}
+          @click=${this.manualBlink}
+        >
         </paper-fab>
         <paper-button
           class=${classMap(rightClasses)}
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 a942cd4..7cb75c6 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
@@ -1197,9 +1197,7 @@
     cancelCondition?: CancelConditionCallback
   ): Promise<ParsedChangeInfo | undefined> {
     if (!changeNum) return;
-    const optionsHex = listChangesOptionsToHex(
-      ...(await this.getChangeOptions())
-    );
+    const optionsHex = await this.getChangeOptionsHex();
 
     return this._getChangeDetail(
       changeNum,
@@ -1213,11 +1211,13 @@
     );
   }
 
-  /**
-   * Returns the options to use for querying multiple changes (e.g. dashboard or search).
-   * @return The options hex to use when fetching multiple changes.
-   */
   private getListChangesOptionsHex() {
+    if (
+      window.DEFAULT_DETAIL_HEXES &&
+      window.DEFAULT_DETAIL_HEXES.dashboardPage
+    ) {
+      return window.DEFAULT_DETAIL_HEXES.dashboardPage;
+    }
     const options = [
       ListChangesOption.LABELS,
       ListChangesOption.DETAILED_ACCOUNTS,
@@ -1228,6 +1228,16 @@
     return listChangesOptionsToHex(...options);
   }
 
+  async getChangeOptionsHex(): Promise<string> {
+    if (
+      window.DEFAULT_DETAIL_HEXES &&
+      window.DEFAULT_DETAIL_HEXES.dashboardPage
+    ) {
+      return window.DEFAULT_DETAIL_HEXES.dashboardPage;
+    }
+    return listChangesOptionsToHex(...(await this.getChangeOptions()));
+  }
+
   async getChangeOptions(): Promise<number[]> {
     const config = await this.getConfig(false);
 
@@ -1241,7 +1251,6 @@
       ListChangesOption.DETAILED_ACCOUNTS,
       ListChangesOption.DETAILED_LABELS,
       ListChangesOption.DOWNLOAD_COMMANDS,
-      ListChangesOption.LABELS,
       ListChangesOption.MESSAGES,
       ListChangesOption.SUBMITTABLE,
       ListChangesOption.WEB_LINKS,
@@ -1268,7 +1277,6 @@
       'DETAILED_LABELS',
       'DETAILED_ACCOUNTS',
       'DOWNLOAD_COMMANDS',
-      'LABELS',
       'MESSAGES',
       'SUBMITTABLE',
       'WEB_LINKS',
diff --git a/polygerrit-ui/app/types/globals.ts b/polygerrit-ui/app/types/globals.ts
index 7448a27..4a709b0 100644
--- a/polygerrit-ui/app/types/globals.ts
+++ b/polygerrit-ui/app/types/globals.ts
@@ -23,6 +23,12 @@
     // it's defined because of limitations from typescript, which don't import .mjs
     page?: unknown;
     hljs?: HighlightJS;
+
+    DEFAULT_DETAIL_HEXES?: {
+      diffPage?: string;
+      changePage?: string;
+      dashboardPage?: string;
+    };
     STATIC_RESOURCE_PATH?: string;
 
     PRELOADED_QUERIES?: {
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index b5892794..dbfef44 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -48,6 +48,14 @@
     // Disable extra font load from paper-styles
     window.polymerSkipLoadingFontRoboto = true;
     window.CLOSURE_NO_DEPS = true;
+    window.DEFAULT_DETAIL_HEXES = {lb}
+      {if $defaultChangeDetailHex}
+        changePage: '{$defaultChangeDetailHex}',
+      {/if}
+      {if $defaultDashboardHex}
+        dashboardPage: '{$defaultDashboardHex}',
+      {/if}
+    {rb};
     window.PRELOADED_QUERIES = {lb}
       {if $userIsAuthenticated and $defaultDashboardHex and $dashboardQuery}
         dashboardQuery: [{for $query in $dashboardQuery}{$query},{/for}],