Merge changes I467d0158,I6c20a2bb,If052dc0e,Ia6a5b440 into stable-3.6

* changes:
  sshd config: Add reference to the implementation sections upstream
  Documentation: Supplement key exchange supported by Apache MINA
  Documentation: Supplement 'encrypt-then-MAC' supported by Apache MINA
  Documentation: Supplement ciphers supported by Apache MINA
diff --git a/.bazelproject b/.bazelproject
index a7f5450..ad7b022 100644
--- a/.bazelproject
+++ b/.bazelproject
@@ -16,7 +16,7 @@
 targets:
   //...:all
 
-java_language_level: 8
+java_language_level: 11
 
 workspace_type: java
 
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index ce3a024..7f1a6e8 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -58,6 +58,9 @@
 link:cmd-ban-commit.html[gerrit ban-commit]::
 	Bans a commit from a project's repository.
 
+link:cmd-copy-approvals.html[gerrit copy-approvals]::
+	Copy all inferred approvals labels to the latest patch-set.
+
 link:cmd-create-branch.html[gerrit create-branch]::
 	Create a new project branch.
 
diff --git a/java/com/google/gerrit/asciidoctor/DocIndexer.java b/java/com/google/gerrit/asciidoctor/DocIndexer.java
index 513bdd7..6cbe2a9 100644
--- a/java/com/google/gerrit/asciidoctor/DocIndexer.java
+++ b/java/com/google/gerrit/asciidoctor/DocIndexer.java
@@ -107,11 +107,19 @@
 
         String title;
         try (BufferedReader titleReader = Files.newBufferedReader(file.toPath(), UTF_8)) {
-          title = titleReader.readLine();
-          if (title != null && title.startsWith("[[")) {
+          while ((title = titleReader.readLine()) != null) {
             // Generally the first line of the txt is the title. In a few cases the
-            // first line is a "[[tag]]" and the second line is the title.
-            title = titleReader.readLine();
+            // first lines are "[[tag]]" and or ":attribute:" and the next line
+            // after those  is the title.
+            if (title.startsWith("[[")) {
+              continue;
+            }
+            // Skip attributes such as :linkattrs:
+            if (title.startsWith(":") && title.endsWith(":")) {
+              continue;
+            }
+            // We found the title
+            break;
           }
         }
         Matcher matcher = SECTION_HEADER.matcher(title);
diff --git a/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java b/java/com/google/gerrit/httpd/GuiceRequestScopePropagator.java
similarity index 80%
rename from java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
rename to java/com/google/gerrit/httpd/GuiceRequestScopePropagator.java
index 99dd8bf..7c8094a 100644
--- a/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
+++ b/java/com/google/gerrit/httpd/GuiceRequestScopePropagator.java
@@ -12,11 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.util;
+package com.google.gerrit.httpd;
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.Provider;
@@ -29,21 +31,25 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.Callable;
+import javax.servlet.http.HttpServletRequest;
 
 /** Propagator for Guice's built-in servlet scope. */
 public class GuiceRequestScopePropagator extends RequestScopePropagator {
 
   private final String url;
   private final SocketAddress peer;
+  private final Provider<HttpServletRequest> request;
 
   @Inject
   GuiceRequestScopePropagator(
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       @RemotePeer Provider<SocketAddress> remotePeerProvider,
-      ThreadLocalRequestContext local) {
+      ThreadLocalRequestContext local,
+      Provider<HttpServletRequest> request) {
     super(ServletScopes.REQUEST, local);
     this.url = urlProvider != null ? urlProvider.get() : null;
     this.peer = remotePeerProvider.get();
+    this.request = request;
   }
 
   /** @see RequestScopePropagator#wrap(Callable) */
@@ -64,6 +70,11 @@
     seedMap.put(Key.get(typeOfProvider(SocketAddress.class), RemotePeer.class), Providers.of(peer));
     seedMap.put(Key.get(SocketAddress.class, RemotePeer.class), peer);
 
+    Key<?> webSessionAttrKey = Key.get(WebSession.class);
+    Object webSessionAttrValue = request.get().getAttribute(webSessionAttrKey.toString());
+    seedMap.put(webSessionAttrKey, webSessionAttrValue);
+    seedMap.put(Key.get(typeOfProvider(WebSession.class)), Providers.of(webSessionAttrValue));
+
     return ServletScopes.continueRequest(callable, seedMap);
   }
 
diff --git a/java/com/google/gerrit/httpd/WebModule.java b/java/com/google/gerrit/httpd/WebModule.java
index da485cc..79dde85 100644
--- a/java/com/google/gerrit/httpd/WebModule.java
+++ b/java/com/google/gerrit/httpd/WebModule.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.config.GitwebCgiConfig;
 import com.google.gerrit.server.git.receive.AsyncReceiveCommits.AsyncReceiveCommitsModule;
-import com.google.gerrit.server.util.GuiceRequestScopePropagator;
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.inject.Inject;
 import com.google.inject.ProvisionException;
diff --git a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index dddc298..a03aa36 100644
--- a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -489,7 +489,7 @@
     }
 
     if (toc != null) {
-      appendPageAsSection(scanner, toc, "Documentaion", md);
+      appendPageAsSection(scanner, toc, "Documentation", md);
     } else {
       appendEntriesSection(scanner, docs, "Documentation", md, prefix, 0);
       appendEntriesSection(scanner, servlets, "Servlets", md, prefix, "servlet-".length());
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index 0d5e7b3..7684545 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -56,6 +56,7 @@
  */
 public abstract class QueryProcessor<T> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final int MAX_LIMIT_BUFFER_MULTIPLIER = 100;
 
   protected static class Metrics {
     final Timer1<String> executionTime;
@@ -366,7 +367,7 @@
 
   private int getEffectiveLimit(Predicate<T> p) {
     if (isNoLimit == true) {
-      return Integer.MAX_VALUE;
+      return getIndexSize() + MAX_LIMIT_BUFFER_MULTIPLIER * getBatchSize();
     }
     List<Integer> possibleLimits = new ArrayList<>(4);
     possibleLimits.add(getBackendSupportedLimit());
@@ -394,4 +395,8 @@
   }
 
   protected abstract String formatForLogging(T t);
+
+  protected abstract int getIndexSize();
+
+  protected abstract int getBatchSize();
 }
diff --git a/java/com/google/gerrit/server/account/AccountAttributeLoader.java b/java/com/google/gerrit/server/account/AccountAttributeLoader.java
new file mode 100644
index 0000000..ae57941
--- /dev/null
+++ b/java/com/google/gerrit/server/account/AccountAttributeLoader.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2022 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.account;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.inject.Inject;
+import java.util.HashMap;
+import java.util.Map;
+
+public class AccountAttributeLoader {
+
+  public interface Factory {
+    AccountAttributeLoader create();
+  }
+
+  private final InternalAccountDirectory directory;
+  private final Map<Account.Id, AccountAttribute> created = new HashMap<>();
+
+  @Inject
+  AccountAttributeLoader(InternalAccountDirectory directory) {
+    this.directory = directory;
+  }
+
+  @Nullable
+  public synchronized AccountAttribute get(@Nullable Account.Id id) {
+    if (id == null) {
+      return null;
+    }
+    return created.computeIfAbsent(id, k -> new AccountAttribute(k.get()));
+  }
+
+  public void fill() {
+    directory.fillAccountAttributeInfo(created.values());
+  }
+}
diff --git a/java/com/google/gerrit/server/account/AccountDirectory.java b/java/com/google/gerrit/server/account/AccountDirectory.java
index 98b2ca9..10aecd3 100644
--- a/java/com/google/gerrit/server/account/AccountDirectory.java
+++ b/java/com/google/gerrit/server/account/AccountDirectory.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import java.util.Set;
 
@@ -24,7 +25,7 @@
  * <p>Implementations supply data to Gerrit about user accounts.
  */
 public abstract class AccountDirectory {
-  /** Fields to be populated for a REST API response. */
+  /** Fields to be populated for SSH or REST API response. */
   public enum FillOptions {
     /** Full name or username. */
     NAME,
@@ -59,4 +60,6 @@
 
   public abstract void fillAccountInfo(Iterable<? extends AccountInfo> in, Set<FillOptions> options)
       throws PermissionBackendException;
+
+  public abstract void fillAccountAttributeInfo(Iterable<? extends AccountAttribute> in);
 }
diff --git a/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index 130fa44..f7b0b60 100644
--- a/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.avatar.AvatarProvider;
+import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -52,6 +53,9 @@
 @Singleton
 public class InternalAccountDirectory extends AccountDirectory {
   static final Set<FillOptions> ID_ONLY = Collections.unmodifiableSet(EnumSet.of(FillOptions.ID));
+  static final Set<FillOptions> ALL_ACCOUNT_ATTRIBUTES =
+      Collections.unmodifiableSet(
+          EnumSet.of(FillOptions.NAME, FillOptions.EMAIL, FillOptions.USERNAME));
 
   public static class InternalAccountDirectoryModule extends AbstractModule {
     @Override
@@ -129,6 +133,44 @@
     }
   }
 
+  @Override
+  public void fillAccountAttributeInfo(Iterable<? extends AccountAttribute> in) {
+    Set<Account.Id> ids = stream(in).map(a -> Account.id(a.accountId)).collect(toSet());
+    Map<Account.Id, AccountState> accountStates = accountCache.get(ids);
+    for (AccountAttribute accountAttribute : in) {
+      Account.Id id = Account.id(accountAttribute.accountId);
+      AccountState accountState = accountStates.get(id);
+      if (accountState != null) {
+        fill(accountAttribute, accountState, ALL_ACCOUNT_ATTRIBUTES);
+      } else {
+        accountAttribute.accountId = null;
+      }
+    }
+  }
+
+  private void fill(
+      AccountAttribute accountAttribute, AccountState accountState, Set<FillOptions> options) {
+    Account account = accountState.account();
+    if (options.contains(FillOptions.NAME)) {
+      accountAttribute.name = Strings.emptyToNull(account.fullName());
+      if (accountAttribute.name == null) {
+        accountAttribute.name = accountState.userName().orElse(null);
+      }
+    }
+    if (options.contains(FillOptions.EMAIL)) {
+      accountAttribute.email = account.preferredEmail();
+    }
+    if (options.contains(FillOptions.USERNAME)) {
+      accountAttribute.username = accountState.userName().orElse(null);
+    }
+    if (options.contains(FillOptions.ID)) {
+      accountAttribute.accountId = account.id().get();
+    } else {
+      // Was previously set to look up account for filling.
+      accountAttribute.accountId = null;
+    }
+  }
+
   private void fill(AccountInfo info, AccountState accountState, Set<FillOptions> options) {
     Account account = accountState.account();
     if (options.contains(FillOptions.ID)) {
diff --git a/java/com/google/gerrit/server/data/AccountAttribute.java b/java/com/google/gerrit/server/data/AccountAttribute.java
index 19605a2..9be221b 100644
--- a/java/com/google/gerrit/server/data/AccountAttribute.java
+++ b/java/com/google/gerrit/server/data/AccountAttribute.java
@@ -18,4 +18,11 @@
   public String name;
   public String email;
   public String username;
+  public Integer accountId;
+
+  public AccountAttribute(Integer id) {
+    this.accountId = id;
+  }
+
+  public AccountAttribute() {}
 }
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 2b402a6..95f6d96 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountAttributeLoader;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.Emails;
@@ -121,7 +122,7 @@
     this.accountTemplateUtil = accountTemplateUtil;
   }
 
-  public ChangeAttribute asChangeAttribute(Change change) {
+  public ChangeAttribute asChangeAttribute(Change change, AccountAttributeLoader accountLoader) {
     ChangeAttribute a = new ChangeAttribute();
     a.project = change.getProject().get();
     a.branch = change.getDest().shortName();
@@ -129,15 +130,9 @@
     a.id = change.getKey().get();
     a.number = change.getId().get();
     a.subject = change.getSubject();
-    try {
-      a.commitMessage = changeDataFactory.create(change).commitMessage();
-    } catch (Exception e) {
-      logger.atSevere().withCause(e).log(
-          "Error while getting full commit message for change %d", a.number);
-    }
     a.url = getChangeUrl(change);
-    a.owner = asAccountAttribute(change.getOwner());
-    a.assignee = asAccountAttribute(change.getAssignee());
+    a.owner = asAccountAttribute(change.getOwner(), accountLoader);
+    a.assignee = asAccountAttribute(change.getAssignee(), accountLoader);
     a.status = change.getStatus();
     a.createdOn = change.getCreatedOn().getEpochSecond();
     a.wip = change.isWorkInProgress() ? true : null;
@@ -151,12 +146,9 @@
 
   /** Create a {@link ChangeAttribute} instance from the specified change. */
   public ChangeAttribute asChangeAttribute(Change change, ChangeNotes notes) {
-    ChangeAttribute a = asChangeAttribute(change);
-    Set<String> hashtags = notes.load().getHashtags();
-    if (!hashtags.isEmpty()) {
-      a.hashtags = new ArrayList<>(hashtags.size());
-      a.hashtags.addAll(hashtags);
-    }
+    ChangeAttribute a = asChangeAttribute(change, (AccountAttributeLoader) null);
+    addHashTags(a, notes);
+    addCommitMessage(a, notes);
     return a;
   }
   /**
@@ -180,25 +172,27 @@
   }
 
   /** Add allReviewers to an existing {@link ChangeAttribute}. */
-  public void addAllReviewers(ChangeAttribute a, ChangeNotes notes) {
+  public void addAllReviewers(
+      ChangeAttribute a, ChangeNotes notes, AccountAttributeLoader accountLoader) {
     Collection<Account.Id> reviewers = approvalsUtil.getReviewers(notes).all();
     if (!reviewers.isEmpty()) {
       a.allReviewers = Lists.newArrayListWithCapacity(reviewers.size());
       for (Account.Id id : reviewers) {
-        a.allReviewers.add(asAccountAttribute(id));
+        a.allReviewers.add(asAccountAttribute(id, accountLoader));
       }
     }
   }
 
   /** Add submitRecords to an existing {@link ChangeAttribute}. */
-  public void addSubmitRecords(ChangeAttribute ca, List<SubmitRecord> submitRecords) {
+  public void addSubmitRecords(
+      ChangeAttribute ca, List<SubmitRecord> submitRecords, AccountAttributeLoader accountLoader) {
     ca.submitRecords = new ArrayList<>();
 
     for (SubmitRecord submitRecord : submitRecords) {
       SubmitRecordAttribute sa = new SubmitRecordAttribute();
       sa.status = submitRecord.status.name();
       if (submitRecord.status != SubmitRecord.Status.RULE_ERROR) {
-        addSubmitRecordLabels(submitRecord, sa);
+        addSubmitRecordLabels(submitRecord, sa, accountLoader);
         addSubmitRecordRequirements(submitRecord, sa);
       }
       ca.submitRecords.add(sa);
@@ -209,7 +203,8 @@
     }
   }
 
-  private void addSubmitRecordLabels(SubmitRecord submitRecord, SubmitRecordAttribute sa) {
+  private void addSubmitRecordLabels(
+      SubmitRecord submitRecord, SubmitRecordAttribute sa, AccountAttributeLoader accountLoader) {
     if (submitRecord.labels != null && !submitRecord.labels.isEmpty()) {
       sa.labels = new ArrayList<>();
       for (SubmitRecord.Label lbl : submitRecord.labels) {
@@ -217,7 +212,7 @@
         la.label = lbl.label;
         la.status = lbl.status.name();
         if (lbl.appliedBy != null) {
-          la.by = asAccountAttribute(lbl.appliedBy);
+          la.by = asAccountAttribute(lbl.appliedBy, accountLoader);
         }
         sa.labels.add(la);
       }
@@ -352,13 +347,23 @@
     a.commitMessage = commitMessage;
   }
 
+  private void addCommitMessage(ChangeAttribute changeAttribute, ChangeNotes notes) {
+    try {
+      addCommitMessage(changeAttribute, changeDataFactory.create(notes).commitMessage());
+    } catch (Exception e) {
+      logger.atSevere().withCause(e).log(
+          "Error while getting full commit message for change %d", changeAttribute.number);
+    }
+  }
+
   public void addPatchSets(
       RevWalk revWalk,
       ChangeAttribute ca,
       Collection<PatchSet> ps,
       Map<PatchSet.Id, Collection<PatchSetApproval>> approvals,
-      LabelTypes labelTypes) {
-    addPatchSets(revWalk, ca, ps, approvals, false, null, labelTypes);
+      LabelTypes labelTypes,
+      AccountAttributeLoader accountLoader) {
+    addPatchSets(revWalk, ca, ps, approvals, false, null, labelTypes, accountLoader);
   }
 
   public void addPatchSets(
@@ -368,13 +373,14 @@
       Map<PatchSet.Id, Collection<PatchSetApproval>> approvals,
       boolean includeFiles,
       Change change,
-      LabelTypes labelTypes) {
+      LabelTypes labelTypes,
+      AccountAttributeLoader accountLoader) {
     if (!ps.isEmpty()) {
       ca.patchSets = new ArrayList<>(ps.size());
       for (PatchSet p : ps) {
-        PatchSetAttribute psa = asPatchSetAttribute(revWalk, change, p);
+        PatchSetAttribute psa = asPatchSetAttribute(revWalk, change, p, accountLoader);
         if (approvals != null) {
-          addApprovals(psa, p.id(), approvals, labelTypes);
+          addApprovals(psa, p.id(), approvals, labelTypes, accountLoader);
         }
         ca.patchSets.add(psa);
         if (includeFiles) {
@@ -385,13 +391,15 @@
   }
 
   public void addPatchSetComments(
-      PatchSetAttribute patchSetAttribute, Collection<HumanComment> comments) {
+      PatchSetAttribute patchSetAttribute,
+      Collection<HumanComment> comments,
+      AccountAttributeLoader accountLoader) {
     for (HumanComment comment : comments) {
       if (comment.key.patchSetId == patchSetAttribute.number) {
         if (patchSetAttribute.comments == null) {
           patchSetAttribute.comments = new ArrayList<>();
         }
-        patchSetAttribute.comments.add(asPatchSetLineAttribute(comment));
+        patchSetAttribute.comments.add(asPatchSetLineAttribute(comment, accountLoader));
       }
     }
   }
@@ -421,22 +429,30 @@
     }
   }
 
-  public void addComments(ChangeAttribute ca, Collection<ChangeMessage> messages) {
+  public void addComments(
+      ChangeAttribute ca,
+      Collection<ChangeMessage> messages,
+      AccountAttributeLoader accountLoader) {
     if (!messages.isEmpty()) {
       ca.comments = new ArrayList<>();
       for (ChangeMessage message : messages) {
-        ca.comments.add(asMessageAttribute(message));
+        ca.comments.add(asMessageAttribute(message, accountLoader));
       }
     }
   }
 
-  /** Create a PatchSetAttribute for the given patchset suitable for serialization to JSON. */
   public PatchSetAttribute asPatchSetAttribute(RevWalk revWalk, Change change, PatchSet patchSet) {
+    return asPatchSetAttribute(revWalk, change, patchSet, null);
+  }
+
+  /** Create a PatchSetAttribute for the given patchset suitable for serialization to JSON. */
+  public PatchSetAttribute asPatchSetAttribute(
+      RevWalk revWalk, Change change, PatchSet patchSet, AccountAttributeLoader accountLoader) {
     PatchSetAttribute p = new PatchSetAttribute();
     p.revision = patchSet.commitId().name();
     p.number = patchSet.number();
     p.ref = patchSet.refName();
-    p.uploader = asAccountAttribute(patchSet.uploader());
+    p.uploader = asAccountAttribute(patchSet.uploader(), accountLoader);
     p.createdOn = patchSet.createdOn().getEpochSecond();
     PatchSet.Id pId = patchSet.id();
     try {
@@ -453,7 +469,7 @@
         p.author.name = author.getName();
         p.author.username = "";
       } else {
-        p.author = asAccountAttribute(author.getAccount());
+        p.author = asAccountAttribute(author.getAccount(), accountLoader);
       }
 
       Map<String, FileDiffOutput> modifiedFiles =
@@ -476,20 +492,24 @@
       PatchSetAttribute p,
       PatchSet.Id id,
       Map<PatchSet.Id, Collection<PatchSetApproval>> all,
-      LabelTypes labelTypes) {
+      LabelTypes labelTypes,
+      AccountAttributeLoader accountLoader) {
     Collection<PatchSetApproval> list = all.get(id);
     if (list != null) {
-      addApprovals(p, list, labelTypes);
+      addApprovals(p, list, labelTypes, accountLoader);
     }
   }
 
   public void addApprovals(
-      PatchSetAttribute p, Collection<PatchSetApproval> list, LabelTypes labelTypes) {
+      PatchSetAttribute p,
+      Collection<PatchSetApproval> list,
+      LabelTypes labelTypes,
+      AccountAttributeLoader accountLoader) {
     if (!list.isEmpty()) {
       p.approvals = new ArrayList<>(list.size());
       for (PatchSetApproval a : list) {
         if (a.value() != 0) {
-          p.approvals.add(asApprovalAttribute(a, labelTypes));
+          p.approvals.add(asApprovalAttribute(a, labelTypes, accountLoader));
         }
       }
       if (p.approvals.isEmpty()) {
@@ -498,6 +518,10 @@
     }
   }
 
+  public AccountAttribute asAccountAttribute(Account.Id id, AccountAttributeLoader accountLoader) {
+    return accountLoader != null ? accountLoader.get(id) : asAccountAttribute(id);
+  }
+
   /** Create an AuthorAttribute for the given account suitable for serialization to JSON. */
   public AccountAttribute asAccountAttribute(Account.Id id) {
     if (id == null) {
@@ -529,11 +553,12 @@
    * @param labelTypes label types for the containing project
    * @return object suitable for serialization to JSON
    */
-  public ApprovalAttribute asApprovalAttribute(PatchSetApproval approval, LabelTypes labelTypes) {
+  public ApprovalAttribute asApprovalAttribute(
+      PatchSetApproval approval, LabelTypes labelTypes, AccountAttributeLoader accountLoader) {
     ApprovalAttribute a = new ApprovalAttribute();
     a.type = approval.labelId().get();
     a.value = Short.toString(approval.value());
-    a.by = asAccountAttribute(approval.accountId());
+    a.by = asAccountAttribute(approval.accountId(), accountLoader);
     a.grantedOn = approval.granted().getEpochSecond();
     a.oldValue = null;
 
@@ -542,20 +567,22 @@
     return a;
   }
 
-  public MessageAttribute asMessageAttribute(ChangeMessage message) {
+  public MessageAttribute asMessageAttribute(
+      ChangeMessage message, AccountAttributeLoader accountLoader) {
     MessageAttribute a = new MessageAttribute();
     a.timestamp = message.getWrittenOn().getEpochSecond();
     a.reviewer =
         message.getAuthor() != null
-            ? asAccountAttribute(message.getAuthor())
+            ? asAccountAttribute(message.getAuthor(), accountLoader)
             : asAccountAttribute(myIdent.get());
     a.message = accountTemplateUtil.replaceTemplates(message.getMessage());
     return a;
   }
 
-  public PatchSetCommentAttribute asPatchSetLineAttribute(HumanComment c) {
+  public PatchSetCommentAttribute asPatchSetLineAttribute(
+      HumanComment c, AccountAttributeLoader accountLoader) {
     PatchSetCommentAttribute a = new PatchSetCommentAttribute();
-    a.reviewer = asAccountAttribute(c.author.getId());
+    a.reviewer = asAccountAttribute(c.author.getId(), accountLoader);
     a.file = c.key.filename;
     a.line = c.lineNbr;
     a.message = c.message;
@@ -569,4 +596,12 @@
     }
     return null;
   }
+
+  private void addHashTags(ChangeAttribute changeAttribute, ChangeNotes notes) {
+    Set<String> hashtags = notes.load().getHashtags();
+    if (!hashtags.isEmpty()) {
+      changeAttribute.hashtags = new ArrayList<>(hashtags.size());
+      changeAttribute.hashtags.addAll(hashtags);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/git/DelegateRepository.java b/java/com/google/gerrit/server/git/DelegateRepository.java
index d839bce..9046d9d 100644
--- a/java/com/google/gerrit/server/git/DelegateRepository.java
+++ b/java/com/google/gerrit/server/git/DelegateRepository.java
@@ -65,7 +65,8 @@
     this.delegate = delegate;
   }
 
-  Repository delegate() {
+  /** Returns the wrapped {@link Repository} instance. */
+  public Repository delegate() {
     return delegate;
   }
 
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 718eec2..dd5af2c 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PublishCommentsOp;
+import com.google.gerrit.server.cache.PerThreadCache;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -385,7 +386,7 @@
         () -> {
           String oldName = Thread.currentThread().getName();
           Thread.currentThread().setName(oldName + "-for-" + currentThreadName);
-          try {
+          try (PerThreadCache threadLocalCache = PerThreadCache.create()) {
             return receiveCommits.processCommands(commands, monitor);
           } finally {
             Thread.currentThread().setName(oldName);
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 889dfd6..c06bbbf 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -3486,7 +3486,7 @@
                     rw.markStart(newTip);
                     rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
 
-                    Map<Change.Key, ChangeNotes> byKey = null;
+                    Map<Change.Key, ChangeData> changeDataByKey = null;
                     List<ReplaceRequest> replaceAndClose = new ArrayList<>();
 
                     int existingPatchSets = 0;
@@ -3522,8 +3522,8 @@
 
                       for (String changeId :
                           ChangeUtil.getChangeIdsFromFooter(c, urlFormatter.get())) {
-                        if (byKey == null) {
-                          byKey =
+                        if (changeDataByKey == null) {
+                          changeDataByKey =
                               retryHelper
                                   .changeIndexQuery(
                                       "queryOpenChangesByKeyByBranch",
@@ -3531,14 +3531,15 @@
                                   .call();
                         }
 
-                        ChangeNotes onto = byKey.get(Change.key(changeId.trim()));
+                        ChangeData onto = changeDataByKey.get(Change.key(changeId.trim()));
                         if (onto != null) {
                           newPatchSets++;
                           // Hold onto this until we're done with the walk, as the call to
                           // req.validate below calls isMergedInto which resets the walk.
+                          ChangeNotes ontoNotes = onto.notes();
                           ReplaceRequest req =
-                              new ReplaceRequest(onto.getChangeId(), c, cmd, false);
-                          req.notes = onto;
+                              new ReplaceRequest(ontoNotes.getChangeId(), c, cmd, false);
+                          req.notes = ontoNotes;
                           replaceAndClose.add(req);
                           continue COMMIT;
                         }
@@ -3611,14 +3612,17 @@
     }
   }
 
-  private Map<Change.Key, ChangeNotes> openChangesByKeyByBranch(
+  private Map<Change.Key, ChangeData> openChangesByKeyByBranch(
       InternalChangeQuery internalChangeQuery, BranchNameKey branch) {
     try (TraceTimer traceTimer =
         newTimer("openChangesByKeyByBranch", Metadata.builder().branchName(branch.branch()))) {
-      Map<Change.Key, ChangeNotes> r = new HashMap<>();
+      Map<Change.Key, ChangeData> r = new HashMap<>();
       for (ChangeData cd : internalChangeQuery.byBranchOpen(branch)) {
         try {
-          r.put(cd.change().getKey(), cd.notes());
+          // ChangeData is not materialised into a ChangeNotes for avoiding
+          // to load a potentially large number of changes meta-data into memory
+          // which would cause unnecessary disk I/O, CPU and heap utilisation.
+          r.put(cd.change().getKey(), cd);
         } catch (NoSuchChangeException e) {
           // Ignore deleted change
         }
diff --git a/java/com/google/gerrit/server/git/validators/AccountValidator.java b/java/com/google/gerrit/server/git/validators/AccountValidator.java
index 873f421..b38f405 100644
--- a/java/com/google/gerrit/server/git/validators/AccountValidator.java
+++ b/java/com/google/gerrit/server/git/validators/AccountValidator.java
@@ -91,7 +91,7 @@
       return ImmutableList.of(String.format("account '%s' does not exist", accountId.get()));
     }
 
-    if (accountId.equals(self.get().getAccountId()) && !newAccount.get().isActive()) {
+    if (!newAccount.get().isActive() && accountId.equals(self.get().getAccountId())) {
       messages.add("cannot deactivate own account");
     }
 
diff --git a/java/com/google/gerrit/server/notedb/RepoSequence.java b/java/com/google/gerrit/server/notedb/RepoSequence.java
index 47e12ff..d743921 100644
--- a/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -340,4 +340,16 @@
       counterLock.unlock();
     }
   }
+
+  /**
+   * Retrieves the last returned sequence number.
+   *
+   * <p>Explicitly calls {@link #next()} if this instance didn't return sequence number until now.
+   */
+  public int last() {
+    if (counter == 0) {
+      next();
+    }
+    return counter - 1;
+  }
 }
diff --git a/java/com/google/gerrit/server/notedb/Sequences.java b/java/com/google/gerrit/server/notedb/Sequences.java
index 7ae98778..b42253e 100644
--- a/java/com/google/gerrit/server/notedb/Sequences.java
+++ b/java/com/google/gerrit/server/notedb/Sequences.java
@@ -55,6 +55,9 @@
   private final RepoSequence changeSeq;
   private final RepoSequence groupSeq;
   private final Timer2<SequenceType, Boolean> nextIdLatency;
+  private final int accountBatchSize;
+  private final int changeBatchSize;
+  private final int groupBatchSize = 1;
 
   @Inject
   public Sequences(
@@ -65,7 +68,7 @@
       AllUsersName allUsers,
       MetricMaker metrics) {
 
-    int accountBatchSize =
+    accountBatchSize =
         cfg.getInt(
             SECTION_NOTEDB,
             NAME_ACCOUNTS,
@@ -80,7 +83,7 @@
             () -> FIRST_ACCOUNT_ID,
             accountBatchSize);
 
-    int changeBatchSize =
+    changeBatchSize =
         cfg.getInt(
             SECTION_NOTEDB,
             NAME_CHANGES,
@@ -95,7 +98,6 @@
             () -> FIRST_CHANGE_ID,
             changeBatchSize);
 
-    int groupBatchSize = 1;
     groupSeq =
         new RepoSequence(
             repoManager,
@@ -147,6 +149,18 @@
     }
   }
 
+  public int changeBatchSize() {
+    return changeBatchSize;
+  }
+
+  public int groupBatchSize() {
+    return groupBatchSize;
+  }
+
+  public int accountBatchSize() {
+    return accountBatchSize;
+  }
+
   public int currentChangeId() {
     return changeSeq.current();
   }
@@ -159,6 +173,18 @@
     return groupSeq.current();
   }
 
+  public int lastChangeId() {
+    return changeSeq.last();
+  }
+
+  public int lastGroupId() {
+    return groupSeq.last();
+  }
+
+  public int lastAccountId() {
+    return accountSeq.last();
+  }
+
   public void setChangeIdValue(int value) {
     changeSeq.storeNew(value);
   }
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java b/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
index 2e29bbd..e380ef1 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.index.account.AccountIndexRewriter;
 import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -41,6 +42,7 @@
  */
 public class AccountQueryProcessor extends QueryProcessor<AccountState> {
   private final AccountControl.Factory accountControlFactory;
+  private final Sequences sequences;
 
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
@@ -57,7 +59,8 @@
       IndexConfig indexConfig,
       AccountIndexCollection indexes,
       AccountIndexRewriter rewriter,
-      AccountControl.Factory accountControlFactory) {
+      AccountControl.Factory accountControlFactory,
+      Sequences sequences) {
     super(
         metricMaker,
         AccountSchemaDefinitions.INSTANCE,
@@ -67,6 +70,7 @@
         FIELD_LIMIT,
         () -> limitsFactory.create(userProvider.get()).getQueryLimit());
     this.accountControlFactory = accountControlFactory;
+    this.sequences = sequences;
   }
 
   @Override
@@ -79,4 +83,14 @@
   protected String formatForLogging(AccountState accountState) {
     return accountState.account().id().toString();
   }
+
+  @Override
+  protected int getIndexSize() {
+    return sequences.lastAccountId();
+  }
+
+  @Override
+  protected int getBatchSize() {
+    return sequences.accountBatchSize();
+  }
 }
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index ed1f2f1..7d02ecd 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.index.change.ChangeIndexRewriter;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.ArrayList;
@@ -61,6 +62,7 @@
   private final Map<String, DynamicBean> dynamicBeans = new HashMap<>();
   private final List<Extension<ChangePluginDefinedInfoFactory>>
       changePluginDefinedInfoFactoriesByPlugin = new ArrayList<>();
+  private final Sequences sequences;
 
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
@@ -77,6 +79,7 @@
       IndexConfig indexConfig,
       ChangeIndexCollection indexes,
       ChangeIndexRewriter rewriter,
+      Sequences sequences,
       ChangeIsVisibleToPredicate.Factory changeIsVisibleToPredicateFactory,
       DynamicSet<ChangePluginDefinedInfoFactory> changePluginDefinedInfoFactories) {
     super(
@@ -89,6 +92,7 @@
         () -> limitsFactory.create(userProvider.get()).getQueryLimit());
     this.userProvider = userProvider;
     this.changeIsVisibleToPredicateFactory = changeIsVisibleToPredicateFactory;
+    this.sequences = sequences;
 
     changePluginDefinedInfoFactories
         .entries()
@@ -138,4 +142,14 @@
   protected String formatForLogging(ChangeData changeData) {
     return changeData.getId().toString();
   }
+
+  @Override
+  protected int getIndexSize() {
+    return sequences.lastChangeId();
+  }
+
+  @Override
+  protected int getBatchSize() {
+    return sequences.changeBatchSize();
+  }
 }
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 1b6dc62..716cf10 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -18,6 +18,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.LabelTypes;
@@ -28,6 +29,8 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryResult;
 import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.account.AccountAttributeLoader;
+import com.google.gerrit.server.cache.PerThreadCache;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
@@ -86,6 +89,7 @@
   private final EventFactory eventFactory;
   private final TrackingFooters trackingFooters;
   private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
+  private final AccountAttributeLoader.Factory accountAttributeLoaderFactory;
 
   private OutputFormat outputFormat = OutputFormat.TEXT;
   private boolean includePatchSets;
@@ -110,13 +114,15 @@
       ChangeQueryProcessor queryProcessor,
       EventFactory eventFactory,
       TrackingFooters trackingFooters,
-      SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
+      SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory,
+      AccountAttributeLoader.Factory accountAttributeLoaderFactory) {
     this.repoManager = repoManager;
     this.queryBuilder = queryBuilder;
     this.queryProcessor = queryProcessor;
     this.eventFactory = eventFactory;
     this.trackingFooters = trackingFooters;
     this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
+    this.accountAttributeLoaderFactory = accountAttributeLoaderFactory;
   }
 
   void setLimit(int n) {
@@ -205,7 +211,7 @@
         return;
       }
 
-      try {
+      try (PerThreadCache ignored = PerThreadCache.create()) {
         final QueryStatsAttribute stats = new QueryStatsAttribute();
         stats.runTimeMilliseconds = TimeUtil.nowMs();
 
@@ -214,9 +220,13 @@
         QueryResult<ChangeData> results = queryProcessor.query(queryBuilder.parse(queryString));
         pluginInfosByChange = queryProcessor.createPluginDefinedInfos(results.entities());
         try {
+          AccountAttributeLoader accountLoader = accountAttributeLoaderFactory.create();
+          List<ChangeAttribute> changeAttributes = new ArrayList<>();
           for (ChangeData d : results.entities()) {
-            show(buildChangeAttribute(d, repos, revWalks));
+            changeAttributes.add(buildChangeAttribute(d, repos, revWalks, accountLoader));
           }
+          accountLoader.fill();
+          changeAttributes.forEach(c -> show(c));
         } finally {
           closeAll(revWalks.values(), repos.values());
         }
@@ -247,10 +257,14 @@
   }
 
   private ChangeAttribute buildChangeAttribute(
-      ChangeData d, Map<Project.NameKey, Repository> repos, Map<Project.NameKey, RevWalk> revWalks)
+      ChangeData d,
+      Map<Project.NameKey, Repository> repos,
+      Map<Project.NameKey, RevWalk> revWalks,
+      AccountAttributeLoader accountLoader)
       throws IOException {
     LabelTypes labelTypes = d.getLabelTypes();
-    ChangeAttribute c = eventFactory.asChangeAttribute(d.change(), d.notes());
+    ChangeAttribute c = eventFactory.asChangeAttribute(d.change(), accountLoader);
+    c.hashtags = Lists.newArrayList(d.hashtags());
     eventFactory.extend(c, d.change());
 
     if (!trackingFooters.isEmpty()) {
@@ -258,13 +272,14 @@
     }
 
     if (includeAllReviewers) {
-      eventFactory.addAllReviewers(c, d.notes());
+      eventFactory.addAllReviewers(c, d.notes(), accountLoader);
     }
 
     if (includeSubmitRecords) {
       SubmitRuleOptions options =
           SubmitRuleOptions.builder().recomputeOnClosedChanges(true).build();
-      eventFactory.addSubmitRecords(c, submitRuleEvaluatorFactory.create(options).evaluate(d));
+      eventFactory.addSubmitRecords(
+          c, submitRuleEvaluatorFactory.create(options).evaluate(d), accountLoader);
     }
 
     if (includeCommitMessage) {
@@ -292,26 +307,28 @@
           includeApprovals ? d.approvals().asMap() : null,
           includeFiles,
           d.change(),
-          labelTypes);
+          labelTypes,
+          accountLoader);
     }
 
     if (includeCurrentPatchSet) {
       PatchSet current = d.currentPatchSet();
       if (current != null) {
         c.currentPatchSet = eventFactory.asPatchSetAttribute(rw, d.change(), current);
-        eventFactory.addApprovals(c.currentPatchSet, d.currentApprovals(), labelTypes);
+        eventFactory.addApprovals(
+            c.currentPatchSet, d.currentApprovals(), labelTypes, accountLoader);
 
         if (includeFiles) {
           eventFactory.addPatchSetFileNames(c.currentPatchSet, d.change(), d.currentPatchSet());
         }
         if (includeComments) {
-          eventFactory.addPatchSetComments(c.currentPatchSet, d.publishedComments());
+          eventFactory.addPatchSetComments(c.currentPatchSet, d.publishedComments(), accountLoader);
         }
       }
     }
 
     if (includeComments) {
-      eventFactory.addComments(c, d.messages());
+      eventFactory.addComments(c, d.messages(), accountLoader);
       if (includePatchSets) {
         eventFactory.addPatchSets(
             rw,
@@ -320,9 +337,10 @@
             includeApprovals ? d.approvals().asMap() : null,
             includeFiles,
             d.change(),
-            labelTypes);
+            labelTypes,
+            accountLoader);
         for (PatchSetAttribute attribute : c.patchSets) {
-          eventFactory.addPatchSetComments(attribute, d.publishedComments());
+          eventFactory.addPatchSetComments(attribute, d.publishedComments(), accountLoader);
         }
       }
     }
diff --git a/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java b/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
index 9e56807..e5fb036 100644
--- a/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gerrit.server.index.group.GroupIndexRewriter;
 import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -42,6 +43,7 @@
 public class GroupQueryProcessor extends QueryProcessor<InternalGroup> {
   private final Provider<CurrentUser> userProvider;
   private final GroupControl.GenericFactory groupControlFactory;
+  private final Sequences sequences;
 
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
@@ -58,7 +60,8 @@
       IndexConfig indexConfig,
       GroupIndexCollection indexes,
       GroupIndexRewriter rewriter,
-      GroupControl.GenericFactory groupControlFactory) {
+      GroupControl.GenericFactory groupControlFactory,
+      Sequences sequences) {
     super(
         metricMaker,
         GroupSchemaDefinitions.INSTANCE,
@@ -69,6 +72,7 @@
         () -> limitsFactory.create(userProvider.get()).getQueryLimit());
     this.userProvider = userProvider;
     this.groupControlFactory = groupControlFactory;
+    this.sequences = sequences;
   }
 
   @Override
@@ -81,4 +85,14 @@
   protected String formatForLogging(InternalGroup internalGroup) {
     return internalGroup.getGroupUUID().get();
   }
+
+  @Override
+  protected int getIndexSize() {
+    return sequences.lastGroupId();
+  }
+
+  @Override
+  protected int getBatchSize() {
+    return sequences.groupBatchSize();
+  }
 }
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java b/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
index 8e6d8a1..5465d6d 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountLimits;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -44,6 +45,7 @@
 public class ProjectQueryProcessor extends QueryProcessor<ProjectData> {
   private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> userProvider;
+  private final ProjectCache projectCache;
 
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
@@ -60,7 +62,8 @@
       IndexConfig indexConfig,
       ProjectIndexCollection indexes,
       ProjectIndexRewriter rewriter,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ProjectCache projectCache) {
     super(
         metricMaker,
         ProjectSchemaDefinitions.INSTANCE,
@@ -71,6 +74,7 @@
         () -> limitsFactory.create(userProvider.get()).getQueryLimit());
     this.permissionBackend = permissionBackend;
     this.userProvider = userProvider;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -83,4 +87,14 @@
   protected String formatForLogging(ProjectData projectData) {
     return projectData.getProject().getName();
   }
+
+  @Override
+  protected int getIndexSize() {
+    return projectCache.all().size();
+  }
+
+  @Override
+  protected int getBatchSize() {
+    return 1;
+  }
 }
diff --git a/java/com/google/gerrit/sshd/CommandModule.java b/java/com/google/gerrit/sshd/CommandModule.java
index ac07056..4242c71 100644
--- a/java/com/google/gerrit/sshd/CommandModule.java
+++ b/java/com/google/gerrit/sshd/CommandModule.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.account.AccountAttributeLoader;
 import com.google.inject.binder.LinkedBindingBuilder;
 import org.apache.sshd.server.command.Command;
 
@@ -29,6 +30,7 @@
    * @return a binding that must be bound to a non-singleton provider for a {@link Command} object.
    */
   protected LinkedBindingBuilder<Command> command(String name) {
+    factory(AccountAttributeLoader.Factory.class);
     return bind(Commands.key(name));
   }
 
diff --git a/java/com/google/gerrit/sshd/SshScope.java b/java/com/google/gerrit/sshd/SshScope.java
index 0fe8b78..f19c395 100644
--- a/java/com/google/gerrit/sshd/SshScope.java
+++ b/java/com/google/gerrit/sshd/SshScope.java
@@ -53,6 +53,8 @@
     private volatile long startedMemory;
     private volatile long finishedMemory;
 
+    private IdentifiedUser identifiedUser;
+
     private Context(SshSession s, String c, long at) {
       session = s;
       commandLine = c;
@@ -125,8 +127,10 @@
     public CurrentUser getUser() {
       CurrentUser user = session.getUser();
       if (user != null && user.isIdentifiedUser()) {
-        IdentifiedUser identifiedUser = userFactory.create(user.getAccountId());
-        identifiedUser.setAccessPath(user.getAccessPath());
+        if (identifiedUser == null) {
+          identifiedUser = userFactory.create(user.getAccountId());
+          identifiedUser.setAccessPath(user.getAccessPath());
+        }
         return identifiedUser;
       }
       return user;
diff --git a/javatests/com/google/gerrit/integration/git/BUILD b/javatests/com/google/gerrit/integration/git/BUILD
index 28755af..86b3f36 100644
--- a/javatests/com/google/gerrit/integration/git/BUILD
+++ b/javatests/com/google/gerrit/integration/git/BUILD
@@ -1,13 +1,32 @@
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
+# TODO(davido): This was only needed as own rule, to provide a dedicated
+# tag to skip Git version v2 protocol tests. That was particularly
+# needed for RBE, because this test assumes that git client version is
+# at least 2.17.1. Once Bazel docker image for Ubuntu 20.04 is available
+# and we removed our own RBE docker image, we can merge this rule with
+# the other rules in this package.
 acceptance_tests(
     srcs = ["GitProtocolV2IT.java"],
     group = "protocol-v2",
     labels = ["git-protocol-v2"],
 )
 
+# This rule can be also merged with the other tests in this package.
 acceptance_tests(
     srcs = ["UploadArchiveIT.java"],
     group = "upload-archive",
     labels = ["git-upload-archive"],
 )
+
+acceptance_tests(
+    srcs = glob(
+        ["*.java"],
+        exclude = [
+            "GitProtocolV2IT.java",
+            "UploadArchiveIT.java",
+        ],
+    ),
+    group = "git_tests",
+    labels = ["git"],
+)
diff --git a/javatests/com/google/gerrit/integration/git/PushToRefsUsersIT.java b/javatests/com/google/gerrit/integration/git/PushToRefsUsersIT.java
new file mode 100644
index 0000000..c42f00d
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/git/PushToRefsUsersIT.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2022 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.integration.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+// TODO(davido): In addition to push over HTTP also add a test for push over SSH
+public class PushToRefsUsersIT extends StandaloneSiteTest {
+  private static final String ADMIN_PASSWORD = "secret";
+  private final String[] GIT_CLONE = new String[] {"git", "clone"};
+  private final String[] GIT_FETCH_USERS_SELF =
+      new String[] {"git", "fetch", "origin", "refs/users/self"};
+  private final String[] GIT_CO_FETCH_HEAD = new String[] {"git", "checkout", "FETCH_HEAD"};
+  private final String[] GIT_CONFIG_USER_EMAIL =
+      new String[] {"git", "config", "user.email", "admin@example.com"};
+  private final String[] GIT_CONFIG_USER_NAME =
+      new String[] {"git", "config", "user.name", "Administrator"};
+  private final String[] GIT_COMMIT = new String[] {"git", "commit", "-am", "OOO"};
+  private final String[] GIT_PUSH_USERS_SELF =
+      new String[] {"git", "push", "origin", "HEAD:refs/users/self"};
+
+  @Inject private GerritApi gApi;
+  @Inject private @GerritServerConfig Config config;
+  @Inject private AllUsersName allUsersName;
+
+  @Test
+  public void testPushToRefsUsersOverHttp() throws Exception {
+    try (ServerContext ctx = startServer()) {
+      ctx.getInjector().injectMembers(this);
+
+      // Setup admin password
+      gApi.accounts().id(admin.username()).setHttpPassword(ADMIN_PASSWORD);
+
+      // Get authenticated Git/HTTP URL
+      String urlWithCredentials =
+          config
+              .getString("gerrit", null, "canonicalweburl")
+              .replace("http://", "http://" + admin.username() + ":" + ADMIN_PASSWORD + "@");
+
+      // Clone All-Users repository
+      execute(
+          ImmutableList.<String>builder()
+              .add(GIT_CLONE)
+              .add(urlWithCredentials + "/a/" + allUsersName)
+              .add(sitePaths.data_dir.toFile().getAbsolutePath())
+              .build(),
+          sitePaths.site_path);
+
+      // Fetch refs/users/self for admin user
+      execute(GIT_FETCH_USERS_SELF);
+
+      // Checkout FETCH_HEAD
+      execute(GIT_CO_FETCH_HEAD);
+
+      // Set admin user status to OOO
+      Files.write(
+          sitePaths.data_dir.resolve("account.config"),
+          "  status = OOO".getBytes(UTF_8),
+          StandardOpenOption.APPEND);
+
+      // Set user email
+      execute(GIT_CONFIG_USER_EMAIL);
+
+      // Set user name
+      execute(GIT_CONFIG_USER_NAME);
+
+      // Commit
+      execute(GIT_COMMIT);
+
+      // Push
+      assertThat(execute(GIT_PUSH_USERS_SELF)).contains("Processing changes: refs: 1, done");
+
+      // Verify user status
+      assertThat(gApi.accounts().id(admin.id().get()).detail().status).isEqualTo("OOO");
+    }
+  }
+
+  private String execute(String... cmds) throws Exception {
+    return execute(ImmutableList.<String>builder().add(cmds).build(), sitePaths.data_dir);
+  }
+
+  private String execute(ImmutableList<String> cmd, Path path) throws Exception {
+    return execute(cmd, path.toFile(), ImmutableMap.of());
+  }
+}
diff --git a/javatests/com/google/gerrit/server/git/delegate/DelegateRepositoryTest.java b/javatests/com/google/gerrit/server/git/delegate/DelegateRepositoryTest.java
new file mode 100644
index 0000000..74e0fac
--- /dev/null
+++ b/javatests/com/google/gerrit/server/git/delegate/DelegateRepositoryTest.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.delegate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.git.DelegateRepository;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
+import java.io.IOException;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+
+public class DelegateRepositoryTest {
+
+  @Test
+  public void shouldDelegateRepositoryFromAnyPackage() throws IOException {
+    Repository foo = new InMemoryRepositoryManager().createRepository(Project.nameKey("foo"));
+    try (TestDelegateRepository delegateRepository = new TestDelegateRepository(foo)) {
+      assertThat(delegateRepository.delegate()).isSameInstanceAs(foo);
+    }
+  }
+
+  static class TestDelegateRepository extends DelegateRepository {
+    protected TestDelegateRepository(Repository delegate) {
+      super(delegate);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
index a768eaf..0c9f731 100644
--- a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -91,27 +91,37 @@
     assertThat(s.acquireCount).isEqualTo(0);
 
     assertThat(s.next()).isEqualTo(1);
+    assertThat(s.last()).isEqualTo(1);
     assertThat(s.acquireCount).isEqualTo(1);
     assertThat(s.next()).isEqualTo(2);
+    assertThat(s.last()).isEqualTo(2);
     assertThat(s.acquireCount).isEqualTo(1);
     assertThat(s.next()).isEqualTo(3);
+    assertThat(s.last()).isEqualTo(3);
     assertThat(s.acquireCount).isEqualTo(1);
 
     assertThat(s.next()).isEqualTo(4);
+    assertThat(s.last()).isEqualTo(4);
     assertThat(s.acquireCount).isEqualTo(2);
     assertThat(s.next()).isEqualTo(5);
+    assertThat(s.last()).isEqualTo(5);
     assertThat(s.acquireCount).isEqualTo(2);
     assertThat(s.next()).isEqualTo(6);
+    assertThat(s.last()).isEqualTo(6);
     assertThat(s.acquireCount).isEqualTo(2);
 
     assertThat(s.next()).isEqualTo(7);
+    assertThat(s.last()).isEqualTo(7);
     assertThat(s.acquireCount).isEqualTo(3);
     assertThat(s.next()).isEqualTo(8);
+    assertThat(s.last()).isEqualTo(8);
     assertThat(s.acquireCount).isEqualTo(3);
     assertThat(s.next()).isEqualTo(9);
+    assertThat(s.last()).isEqualTo(9);
     assertThat(s.acquireCount).isEqualTo(3);
 
     assertThat(s.next()).isEqualTo(10);
+    assertThat(s.last()).isEqualTo(10);
     assertThat(s.acquireCount).isEqualTo(4);
   }
 
@@ -127,6 +137,8 @@
     assertThat(s2.next()).isEqualTo(5);
     assertThat(s1.next()).isEqualTo(3);
     assertThat(s2.next()).isEqualTo(6);
+    assertThat(s1.last()).isEqualTo(3);
+    assertThat(s2.last()).isEqualTo(6);
 
     // s2 acquires 7-9; s1 acquires 10-12.
     assertThat(s2.next()).isEqualTo(7);
@@ -135,6 +147,8 @@
     assertThat(s1.next()).isEqualTo(11);
     assertThat(s2.next()).isEqualTo(9);
     assertThat(s1.next()).isEqualTo(12);
+    assertThat(s1.last()).isEqualTo(12);
+    assertThat(s2.last()).isEqualTo(9);
   }
 
   @Test
@@ -284,48 +298,61 @@
   }
 
   @Test
-  public void nextWithCountOneCaller() throws Exception {
+  public void nextWithCountAndLastByOneCaller() throws Exception {
     RepoSequence s = newSequence("id", 1, 3);
     assertThat(s.next(2)).containsExactly(1, 2).inOrder();
     assertThat(s.acquireCount).isEqualTo(1);
+    assertThat(s.last()).isEqualTo(2);
     assertThat(s.next(2)).containsExactly(3, 4).inOrder();
     assertThat(s.acquireCount).isEqualTo(2);
+    assertThat(s.last()).isEqualTo(4);
     assertThat(s.next(2)).containsExactly(5, 6).inOrder();
     assertThat(s.acquireCount).isEqualTo(2);
+    assertThat(s.last()).isEqualTo(6);
 
     assertThat(s.next(3)).containsExactly(7, 8, 9).inOrder();
     assertThat(s.acquireCount).isEqualTo(3);
+    assertThat(s.last()).isEqualTo(9);
     assertThat(s.next(3)).containsExactly(10, 11, 12).inOrder();
     assertThat(s.acquireCount).isEqualTo(4);
+    assertThat(s.last()).isEqualTo(12);
     assertThat(s.next(3)).containsExactly(13, 14, 15).inOrder();
     assertThat(s.acquireCount).isEqualTo(5);
+    assertThat(s.last()).isEqualTo(15);
 
     assertThat(s.next(7)).containsExactly(16, 17, 18, 19, 20, 21, 22).inOrder();
     assertThat(s.acquireCount).isEqualTo(6);
+    assertThat(s.last()).isEqualTo(22);
     assertThat(s.next(7)).containsExactly(23, 24, 25, 26, 27, 28, 29).inOrder();
     assertThat(s.acquireCount).isEqualTo(7);
+    assertThat(s.last()).isEqualTo(29);
     assertThat(s.next(7)).containsExactly(30, 31, 32, 33, 34, 35, 36).inOrder();
     assertThat(s.acquireCount).isEqualTo(8);
+    assertThat(s.last()).isEqualTo(36);
   }
 
   @Test
-  public void nextWithCountMultipleCallers() throws Exception {
+  public void nextWithCountAndLastByMultipleCallers() throws Exception {
     RepoSequence s1 = newSequence("id", 1, 3);
     RepoSequence s2 = newSequence("id", 1, 4);
 
     assertThat(s1.next(2)).containsExactly(1, 2).inOrder();
+    assertThat(s1.last()).isEqualTo(2);
     assertThat(s1.acquireCount).isEqualTo(1);
 
     // s1 hasn't exhausted its last batch.
     assertThat(s2.next(2)).containsExactly(4, 5).inOrder();
+    assertThat(s2.last()).isEqualTo(5);
     assertThat(s2.acquireCount).isEqualTo(1);
 
     // s1 acquires again to cover this request, plus a whole new batch.
     assertThat(s1.next(3)).containsExactly(3, 8, 9);
+    assertThat(s1.last()).isEqualTo(9);
     assertThat(s1.acquireCount).isEqualTo(2);
 
     // s2 hasn't exhausted its last batch, do so now.
     assertThat(s2.next(2)).containsExactly(6, 7);
+    assertThat(s2.last()).isEqualTo(7);
     assertThat(s2.acquireCount).isEqualTo(1);
   }
 
diff --git a/polygerrit-ui/app/api/BUILD_for_publishing_api_only b/polygerrit-ui/app/api/BUILD_for_publishing_api_only
index 9d3029b..67a26cd 100644
--- a/polygerrit-ui/app/api/BUILD_for_publishing_api_only
+++ b/polygerrit-ui/app/api/BUILD_for_publishing_api_only
@@ -38,6 +38,7 @@
 # Use this rule for publishing the js plugin api as a package to the npm repo.
 pkg_npm(
     name = "js_plugin_api_npm_package",
+    package_name = "@gerritcodereview/typescript-api",
     srcs = glob(
         ["**/*"],
         exclude = [
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 e9b7e2f..10fd50e 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
@@ -836,6 +836,7 @@
 
     if (changedProperties.has('change')) {
       this.reload();
+      this.actions = this.change?.actions ?? {};
     }
 
     this.editStatusChanged();
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 01bc9ef..3207279 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
@@ -135,17 +135,19 @@
       element = await fixture<GrChangeActions>(html`
         <gr-change-actions></gr-change-actions>
       `);
-      element.change = createChangeViewChange();
-      element.changeNum = 42 as NumericChangeId;
-      element.latestPatchNum = 2 as PatchSetNum;
-      element.actions = {
-        '/': {
-          method: HttpMethod.DELETE,
-          label: 'Delete Change',
-          title: 'Delete change X_X',
-          enabled: true,
+      element.change = {
+        ...createChangeViewChange(),
+        actions: {
+          '/': {
+            method: HttpMethod.DELETE,
+            label: 'Delete Change',
+            title: 'Delete change X_X',
+            enabled: true,
+          },
         },
       };
+      element.changeNum = 42 as NumericChangeId;
+      element.latestPatchNum = 2 as PatchSetNum;
       element.account = {
         _account_id: 123 as AccountId,
       };
@@ -1294,13 +1296,16 @@
       setup(async () => {
         fireActionStub = sinon.stub(element, 'fireAction');
         element.commitMessage = 'random commit message';
-        element.change!.current_revision = 'abcdef' as CommitId;
-        element.actions = {
-          revert: {
-            method: HttpMethod.POST,
-            label: 'Revert',
-            title: 'Revert the change',
-            enabled: true,
+        element.change = {
+          ...createChangeViewChange(),
+          current_revision: 'abcdef' as CommitId,
+          actions: {
+            revert: {
+              method: HttpMethod.POST,
+              label: 'Revert',
+              title: 'Revert the change',
+              enabled: true,
+            },
           },
         };
         await element.updateComplete;
@@ -1322,6 +1327,14 @@
         element.change = {
           ...createChangeViewChange(),
           current_revision: 'abc1234' as CommitId,
+          actions: {
+            revert: {
+              method: HttpMethod.POST,
+              label: 'Revert',
+              title: 'Revert the change',
+              enabled: true,
+            },
+          },
         };
         stubRestApi('getChanges').returns(
           Promise.resolve([
@@ -1368,6 +1381,14 @@
             ...createChangeViewChange(),
             submission_id: '199 0' as ChangeSubmissionId,
             current_revision: '2000' as CommitId,
+            actions: {
+              revert: {
+                method: HttpMethod.POST,
+                label: 'Revert',
+                title: 'Revert the change',
+                enabled: true,
+              },
+            },
           };
           getChangesStub = stubRestApi('getChanges').returns(
             Promise.resolve([
@@ -1521,6 +1542,14 @@
             ...createChangeViewChange(),
             submission_id: '199' as ChangeSubmissionId,
             current_revision: '2000' as CommitId,
+            actions: {
+              revert: {
+                method: HttpMethod.POST,
+                label: 'Revert',
+                title: 'Revert the change',
+                enabled: true,
+              },
+            },
           };
           stubRestApi('getChanges').returns(
             Promise.resolve([
@@ -1725,18 +1754,18 @@
 
       setup(async () => {
         fireActionStub = sinon.stub(element, 'fireAction');
-        element.change = {
-          ...createChangeViewChange(),
-          current_revision: 'abc1234' as CommitId,
-        };
         deleteAction = {
           method: HttpMethod.DELETE,
           label: 'Delete Change',
           title: 'Delete change X_X',
           enabled: true,
         };
-        element.actions = {
-          '/': deleteAction,
+        element.change = {
+          ...createChangeViewChange(),
+          current_revision: 'abc1234' as CommitId,
+          actions: {
+            '/': deleteAction,
+          },
         };
         await element.updateComplete;
       });
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index 9af14f5..2c80a86 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -37,6 +37,7 @@
 import {
   ChangeStatus,
   GpgKeyInfoStatus,
+  InheritedBooleanInfoConfiguredValue,
   SubmitType,
 } from '../../../constants/constants';
 import {changeIsOpen, isOwner} from '../../../utils/change-util';
@@ -48,6 +49,7 @@
   ChangeInfo,
   CommitId,
   CommitInfo,
+  ConfigInfo,
   GpgKeyInfo,
   Hashtag,
   isAccount,
@@ -150,6 +152,8 @@
 
   @property({type: Boolean}) parentIsCurrent?: boolean;
 
+  @property({type: Object}) repoConfig?: ConfigInfo;
+
   // private but used in test
   @state() mutable = false;
 
@@ -749,7 +753,8 @@
     }
     if (
       changedProperties.has('serverConfig') ||
-      changedProperties.has('change')
+      changedProperties.has('change') ||
+      changedProperties.has('repoConfig')
     ) {
       this.pushCertificateValidation = this.computePushCertificateValidation();
     }
@@ -887,6 +892,9 @@
     if (!this.change || !this.serverConfig?.receive?.enable_signed_push)
       return undefined;
 
+    if (!this.isEnabledSignedPushOnRepo()) {
+      return undefined;
+    }
     const rev = this.change.revisions[this.change.current_revision];
     if (!rev.push_certificate?.key) {
       return {
@@ -930,6 +938,20 @@
     }
   }
 
+  // private but used in test
+  isEnabledSignedPushOnRepo() {
+    if (!this.repoConfig?.enable_signed_push) return false;
+
+    const enableSignedPush = this.repoConfig.enable_signed_push;
+    return (
+      (enableSignedPush.configured_value ===
+        InheritedBooleanInfoConfiguredValue.INHERIT &&
+        enableSignedPush.inherited_value) ||
+      enableSignedPush.configured_value ===
+        InheritedBooleanInfoConfiguredValue.TRUE
+    );
+  }
+
   private problems(msg: string, key: GpgKeyInfo) {
     if (!key?.problems || key.problems.length === 0) {
       return msg;
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index 2b48697..a000445 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -32,11 +32,13 @@
   createCommit,
   createRevision,
   createAccountDetailWithId,
+  createConfig,
 } from '../../../test/test-data-generators';
 import {
   ChangeStatus,
   SubmitType,
   GpgKeyInfoStatus,
+  InheritedBooleanInfoConfiguredValue,
 } from '../../../constants/constants';
 import {
   EmailAddress,
@@ -494,6 +496,13 @@
         labels: {},
         mergeable: true,
       };
+      element.repoConfig = {
+        ...createConfig(),
+        enable_signed_push: {
+          configured_value: 'TRUE' as InheritedBooleanInfoConfiguredValue,
+          value: true,
+        },
+      };
     });
 
     test('Push Certificate Validation test BAD', () => {
@@ -546,6 +555,38 @@
       assert.equal(result?.icon, 'gr-icons:help');
       assert.equal(result?.class, 'help');
     });
+
+    test('computePushCertificateValidation returns undefined', () => {
+      element.change = change;
+      delete serverConfig!.receive!.enable_signed_push;
+      element.serverConfig = serverConfig;
+      assert.isUndefined(element.computePushCertificateValidation());
+    });
+
+    test('isEnabledSignedPushOnRepo', () => {
+      change!.revisions.rev1!.push_certificate = {
+        certificate: 'Push certificate',
+        key: {
+          status: GpgKeyInfoStatus.TRUSTED,
+        },
+      };
+      element.change = change;
+      element.serverConfig = serverConfig;
+      element.repoConfig!.enable_signed_push!.configured_value =
+        InheritedBooleanInfoConfiguredValue.INHERIT;
+      element.repoConfig!.enable_signed_push!.inherited_value = true;
+      assert.isTrue(element.isEnabledSignedPushOnRepo());
+
+      element.repoConfig!.enable_signed_push!.inherited_value = false;
+      assert.isFalse(element.isEnabledSignedPushOnRepo());
+
+      element.repoConfig!.enable_signed_push!.configured_value =
+        InheritedBooleanInfoConfiguredValue.TRUE;
+      assert.isTrue(element.isEnabledSignedPushOnRepo());
+
+      element.repoConfig = undefined;
+      assert.isFalse(element.isEnabledSignedPushOnRepo());
+    });
   });
 
   test('computeParents', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index b7c4d7b..c8eb6a8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -1303,10 +1303,6 @@
     if (value.basePatchNum === undefined)
       value.basePatchNum = ParentPatchSetNum;
 
-    if (value.patchNum === undefined) {
-      value.patchNum = computeLatestPatchNum(this._allPatchSets);
-    }
-
     const patchChanged = this.hasPatchRangeChanged(value);
     let patchNumChanged = this.hasPatchNumChanged(value);
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index 5d4db56..025a74b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -362,7 +362,6 @@
             change="[[_change]]"
             disable-edit="[[disableEdit]]"
             has-parent="[[hasParent]]"
-            actions="[[_change.actions]]"
             revision-actions="[[_currentRevisionActions]]"
             account="[[_account]]"
             change-num="[[_changeNum]]"
@@ -397,6 +396,7 @@
             commit-info="[[_commitInfo]]"
             server-config="[[_serverConfig]]"
             parent-is-current="[[_parentIsCurrent]]"
+            repo-config="[[_projectConfig]]"
             on-show-reply-dialog="_handleShowReplyDialog"
           >
           </gr-change-metadata>
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 fb8f279..f8103fd 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
@@ -39,7 +39,6 @@
   ChangeStatus,
   ProgressStatus,
 } from '../../../constants/constants';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {fireEvent} from '../../../utils/event-util';
 import {css, html, LitElement, PropertyValues} from 'lit';
 import {sharedStyles} from '../../../styles/shared-styles';
@@ -423,9 +422,7 @@
   }
 
   private toggleChangeSelected(e: Event) {
-    const changeId = ((dom(e) as EventApi).localTarget as HTMLElement).dataset[
-      'item'
-    ]! as ChangeInfoId;
+    const changeId = (e.target as HTMLElement).dataset['item']! as ChangeInfoId;
     if (this.selectedChangeIds.has(changeId))
       this.selectedChangeIds.delete(changeId);
     else this.selectedChangeIds.add(changeId);
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index 0902fe6..f9afd8c 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -25,6 +25,7 @@
 import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {MessageTag, SpecialFilePath} from '../../../constants/constants';
 import {customElement, property, state} from 'lit/decorators';
+import {hasOwnProperty} from '../../../utils/common-util';
 import {
   ChangeInfo,
   ServerInfo,
@@ -36,6 +37,7 @@
   PatchSetNum,
   AccountInfo,
   BasePatchSetNum,
+  LabelNameToInfoMap,
 } from '../../../types/common';
 import {
   ChangeMessage,
@@ -44,6 +46,7 @@
   LabelExtreme,
   PATCH_SET_PREFIX_PATTERN,
 } from '../../../utils/comment-util';
+import {LABEL_TITLE_SCORE_PATTERN} from '../gr-message-scores/gr-message-scores';
 import {getAppContext} from '../../../services/app-context';
 import {pluralize} from '../../../utils/string-util';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
@@ -377,7 +380,8 @@
         false,
         this.message.message.substring(0, 1000),
         this.message.accounts_in_message,
-        this.message.tag
+        this.message.tag,
+        this.change?.labels
       ) || this.patchsetCommentSummary();
     return html` <div class="content messageContent">
       <div class="message hideOnOpen">${messageContentCollapsed}</div>
@@ -391,7 +395,8 @@
       true,
       this.message.message,
       this.message.accounts_in_message,
-      this.message.tag
+      this.message.tag,
+      this.change?.labels
     );
     return html`
       <gr-formatted-text
@@ -594,7 +599,8 @@
     isExpanded: boolean,
     content?: string,
     accountsInMessage?: AccountInfo[],
-    tag?: ReviewInputTag
+    tag?: ReviewInputTag,
+    labels?: LabelNameToInfoMap
   ) {
     if (!content) return '';
     const isNewPatchSet = this.isNewPatchsetTag(tag);
@@ -614,8 +620,24 @@
       if (line.startsWith('(') && line.endsWith(' comments)')) {
         return false;
       }
-      if (!isNewPatchSet && line.match(PATCH_SET_PREFIX_PATTERN)) {
-        return false;
+      if (!isNewPatchSet && labels) {
+        // Legacy change messages may contain the 'Patch Set' prefix
+        // and a message(not containing label scores) on the same line.
+        // To handle them correctly, only filter out lines which contain
+        // the 'Patch Set' prefix and label scores.
+        const match = line.match(PATCH_SET_PREFIX_PATTERN);
+        if (match && match[1]) {
+          const message = match[1].split(' ');
+          if (
+            message
+              .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
+              .filter(
+                ms => ms && ms.length === 4 && hasOwnProperty(labels, ms[2])
+              ).length === message.length
+          ) {
+            return false;
+          }
+        }
       }
       return true;
     });
@@ -630,7 +652,7 @@
       // Only make this replacement if the line starts with Patch Set, since if
       // it starts with "Uploaded patch set" (e.g for votes) we want to keep the
       // "Uploaded patch set".
-      if (isNewPatchSet && line.startsWith('Patch Set')) {
+      if (line.startsWith('Patch Set')) {
         line = line.replace(PATCH_SET_PREFIX_PATTERN, '$1');
       }
       return line;
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
index bbe39ff..6e550ac 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
@@ -24,6 +24,7 @@
   createChangeMessage,
   createComment,
   createRevisions,
+  createLabelInfo,
 } from '../../../test/test-data-generators';
 import {
   mockPromise,
@@ -444,13 +445,18 @@
     });
 
     suite('compute messages', () => {
+      const labels = {
+        'Code-Review': createLabelInfo(1),
+        'Code-Style': createLabelInfo(1),
+      };
       test('empty', () => {
         assert.equal(
           element.computeMessageContent(
             true,
             '',
             undefined,
-            '' as ReviewInputTag
+            '' as ReviewInputTag,
+            labels
           ),
           ''
         );
@@ -459,7 +465,8 @@
             false,
             '',
             undefined,
-            '' as ReviewInputTag
+            '' as ReviewInputTag,
+            labels
           ),
           ''
         );
@@ -469,8 +476,18 @@
         const original = 'Uploaded patch set 1.';
         const tag = 'autogenerated:gerrit:newPatchSet' as ReviewInputTag;
         let actual = element.computeMessageContent(true, original, [], tag);
+        assert.equal(
+          actual,
+          element.computeMessageContent(true, original, [], tag, labels)
+        );
         assert.equal(actual, original);
-        actual = element.computeMessageContent(false, original, [], tag);
+        actual = element.computeMessageContent(
+          false,
+          original,
+          [],
+          tag,
+          labels
+        );
         assert.equal(actual, original);
       });
 
@@ -478,9 +495,25 @@
         const original = 'Patch Set 27: Patch Set 26 was rebased';
         const tag = 'autogenerated:gerrit:newPatchSet' as ReviewInputTag;
         const expected = 'Patch Set 26 was rebased';
-        let actual = element.computeMessageContent(true, original, [], tag);
+        let actual = element.computeMessageContent(
+          true,
+          original,
+          [],
+          tag,
+          labels
+        );
         assert.equal(actual, expected);
-        actual = element.computeMessageContent(false, original, [], tag);
+        assert.equal(
+          actual,
+          element.computeMessageContent(true, original, [], tag)
+        );
+        actual = element.computeMessageContent(
+          false,
+          original,
+          [],
+          tag,
+          labels
+        );
         assert.equal(actual, expected);
       });
 
@@ -488,27 +521,89 @@
         const original = 'Patch Set 1:\n\nThis change is ready for review.';
         const tag = undefined;
         const expected = 'This change is ready for review.';
-        let actual = element.computeMessageContent(true, original, [], tag);
+        let actual = element.computeMessageContent(
+          true,
+          original,
+          [],
+          tag,
+          labels
+        );
         assert.equal(actual, expected);
-        actual = element.computeMessageContent(false, original, [], tag);
+        assert.equal(
+          actual,
+          element.computeMessageContent(true, original, [], tag)
+        );
+        actual = element.computeMessageContent(
+          false,
+          original,
+          [],
+          tag,
+          labels
+        );
         assert.equal(actual, expected);
       });
       test('new patchset with vote', () => {
         const original = 'Uploaded patch set 2: Code-Review+1';
         const tag = 'autogenerated:gerrit:newPatchSet' as ReviewInputTag;
         const expected = 'Uploaded patch set 2: Code-Review+1';
-        let actual = element.computeMessageContent(true, original, [], tag);
+        let actual = element.computeMessageContent(
+          true,
+          original,
+          [],
+          tag,
+          labels
+        );
         assert.equal(actual, expected);
-        actual = element.computeMessageContent(false, original, [], tag);
+        actual = element.computeMessageContent(
+          false,
+          original,
+          [],
+          tag,
+          labels
+        );
         assert.equal(actual, expected);
       });
       test('vote', () => {
         const original = 'Patch Set 1: Code-Style+1';
         const tag = undefined;
         const expected = '';
-        let actual = element.computeMessageContent(true, original, [], tag);
+        let actual = element.computeMessageContent(
+          true,
+          original,
+          [],
+          tag,
+          labels
+        );
         assert.equal(actual, expected);
-        actual = element.computeMessageContent(false, original, [], tag);
+        actual = element.computeMessageContent(
+          false,
+          original,
+          [],
+          tag,
+          labels
+        );
+        assert.equal(actual, expected);
+      });
+
+      test('legacy change message', () => {
+        const original = 'Patch Set 1: Legacy Message';
+        const tag = undefined;
+        const expected = 'Legacy Message';
+        let actual = element.computeMessageContent(
+          true,
+          original,
+          [],
+          tag,
+          labels
+        );
+        assert.equal(actual, expected);
+        actual = element.computeMessageContent(
+          false,
+          original,
+          [],
+          tag,
+          labels
+        );
         assert.equal(actual, expected);
       });
 
@@ -516,9 +611,21 @@
         const original = 'Patch Set 1:\n\n(3 comments)';
         const tag = undefined;
         const expected = '';
-        let actual = element.computeMessageContent(true, original, [], tag);
+        let actual = element.computeMessageContent(
+          true,
+          original,
+          [],
+          tag,
+          labels
+        );
         assert.equal(actual, expected);
-        actual = element.computeMessageContent(false, original, [], tag);
+        actual = element.computeMessageContent(
+          false,
+          original,
+          [],
+          tag,
+          labels
+        );
         assert.equal(actual, expected);
       });
 
@@ -536,14 +643,16 @@
           true,
           original,
           accountsInMessage,
-          tag
+          tag,
+          labels
         );
         assert.equal(actual, expected);
         actual = element.computeMessageContent(
           false,
           original,
           accountsInMessage,
-          tag
+          tag,
+          labels
         );
         assert.equal(actual, expected);
       });
@@ -554,9 +663,21 @@
         const tag = undefined;
         const expected =
           'Removed vote: \n\n * Code-Style+1 by Gerrit Account 1\n * Code-Style-1 by Gerrit Account 2';
-        let actual = element.computeMessageContent(true, original, [], tag);
+        let actual = element.computeMessageContent(
+          true,
+          original,
+          [],
+          tag,
+          labels
+        );
         assert.equal(actual, expected);
-        actual = element.computeMessageContent(false, original, [], tag);
+        actual = element.computeMessageContent(
+          false,
+          original,
+          [],
+          tag,
+          labels
+        );
         assert.equal(actual, expected);
       });
     });
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
index daed7d2..ae20c4e 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
@@ -18,7 +18,6 @@
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
 import '../../shared/gr-overlay/gr-overlay';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {SshKeyInfo} from '../../../types/common';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
@@ -240,14 +239,14 @@
   }
 
   private showKey(e: Event) {
-    const el = (dom(e) as EventApi).localTarget as GrButton;
+    const el = e.target as GrButton;
     const index = Number(el.getAttribute('data-index')!);
     this.keyToView = this.keys[index];
     this.viewKeyOverlay.open();
   }
 
   private handleDeleteKey(e: Event) {
-    const el = (dom(e) as EventApi).localTarget as GrButton;
+    const el = e.target as GrButton;
     const index = Number(el.getAttribute('data-index')!);
     this.keysToRemove.push(this.keys[index]);
     this.keys.splice(index, 1);
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
index b1d3914..13375b3 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
@@ -24,7 +24,6 @@
 import '../gr-icons/gr-icons';
 import '../gr-label/gr-label';
 import '../gr-tooltip-content/gr-tooltip-content';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {
   AccountInfo,
   LabelInfo,
@@ -448,7 +447,7 @@
     if (!this.change) return;
 
     e.preventDefault();
-    let target = (dom(e) as EventApi).rootTarget as GrButton;
+    let target = e.composedPath()[0] as GrButton;
     while (!target.classList.contains('deleteBtn')) {
       if (!target.parentElement) {
         return;
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 8e6a147..2729e69 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
@@ -97,6 +97,7 @@
   HashtagsInput,
   ImagesForDiff,
   IncludedInInfo,
+  LabelNameToLabelTypeInfoMap,
   MergeableInfo,
   NameToProjectInfoMap,
   NumericChangeId,
@@ -130,11 +131,13 @@
   TagInput,
   TopMenuEntryInfo,
   UrlEncodedCommentId,
+  UrlEncodedRepoName,
 } from '../../types/common';
 import {
   DiffInfo,
   DiffPreferencesInfo,
   IgnoreWhitespaceType,
+  WebLinkInfo,
 } from '../../types/diff';
 import {
   CancelConditionCallback,
@@ -148,6 +151,7 @@
   createDefaultEditPrefs,
   createDefaultPreferences,
   HttpMethod,
+  ProjectState,
   ReviewerState,
 } from '../../constants/constants';
 import {firePageError, fireServerError} from '../../utils/event-util';
@@ -1429,36 +1433,26 @@
     filter: string | undefined,
     reposPerPage: number,
     offset?: number
-  ) {
-    const defaultFilter = 'state:active OR state:read-only';
-    const namePartDelimiters = /[@.\-\s/_]/g;
+  ): [boolean, string] {
+    const defaultFilter = '';
     offset = offset || 0;
-
-    if (filter && !filter.includes(':') && filter.match(namePartDelimiters)) {
-      // The query language specifies hyphens as operators. Split the string
-      // by hyphens and 'AND' the parts together as 'inname:' queries.
-      // If the filter includes a semicolon, the user is using a more complex
-      // query so we trust them and don't do any magic under the hood.
-      const originalFilter = filter;
-      filter = '';
-      originalFilter.split(namePartDelimiters).forEach(part => {
-        if (part) {
-          filter += (filter === '' ? 'inname:' : ' AND inname:') + part;
-        }
-      });
-    }
-    // Check if filter is now empty which could be either because the user did
-    // not provide it or because the user provided only a split character.
-    if (!filter) {
-      filter = defaultFilter;
-    }
-
-    filter = filter.trim();
+    filter ??= defaultFilter;
     const encodedFilter = encodeURIComponent(filter);
 
-    return (
-      `/projects/?n=${reposPerPage + 1}&S=${offset}` + `&query=${encodedFilter}`
-    );
+    if (filter.includes(':')) {
+      // If the filter includes a semicolon, the user is using a more complex
+      // query so we trust them and don't do any magic under the hood.
+      return [
+        true,
+        `/projects/?n=${reposPerPage + 1}&S=${offset}` +
+          `&query=${encodedFilter}`,
+      ];
+    }
+
+    return [
+      false,
+      `/projects/?n=${reposPerPage + 1}&S=${offset}` + `&d=&m=${encodedFilter}`,
+    ];
   }
 
   invalidateGroupsCache() {
@@ -1491,14 +1485,47 @@
     reposPerPage: number,
     offset?: number
   ): Promise<ProjectInfoWithName[] | undefined> {
-    const url = this._getReposUrl(filter, reposPerPage, offset);
+    const [isQuery, url] = this._getReposUrl(filter, reposPerPage, offset);
 
     // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
     // supports it.
-    return this._fetchSharedCacheURL({
-      url, // The url contains query,so the response is an array, not map
-      anonymizedUrl: '/projects/?*',
-    }) as Promise<ProjectInfoWithName[] | undefined>;
+    // If query then return directly as the result will be expected to be an array
+    if (isQuery) {
+      return this._fetchSharedCacheURL({
+        url, // The url contains query,so the response is an array, not map
+        anonymizedUrl: '/projects/?*',
+      }) as Promise<ProjectInfoWithName[] | undefined>;
+    }
+    const result: Promise<NameToProjectInfoMap[] | undefined> =
+      this._fetchSharedCacheURL({
+        url, // The url contains query,so the response is an array, not map
+        anonymizedUrl: '/projects/?*',
+      }) as Promise<NameToProjectInfoMap[] | undefined>;
+    return this._transformToArray(result);
+  }
+
+  _transformToArray(
+    res: Promise<NameToProjectInfoMap[] | undefined>
+  ): Promise<ProjectInfoWithName[] | undefined> {
+    return res.then(response => {
+      const reposList: ProjectInfoWithName[] = [];
+      for (const [name, project] of Object.entries(response ?? {})) {
+        const projectInfo: ProjectInfoWithName = {
+          id: project.id as unknown as UrlEncodedRepoName,
+          name: name as RepoName,
+          parent: project.parent as unknown as RepoName,
+          description: project.description as unknown as string,
+          state: project.state as unknown as ProjectState,
+          branches: project.branches as unknown as {
+            [branchName: string]: CommitId;
+          },
+          labels: project.labels as unknown as LabelNameToLabelTypeInfoMap,
+          web_links: project.web_links as unknown as WebLinkInfo[],
+        };
+        reposList.push(projectInfo);
+      }
+      return reposList;
+    });
   }
 
   setRepoHead(repo: RepoName, ref: GitRef) {
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.js b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.js
index fbfd152..d8c3ff2 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.js
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.js
@@ -677,16 +677,19 @@
   });
 
   test('normal use', () => {
-    const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
+    const defaultQuery = '';
 
-    assert.equal(element._getReposUrl('test', 25),
-        '/projects/?n=26&S=0&query=test');
+    assert.equal(element._getReposUrl('test', 25).toString(),
+        [false, '/projects/?n=26&S=0&d=&m=test'].toString());
 
-    assert.equal(element._getReposUrl(null, 25),
-        `/projects/?n=26&S=0&query=${defaultQuery}`);
+    assert.equal(element._getReposUrl(null, 25).toString(),
+        [false, `/projects/?n=26&S=0&d=&m=${defaultQuery}`].toString());
 
-    assert.equal(element._getReposUrl('test', 25, 25),
-        '/projects/?n=26&S=25&query=test');
+    assert.equal(element._getReposUrl('test', 25, 25).toString(),
+        [false, '/projects/?n=26&S=25&d=&m=test'].toString());
+
+    assert.equal(element._getReposUrl('inname:test', 25, 25).toString(),
+        [true, '/projects/?n=26&S=25&query=inname%3Atest'].toString());
   });
 
   test('invalidateReposCache', () => {
@@ -714,67 +717,74 @@
   });
 
   suite('getRepos', () => {
-    const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
+    const defaultQuery = '';
     let fetchCacheURLStub;
     setup(() => {
       fetchCacheURLStub =
-          sinon.stub(element._restApiHelper, 'fetchCacheURL');
+          sinon.stub(element._restApiHelper, 'fetchCacheURL')
+              .returns(Promise.resolve([]));
     });
 
     test('normal use', () => {
       element.getRepos('test', 25);
       assert.equal(fetchCacheURLStub.lastCall.args[0].url,
-          '/projects/?n=26&S=0&query=test');
+          '/projects/?n=26&S=0&d=&m=test');
 
       element.getRepos(null, 25);
       assert.equal(fetchCacheURLStub.lastCall.args[0].url,
-          `/projects/?n=26&S=0&query=${defaultQuery}`);
+          `/projects/?n=26&S=0&d=&m=${defaultQuery}`);
 
       element.getRepos('test', 25, 25);
       assert.equal(fetchCacheURLStub.lastCall.args[0].url,
-          '/projects/?n=26&S=25&query=test');
+          '/projects/?n=26&S=25&d=&m=test');
     });
 
     test('with blank', () => {
       element.getRepos('test/test', 25);
       assert.equal(fetchCacheURLStub.lastCall.args[0].url,
-          '/projects/?n=26&S=0&query=inname%3Atest%20AND%20inname%3Atest');
+          '/projects/?n=26&S=0&d=&m=test%2Ftest');
     });
 
     test('with hyphen', () => {
       element.getRepos('foo-bar', 25);
       assert.equal(fetchCacheURLStub.lastCall.args[0].url,
-          '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+          '/projects/?n=26&S=0&d=&m=foo-bar');
     });
 
     test('with leading hyphen', () => {
       element.getRepos('-bar', 25);
       assert.equal(fetchCacheURLStub.lastCall.args[0].url,
-          '/projects/?n=26&S=0&query=inname%3Abar');
+          '/projects/?n=26&S=0&d=&m=-bar');
     });
 
     test('with trailing hyphen', () => {
       element.getRepos('foo-bar-', 25);
       assert.equal(fetchCacheURLStub.lastCall.args[0].url,
-          '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+          '/projects/?n=26&S=0&d=&m=foo-bar-');
     });
 
     test('with underscore', () => {
       element.getRepos('foo_bar', 25);
       assert.equal(fetchCacheURLStub.lastCall.args[0].url,
-          '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+          '/projects/?n=26&S=0&d=&m=foo_bar');
     });
 
     test('with underscore', () => {
       element.getRepos('foo_bar', 25);
       assert.equal(fetchCacheURLStub.lastCall.args[0].url,
-          '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+          '/projects/?n=26&S=0&d=&m=foo_bar');
     });
 
     test('hyphen only', () => {
       element.getRepos('-', 25);
       assert.equal(fetchCacheURLStub.lastCall.args[0].url,
-          `/projects/?n=26&S=0&query=${defaultQuery}`);
+          `/projects/?n=26&S=0&d=&m=-`);
+    });
+
+    test('using query', () =>{
+      element.getRepos('description:project', 25);
+      assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+          `/projects/?n=26&S=0&query=description%3Aproject`);
     });
   });
 
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 2fffc9a..8c33c79 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -51,6 +51,7 @@
   GroupId,
   GroupInfo,
   InheritedBooleanInfo,
+  LabelInfo,
   MaxObjectSizeLimitInfo,
   MergeableInfo,
   NumericChangeId,
@@ -239,6 +240,30 @@
   };
 }
 
+export function createLabelInfo(score = 1): LabelInfo {
+  return {
+    all: [
+      {
+        value: score,
+        permitted_voting_range: {
+          min: -1,
+          max: 1,
+        },
+        _account_id: 1000 as AccountId,
+        name: 'Foo',
+        email: 'foo@example.com' as EmailAddress,
+        username: 'foo',
+      },
+    ],
+    values: {
+      '-1': 'Fail',
+      ' 0': 'No score',
+      '+1': 'Pass',
+    },
+    default_value: 0,
+  };
+}
+
 export function createCommit(): CommitInfo {
   return {
     parents: [],