Merge "Upgrade mockito-core to 2.24.0 and byte-buddy to 1.9.7"
diff --git a/.mailmap b/.mailmap
index cbf1f3b..c863847 100644
--- a/.mailmap
+++ b/.mailmap
@@ -12,6 +12,7 @@
 Carlos Eduardo Baldacin <carloseduardo.baldacin@sonyericsson.com>                           carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
 Changcheng Xiao <xchangcheng@google.com>                                                    xchangcheng
 Dariusz Luksza <dluksza@collab.net>                                                         <dariusz@luksza.org>
+Darrien Glasser <darrien@arista.com>                                                        darrien <darrien@arista.com>
 Dave Borowitz <dborowitz@google.com>                                                        <dborowitz@google.com>
 David Ostrovsky <david@ostrovsky.org>                                                       <d.ostrovsky@gmx.de>
 David Ostrovsky <david@ostrovsky.org>                                                       <david.ostrovsky@gmail.com>
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index c514321..2e7cf17 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1358,7 +1358,8 @@
 any of their groups is used.
 
 This limit applies not only to the link:cmd-query.html[`gerrit query`]
-command, but also to the web UI results pagination size.
+command, but also to the web UI results pagination size in the new
+PolyGerrit UI and, limited to the full project list, in the old GWT UI.
 
 
 [[capability_readAs]]
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 681b5ff..b15aea7 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -147,6 +147,11 @@
 $ ssh -p 29418 review.example.com gerrit review --verified +1 8242,2
 ----
 
+Approve the change with change number 8242 and patch set 2 as "Code-Review +2"
+----
+$ ssh -p 29418 review.example.com gerrit review --code-review +2 8242,2
+----
+
 Vote on the project specific label "mylabel":
 ----
 $ ssh -p 29418 review.example.com gerrit review --label mylabel=+1 8242,2
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index efa17da..bec1984 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -286,33 +286,6 @@
   bazel test //javatests/com/google/gerrit/acceptance/rest/account:rest_account
 ----
 
-The tests run with NoteDb fully enabled by default.
-
-To run the tests against NoteDb backend with write to NoteDb, but not read from
-it:
-
-----
-  bazel test --test_env=GERRIT_NOTEDB=WRITE //...
-----
-
-Write and read from NoteDb:
-
-----
-  bazel test --test_env=GERRIT_NOTEDB=READ_WRITE //...
-----
-
-Primary storage NoteDb:
-
-----
-  bazel test --test_env=GERRIT_NOTEDB=PRIMARY //...
-----
-
-NoteDb entirely disabled:
-
-----
-  bazel test --test_env=GERRIT_NOTEDB=OFF //...
-----
-
 To run only tests that do not use SSH:
 
 ----
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt
index ea51977..ca47690 100644
--- a/Documentation/dev-intellij.txt
+++ b/Documentation/dev-intellij.txt
@@ -184,10 +184,6 @@
 plugin manages a project, it intercepts the creation and creates a Bazel test
 run configuration instead, which can be used just like the standard ones.
 
-TIP: Tests run with NoteDb enabled by default. If you would like to execute a
-test with NoteDb turned off, add `--test_env=GERRIT_NOTEDB=OFF` to the *Bazel
-flags* of your run configuration.
-
 [[remote-debug]]
 === Debugging a remote Gerrit server
 If a remote Gerrit server is running and has opened a debug port, you can attach
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 8078922..c326b66 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -2153,8 +2153,8 @@
 link:config-gerrit.txt#accounts.visibility[visible] to the calling user are not
 considered.
 
-In all cases _except_ a bare account ID, inactive accounts are not considered.
-Inactive accounts may only be referenced by bare ID.
+In all cases _except_ a bare account ID and `self`/`me`, inactive accounts are
+not considered. Inactive accounts should only be referenced by bare ID.
 
 If the input is a bare account ID, this will always resolve to exactly
 one account if there is a visible account with that ID, and zero accounts
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index a6df45f..7d356bf 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.common.collect.ListMultimap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.CherryPickChangeInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
@@ -150,6 +152,9 @@
 
   RelatedChangesInfo related() throws RestApiException;
 
+  /** Returns votes on the revision. */
+  ListMultimap<String, ApprovalInfo> votes() throws RestApiException;
+
   abstract class MergeListRequest {
     private boolean addLinks;
     private int uninterestingParent = 1;
@@ -361,6 +366,11 @@
     }
 
     @Override
+    public ListMultimap<String, ApprovalInfo> votes() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public void description(String description) throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/java/com/google/gerrit/extensions/common/ApprovalInfo.java b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
index 703235d..e40004b 100644
--- a/java/com/google/gerrit/extensions/common/ApprovalInfo.java
+++ b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.common;
 
+import com.google.gerrit.common.Nullable;
 import java.sql.Timestamp;
 
 public class ApprovalInfo extends AccountInfo {
@@ -28,7 +29,11 @@
   }
 
   public ApprovalInfo(
-      Integer id, Integer value, VotingRangeInfo permittedVotingRange, String tag, Timestamp date) {
+      Integer id,
+      Integer value,
+      @Nullable VotingRangeInfo permittedVotingRange,
+      @Nullable String tag,
+      Timestamp date) {
     super(id);
     this.value = value;
     this.permittedVotingRange = permittedVotingRange;
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 3759636..e2fd7f3 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.pgm;
 
+import static com.google.gerrit.common.Version.getVersion;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -364,7 +366,15 @@
   }
 
   private String myVersion() {
-    return com.google.gerrit.common.Version.getVersion();
+    List<String> versionParts = new ArrayList<>();
+    if (slave) {
+      versionParts.add("[slave]");
+    }
+    if (headless) {
+      versionParts.add("[headless]");
+    }
+    versionParts.add(getVersion());
+    return Joiner.on(" ").join(versionParts);
   }
 
   private Injector createCfgInjector() {
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 24a2596..9b4952b 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -68,7 +68,7 @@
  * </ol>
  *
  * <p>The result never includes accounts that are not visible to the calling user. It also never
- * includes inactive accounts, with one specific exception noted in method Javadoc.
+ * includes inactive accounts, with a small number of specific exceptions noted in method Javadoc.
  */
 @Singleton
 public class AccountResolver {
@@ -123,7 +123,7 @@
         + state.getAccount().getNameEmail(result.accountResolver().anonymousCowardName);
   }
 
-  private static boolean isSelf(String input) {
+  public static boolean isSelf(String input) {
     return "self".equals(input) || "me".equals(input);
   }
 
@@ -252,6 +252,11 @@
 
   private class BySelf extends StringSearcher {
     @Override
+    public boolean callerShouldFilterOutInactiveCandidates() {
+      return false;
+    }
+
+    @Override
     public boolean callerMayAssumeCandidatesAreVisible() {
       return true;
     }
@@ -279,8 +284,6 @@
   private class ByExactAccountId extends AccountIdSearcher {
     @Override
     public boolean callerShouldFilterOutInactiveCandidates() {
-      // The only case where we *don't* enforce that the account is active is when passing an exact
-      // numeric account ID.
       return false;
     }
 
@@ -497,9 +500,9 @@
    *
    * <ul>
    *   <li>The strings {@code "self"} and {@code "me"}, if the current user is an {@link
-   *       IdentifiedUser}.
-   *   <li>A bare account ID ({@code "18419"}). In this case, and <strong>only</strong> this case,
-   *       may return exactly one inactive account. This case short-circuits if the input matches.
+   *       IdentifiedUser}. In this case, may return exactly one inactive account.
+   *   <li>A bare account ID ({@code "18419"}). In this case, may return exactly one inactive
+   *       account. This case short-circuits if the input matches.
    *   <li>An account ID in parentheses following a full name ({@code "Full Name (18419)"}). This
    *       case short-circuits if the input matches.
    *   <li>A username ({@code "username"}).
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index f8a2ecb..2df7ae6 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -18,6 +18,8 @@
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder.ListMultimapBuilder;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes;
@@ -36,6 +38,7 @@
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.CherryPickChangeInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
@@ -51,6 +54,10 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.account.AccountDirectory.FillOptions;
+import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.change.RebaseUtil;
 import com.google.gerrit.server.change.RevisionResource;
@@ -85,6 +92,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -135,6 +143,8 @@
   private final GetRelated getRelated;
   private final PutDescription putDescription;
   private final GetDescription getDescription;
+  private final ApprovalsUtil approvalsUtil;
+  private final AccountLoader.Factory accountLoaderFactory;
 
   @Inject
   RevisionApiImpl(
@@ -176,6 +186,8 @@
       GetRelated getRelated,
       PutDescription putDescription,
       GetDescription getDescription,
+      ApprovalsUtil approvalsUtil,
+      AccountLoader.Factory accountLoaderFactory,
       @Assisted RevisionResource r) {
     this.repoManager = repoManager;
     this.changes = changes;
@@ -215,6 +227,8 @@
     this.getRelated = getRelated;
     this.putDescription = putDescription;
     this.getDescription = getDescription;
+    this.approvalsUtil = approvalsUtil;
+    this.accountLoaderFactory = accountLoaderFactory;
     this.revision = r;
   }
 
@@ -568,6 +582,36 @@
   }
 
   @Override
+  public ListMultimap<String, ApprovalInfo> votes() throws RestApiException {
+    ListMultimap<String, ApprovalInfo> result =
+        ListMultimapBuilder.treeKeys().arrayListValues().build();
+    try {
+      Iterable<PatchSetApproval> approvals =
+          approvalsUtil.byPatchSet(revision.getNotes(), revision.getPatchSet().getId(), null, null);
+      AccountLoader accountLoader =
+          accountLoaderFactory.create(
+              EnumSet.of(
+                  FillOptions.ID, FillOptions.NAME, FillOptions.EMAIL, FillOptions.USERNAME));
+      for (PatchSetApproval approval : approvals) {
+        String label = approval.getLabel();
+        ApprovalInfo info =
+            new ApprovalInfo(
+                approval.getAccountId().get(),
+                Integer.valueOf(approval.getValue()),
+                null,
+                approval.getTag(),
+                approval.getGranted());
+        accountLoader.put(info);
+        result.get(label).add(info);
+      }
+      accountLoader.fill();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get votes", e);
+    }
+    return result;
+  }
+
+  @Override
   public void description(String description) throws RestApiException {
     DescriptionInput in = new DescriptionInput();
     in.description = description;
diff --git a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
index 1e6b94d..721d878 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.project.ListProjects;
@@ -155,7 +154,7 @@
           .withQuery(r.getQuery())
           .withLimit(r.getLimit())
           .withStart(r.getStart())
-          .apply(TopLevelResource.INSTANCE);
+          .apply();
     } catch (OrmException e) {
       throw new RestApiException("Cannot query projects", e);
     }
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index c588bdf..c189f33 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -133,6 +133,9 @@
     // See if we can get away with a single, cheap ref evaluation.
     if (refs.size() == 1) {
       String refName = Iterables.getOnlyElement(refs.values()).getName();
+      if (opts.filterMeta() && isMetadata(refName)) {
+        return ImmutableMap.of();
+      }
       if (RefNames.isRefsChanges(refName)) {
         return canSeeSingleChangeRef(refName) ? refs : ImmutableMap.of();
       }
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 8bb6694..9fdc7e8 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -50,6 +50,7 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.UsedAt;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -1542,6 +1543,7 @@
     return m.stream().sorted().collect(toImmutableList());
   }
 
+  @UsedAt(UsedAt.Project.GOOGLE)
   public boolean hasLegacyPermissions() {
     return hasLegacyPermissions;
   }
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index b62a1d7..93ece2b 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.reviewdb.client.Change.CHANGE_ID_PATTERN;
+import static com.google.gerrit.server.account.AccountResolver.isSelf;
 import static com.google.gerrit.server.query.change.ChangeData.asChanges;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
@@ -951,6 +952,9 @@
   @Operator
   public Predicate<ChangeData> visibleto(String who)
       throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+    if (isSelf(who)) {
+      return is_visible();
+    }
     try {
       return Predicate.or(
           parseAccount(who)
diff --git a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
index 571c319..092f118 100644
--- a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -54,7 +55,7 @@
 
   @Override
   protected Response<String> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, SetPrivateOp.Input input)
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, @Nullable SetPrivateOp.Input input)
       throws RestApiException, UpdateException {
     if (!canDeletePrivate(rsrc).value()) {
       throw new AuthException("not allowed to unmark private");
diff --git a/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java b/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java
index 04c94be..3bb297d 100644
--- a/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java
+++ b/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -43,14 +44,14 @@
   }
 
   public interface Factory {
-    SetPrivateOp create(ChangeMessagesUtil cmUtil, boolean isPrivate, Input input);
+    SetPrivateOp create(ChangeMessagesUtil cmUtil, boolean isPrivate, @Nullable Input input);
   }
 
-  private final ChangeMessagesUtil cmUtil;
-  private final PatchSetUtil psUtil;
-  private final boolean isPrivate;
-  private final Input input;
   private final PrivateStateChanged privateStateChanged;
+  private final PatchSetUtil psUtil;
+  private final ChangeMessagesUtil cmUtil;
+  private final boolean isPrivate;
+  @Nullable private final Input input;
 
   private Change change;
   private PatchSet ps;
@@ -61,12 +62,12 @@
       PatchSetUtil psUtil,
       @Assisted ChangeMessagesUtil cmUtil,
       @Assisted boolean isPrivate,
-      @Assisted Input input) {
-    this.cmUtil = cmUtil;
+      @Assisted @Nullable Input input) {
+    this.privateStateChanged = privateStateChanged;
     this.psUtil = psUtil;
+    this.cmUtil = cmUtil;
     this.isPrivate = isPrivate;
     this.input = input;
-    this.privateStateChanged = privateStateChanged;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
index 12f747b..cfeec5a 100644
--- a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -90,7 +89,7 @@
         .get()
         .withQuery("parent:" + parent.get())
         .withLimit(limit)
-        .apply(TopLevelResource.INSTANCE)
+        .apply()
         .stream()
         .filter(
             p ->
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index 35d1ca1..1600fed 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -14,11 +14,15 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static com.google.common.collect.Ordering.natural;
 import static com.google.gerrit.extensions.client.ProjectState.HIDDEN;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.base.Strings;
+import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -29,6 +33,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
@@ -51,7 +56,9 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.TreeFormatter;
 import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import java.io.BufferedWriter;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -67,6 +74,7 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.SortedMap;
 import java.util.SortedSet;
 import java.util.TreeMap;
@@ -244,6 +252,7 @@
   private String matchSubstring;
   private String matchRegex;
   private AccountGroup.UUID groupUuid;
+  private final Provider<QueryProjects> queryProjectsProvider;
 
   @Inject
   protected ListProjects(
@@ -254,7 +263,8 @@
       GitRepositoryManager repoManager,
       PermissionBackend permissionBackend,
       ProjectNode.Factory projectNodeFactory,
-      WebLinks webLinks) {
+      WebLinks webLinks,
+      Provider<QueryProjects> queryProjectsProvider) {
     this.currentUser = currentUser;
     this.projectCache = projectCache;
     this.groupResolver = groupResolver;
@@ -263,6 +273,7 @@
     this.permissionBackend = permissionBackend;
     this.projectNodeFactory = projectNodeFactory;
     this.webLinks = webLinks;
+    this.queryProjectsProvider = queryProjectsProvider;
   }
 
   public List<String> getShowBranch() {
@@ -291,7 +302,7 @@
       throws BadRequestException, PermissionBackendException {
     if (format == OutputFormat.TEXT) {
       ByteArrayOutputStream buf = new ByteArrayOutputStream();
-      display(buf);
+      displayToStream(buf);
       return BinaryResult.create(buf.toByteArray())
           .setContentType("text/plain")
           .setCharacterEncoding(UTF_8);
@@ -301,11 +312,104 @@
 
   public SortedMap<String, ProjectInfo> apply()
       throws BadRequestException, PermissionBackendException {
+    Optional<String> projectQuery = expressAsProjectsQuery();
+    if (projectQuery.isPresent()) {
+      return applyAsQuery(projectQuery.get());
+    }
+
     format = OutputFormat.JSON;
     return display(null);
   }
 
-  public SortedMap<String, ProjectInfo> display(@Nullable OutputStream displayOutputStream)
+  private Optional<String> expressAsProjectsQuery() {
+    return !all
+            && state != HIDDEN
+            && isNullOrEmpty(matchPrefix)
+            && isNullOrEmpty(matchRegex)
+            && isNullOrEmpty(matchSubstring) // TODO: see Issue 10446
+            && type == FilterType.ALL
+            && showBranch.isEmpty()
+            && !showTree
+        ? Optional.of(stateToQuery())
+        : Optional.empty();
+  }
+
+  private String stateToQuery() {
+    List<String> queries = new ArrayList<>();
+    if (state == null) {
+      queries.add("(state:active OR state:read-only)");
+    } else {
+      queries.add(String.format("(state:%s)", state.name()));
+    }
+
+    return Joiner.on(" AND ").join(queries).toString();
+  }
+
+  private SortedMap<String, ProjectInfo> applyAsQuery(String query) throws BadRequestException {
+    try {
+      return queryProjectsProvider
+          .get()
+          .withQuery(query)
+          .withStart(start)
+          .withLimit(limit)
+          .apply()
+          .stream()
+          .collect(
+              ImmutableSortedMap.toImmutableSortedMap(
+                  natural(), p -> p.name, p -> showDescription ? p : nullifyDescription(p)));
+    } catch (OrmException | MethodNotAllowedException e) {
+      logger.atWarning().withCause(e).log(
+          "Internal error while processing the query '{}' request", query);
+      throw new BadRequestException("Internal error while processing the query request");
+    }
+  }
+
+  private ProjectInfo nullifyDescription(ProjectInfo p) {
+    p.description = null;
+    return p;
+  }
+
+  private void printQueryResults(String query, PrintWriter out) throws BadRequestException {
+    try {
+      if (format.isJson()) {
+        format.newGson().toJson(applyAsQuery(query), out);
+      } else {
+        newProjectsNamesStream(query).forEach(out::println);
+      }
+      out.flush();
+    } catch (OrmException | MethodNotAllowedException e) {
+      logger.atWarning().withCause(e).log(
+          "Internal error while processing the query '{}' request", query);
+      throw new BadRequestException("Internal error while processing the query request");
+    }
+  }
+
+  private Stream<String> newProjectsNamesStream(String query)
+      throws OrmException, MethodNotAllowedException, BadRequestException {
+    Stream<String> projects =
+        queryProjectsProvider.get().withQuery(query).apply().stream().map(p -> p.name).skip(start);
+    if (limit > 0) {
+      projects = projects.limit(limit);
+    }
+
+    return projects;
+  }
+
+  public void displayToStream(OutputStream displayOutputStream)
+      throws BadRequestException, PermissionBackendException {
+    PrintWriter stdout =
+        new PrintWriter(new BufferedWriter(new OutputStreamWriter(displayOutputStream, UTF_8)));
+    Optional<String> projectsQuery = expressAsProjectsQuery();
+
+    if (projectsQuery.isPresent()) {
+      printQueryResults(projectsQuery.get(), stdout);
+    } else {
+      display(stdout);
+    }
+  }
+
+  @Nullable
+  public SortedMap<String, ProjectInfo> display(@Nullable PrintWriter stdout)
       throws BadRequestException, PermissionBackendException {
     if (all && state != null) {
       throw new BadRequestException("'all' and 'state' may not be used together");
@@ -320,12 +424,6 @@
       }
     }
 
-    PrintWriter stdout = null;
-    if (displayOutputStream != null) {
-      stdout =
-          new PrintWriter(new BufferedWriter(new OutputStreamWriter(displayOutputStream, UTF_8)));
-    }
-
     int foundIndex = 0;
     int found = 0;
     TreeMap<String, ProjectInfo> output = new TreeMap<>();
@@ -373,7 +471,7 @@
         }
 
         if (showDescription) {
-          info.description = Strings.emptyToNull(e.getProject().getDescription());
+          info.description = emptyToNull(e.getProject().getDescription());
         }
         info.state = e.getProject().getState();
 
diff --git a/java/com/google/gerrit/server/restapi/project/QueryProjects.java b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
index 5561d4a..44432aa 100644
--- a/java/com/google/gerrit/server/restapi/project/QueryProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
@@ -89,6 +89,11 @@
   @Override
   public List<ProjectInfo> apply(TopLevelResource resource)
       throws BadRequestException, MethodNotAllowedException, OrmException {
+    return apply();
+  }
+
+  public List<ProjectInfo> apply()
+      throws BadRequestException, MethodNotAllowedException, OrmException {
     if (Strings.isNullOrEmpty(query)) {
       throw new BadRequestException("missing query field");
     }
diff --git a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index d04e2d3..9f2ffa9 100644
--- a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -41,6 +41,6 @@
         throw die("--tree and --description options are not compatible.");
       }
     }
-    impl.display(out);
+    impl.displayToStream(out);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 3507714..e8c828a 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -35,6 +35,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Iterators;
+import com.google.common.collect.ListMultimap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
@@ -1460,6 +1461,47 @@
         .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
   }
 
+  @Test
+  public void listVotesByRevision() throws Exception {
+    // Create patch set 1 and vote on it
+    String changeId = createChange().getChangeId();
+    ListMultimap<String, ApprovalInfo> votes = gApi.changes().id(changeId).current().votes();
+    assertThat(votes).isEmpty();
+    recommend(changeId);
+    votes = gApi.changes().id(changeId).current().votes();
+    assertThat(votes.keySet()).containsExactly("Code-Review");
+    List<ApprovalInfo> approvals = votes.get("Code-Review");
+    assertThat(approvals).hasSize(1);
+    ApprovalInfo approval = approvals.get(0);
+    assertThat(approval._accountId).isEqualTo(admin.id.get());
+    assertThat(approval.email).isEqualTo(admin.email);
+    assertThat(approval.username).isEqualTo(admin.username);
+
+    // Also vote on it with another user
+    requestScopeOperations.setApiUser(user.getId());
+    gApi.changes().id(changeId).current().review(ReviewInput.dislike());
+
+    // Patch set 1 has 2 votes on Code-Review
+    requestScopeOperations.setApiUser(admin.getId());
+    votes = gApi.changes().id(changeId).current().votes();
+    assertThat(votes.keySet()).containsExactly("Code-Review");
+    approvals = votes.get("Code-Review");
+    assertThat(approvals).hasSize(2);
+    assertThat(approvals.stream().map(a -> a._accountId))
+        .containsExactlyElementsIn(ImmutableList.of(admin.id.get(), user.id.get()));
+
+    // Create a new patch set which does not have any votes
+    amendChange(changeId);
+    votes = gApi.changes().id(changeId).current().votes();
+    assertThat(votes).isEmpty();
+
+    // Votes are still returned for ps 1
+    votes = gApi.changes().id(changeId).revision(1).votes();
+    assertThat(votes.keySet()).containsExactly("Code-Review");
+    approvals = votes.get("Code-Review");
+    assertThat(approvals).hasSize(2);
+  }
+
   private static void assertCherryPickResult(
       ChangeInfo changeInfo, CherryPickInput input, String srcChangeId) throws Exception {
     assertThat(changeInfo.changeId).isEqualTo(srcChangeId);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index 0208926..a30e2b9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -17,7 +17,10 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertThatNameList;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static java.util.stream.Collectors.toList;
 
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -33,12 +36,19 @@
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.restapi.project.ListProjects;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
 import com.google.inject.Inject;
+import java.io.ByteArrayOutputStream;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.IntStream;
 import org.junit.Test;
 
 @NoHttpd
@@ -46,6 +56,7 @@
 public class ListProjectsIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ListProjects listProjects;
 
   @Test
   public void listProjects() throws Exception {
@@ -110,6 +121,63 @@
   }
 
   @Test
+  public void listProjectsToOutputStream() throws Exception {
+    int numInitialProjects = gApi.projects().list().get().size();
+    int numTestProjects = 5;
+    List<String> testProjects = createProjects("zzz_testProject", numTestProjects);
+    try (ByteArrayOutputStream displayOut = new ByteArrayOutputStream()) {
+
+      listProjects.setStart(numInitialProjects);
+      listProjects.displayToStream(displayOut);
+
+      List<String> lines =
+          Splitter.on("\n").omitEmptyStrings().splitToList(new String(displayOut.toByteArray()));
+      assertThat(lines).isEqualTo(testProjects);
+    }
+  }
+
+  @Test
+  public void listProjectsAsJsonMultilineToOutputStream() throws Exception {
+    listProjectsAsJsonToOutputStream(OutputFormat.JSON);
+  }
+
+  @Test
+  public void listProjectsAsJsonCompactToOutputStream() throws Exception {
+    String jsonOutput = listProjectsAsJsonToOutputStream(OutputFormat.JSON_COMPACT).trim();
+    assertThat(jsonOutput).doesNotContain("\n");
+  }
+
+  private String listProjectsAsJsonToOutputStream(OutputFormat jsonFormat) throws Exception {
+    assertThat(jsonFormat.isJson()).isTrue();
+
+    int numInitialProjects = gApi.projects().list().get().size();
+    int numTestProjects = 5;
+    Set<String> testProjects =
+        ImmutableSet.copyOf(createProjects("zzz_testProject", numTestProjects));
+    try (ByteArrayOutputStream displayOut = new ByteArrayOutputStream()) {
+
+      listProjects.setStart(numInitialProjects);
+      listProjects.setFormat(jsonFormat);
+      listProjects.displayToStream(displayOut);
+
+      String projectsJsonOutput = new String(displayOut.toByteArray());
+
+      Gson gson = jsonFormat.newGson();
+      Set<String> projectsJsonNames = gson.fromJson(projectsJsonOutput, JsonObject.class).keySet();
+      assertThat(projectsJsonNames).isEqualTo(testProjects);
+
+      return projectsJsonOutput;
+    }
+  }
+
+  private List<String> createProjects(String prefix, int numProjects) {
+    return IntStream.range(0, numProjects)
+        .mapToObj(i -> projectOperations.newProject().name(prefix + i).create())
+        .map(Project.NameKey::get)
+        .collect(toList());
+  }
+
+  @Test
   public void listProjectsWithPrefix() throws Exception {
     Project.NameKey someProject = projectOperations.newProject().name("listtest-p1").create();
     Project.NameKey someOtherProject = projectOperations.newProject().name("listtest-p2").create();
diff --git a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
index 63fda12..d99fa72 100644
--- a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
@@ -105,6 +105,19 @@
   }
 
   @Test
+  public void bySelfInactive() throws Exception {
+    gApi.accounts().id(user.id.get()).setActive(false);
+
+    requestScopeOperations.setApiUser(user.id);
+    assertThat(gApi.accounts().id("self").getActive()).isFalse();
+
+    Result result = resolveAsResult("self");
+    assertThat(result.asIdSet()).containsExactly(user.id);
+    assertThat(result.isSelf()).isTrue();
+    assertThat(result.asUniqueUser()).isSameAs(self.get());
+  }
+
+  @Test
   public void byExactAccountId() throws Exception {
     Account.Id existingId = accountOperations.newAccount().create();
     Account.Id idWithExistingIdAsFullname =
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 7872a82..da62b7c 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -64,6 +64,7 @@
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.index.FieldDef;
@@ -77,6 +78,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -155,6 +157,7 @@
   @Inject protected ChangeIndexer indexer;
   @Inject protected IndexConfig indexConfig;
   @Inject protected InMemoryRepositoryManager repoManager;
+  @Inject protected Provider<AnonymousUser> anonymousUserProvider;
   @Inject protected Provider<InternalChangeQuery> queryProvider;
   @Inject protected ChangeNotes.Factory notesFactory;
   @Inject protected OneOffRequestContext oneOffRequestContext;
@@ -1581,11 +1584,21 @@
     assertQuery("directory:/b/c", change5);
     assertQuery("directory:/b/c/", change5);
     assertQuery("directory:b/c/", change5);
+  }
+
+  @Test
+  public void byDirectoryRegex() throws Exception {
+    assume().that(getSchemaVersion()).isAtLeast(55);
+
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(repo, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
+    Change change2 =
+        insert(repo, newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
 
     // match by regexp
-    assertQuery("directory:^.*va.*", change2);
-    assertQuery("directory:^documentation/.*/slides", change3);
-    assertQuery("directory:^train.*", change3);
+    assertQuery("directory:^.*va.*", change1);
+    assertQuery("directory:^documentation/.*/slides", change2);
+    assertQuery("directory:^train.*", change2);
   }
 
   @Test
@@ -1847,6 +1860,24 @@
   }
 
   @Test
+  public void visibleToSelf() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(repo, newChange(repo));
+    Change change2 = insert(repo, newChange(repo));
+
+    gApi.changes().id(change2.getChangeId()).setPrivate(true, "private");
+
+    String q = "project:repo";
+    assertQuery(q + " visibleto:self", change2, change1);
+    assertQuery(q + " visibleto:me", change2, change1);
+
+    // Anonymous user cannot see first user's private change.
+    requestContext.setContext(anonymousUserProvider::get);
+    assertQuery(q + " visibleto:self", change1);
+    assertQuery(q + " visibleto:me", change1);
+  }
+
+  @Test
   public void byCommentBy() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo));
@@ -3065,6 +3096,44 @@
     assertQuery("project:repo+foo", change);
   }
 
+  @Test
+  public void selfFailsForAnonymousUser() throws Exception {
+    for (String query : ImmutableList.of("assignee:self", "starredby:self", "is:starred")) {
+      assertQuery(query);
+      RequestContext oldContext = requestContext.setContext(anonymousUserProvider::get);
+
+      try {
+        requestContext.setContext(anonymousUserProvider::get);
+        assertThatAuthException(query)
+            .hasMessageThat()
+            .isEqualTo("Must be signed-in to use this operator");
+      } finally {
+        requestContext.setContext(oldContext);
+      }
+    }
+  }
+
+  @Test
+  public void selfSucceedsForInactiveAccount() throws Exception {
+    Account.Id user2 =
+        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+
+    TestRepository<Repo> repo = createProject("repo");
+    Change change = insert(repo, newChange(repo));
+    AssigneeInput ain = new AssigneeInput();
+    ain.assignee = user2.toString();
+    gApi.changes().id(change.getId().get()).setAssignee(ain);
+
+    RequestContext adminContext = requestContext.setContext(newRequestContext(user2));
+    assertQuery("assignee:self", change);
+
+    requestContext.setContext(adminContext);
+    gApi.accounts().id(user2.get()).setActive(false);
+
+    requestContext.setContext(newRequestContext(user2));
+    assertQuery("assignee:self", change);
+  }
+
   protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception {
     return newChange(repo, null, null, null, null, false);
   }
@@ -3194,6 +3263,15 @@
     }
   }
 
+  protected ThrowableSubject assertThatAuthException(Object query) throws Exception {
+    try {
+      newQuery(query).get();
+      throw new AssertionError("expected AuthException for query: " + query);
+    } catch (AuthException e) {
+      return assertThat(e);
+    }
+  }
+
   protected TestRepository<Repo> createProject(String name) throws Exception {
     gApi.projects().create(name).get();
     return new TestRepository<>(repoManager.openRepository(new Project.NameKey(name)));
diff --git a/plugins/delete-project b/plugins/delete-project
index 83b92fd..59b9220 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit 83b92fdba19ff01cd87b08e7ef2ffdfbcffcc3ce
+Subproject commit 59b922061d791aaa0e65922023177d3347bea365
diff --git a/plugins/gitiles b/plugins/gitiles
index 09a5ff0..b90db6d 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 09a5ff01af4607b54b5a584fa87ae3d7901c7ee9
+Subproject commit b90db6dd4c43294e94523222a9ee57a65d11d058
diff --git a/plugins/webhooks b/plugins/webhooks
index 217c8ef..4d67d66 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit 217c8ef00564e29309b76fdf71bc91c127ec67ec
+Subproject commit 4d67d6654a121462032c96bc45e193a4bcfed025
diff --git a/polygerrit-ui/app/.eslintrc.json b/polygerrit-ui/app/.eslintrc.json
index 97151f2..b5d3dae 100644
--- a/polygerrit-ui/app/.eslintrc.json
+++ b/polygerrit-ui/app/.eslintrc.json
@@ -63,6 +63,7 @@
     "object-shorthand": ["error", "always"],
     "prefer-arrow-callback": "error",
     "prefer-const": "error",
+    "prefer-promise-reject-errors": "off",
     "prefer-spread": "error",
     "quote-props": ["error", "consistent-as-needed"],
     "require-jsdoc": "off",
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
index 22f461b..a5bb5fd 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
@@ -109,6 +109,7 @@
               items="{{_rules}}"
               as="rule">
             <gr-rule-editor
+                has-range="[[_computeHasRange(name)]]"
                 label="[[_label]]"
                 editing="[[editing]]"
                 group-id="[[rule.id]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
index 31d371d..afe5a86 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -19,6 +19,11 @@
 
   const MAX_AUTOCOMPLETE_RESULTS = 20;
 
+  const RANGE_NAMES = [
+    'QUERY LIMIT',
+    'BATCH CHANGES LIMIT',
+  ];
+
   /**
    * Fired when the permission has been modified or removed.
    *
@@ -269,5 +274,11 @@
       this.set(['permission', 'value', 'rules', groupId], value);
       this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
     },
+
+    _computeHasRange(name) {
+      if (!name) { return false; }
+
+      return RANGE_NAMES.includes(name.toUpperCase());
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
index e29c4a2..a6381d1 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -255,6 +255,14 @@
         assert.isFalse(element._deleted);
         assert.isNotOk(element.permission.value.deleted);
       });
+
+      test('_computeHasRange', () => {
+        assert.isTrue(element._computeHasRange('Query Limit'));
+
+        assert.isTrue(element._computeHasRange('Batch Changes Limit'));
+
+        assert.isFalse(element._computeHasRange('test'));
+      });
     });
 
     suite('interactions', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
index 77a1e2a..d79bf0d 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -307,8 +307,8 @@
         commands.push({
           title,
           command: commandObj[title]
-              .replace('${project}', encodeURI(repo))
-              .replace('${project-base-name}',
+              .replace(/\$\{project\}/gi, encodeURI(repo))
+              .replace(/\$\{project-base-name\}/gi,
               encodeURI(repo.substring(repo.lastIndexOf('/') + 1))),
         });
       }
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
index d59deed4..c8ae650 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
@@ -71,7 +71,11 @@
         color: var(--deemphasized-text-color);
       }
     </style>
-    <style include="gr-form-styles"></style>
+    <style include="gr-form-styles">
+      iron-autogrow-textarea {
+        width: 14em;
+      }
+    </style>
     <div id="mainContainer"
         class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
       <div id="options">
@@ -106,6 +110,22 @@
             </select>
           </gr-select>
         </template>
+        <template is="dom-if" if="[[hasRange]]">
+          <iron-autogrow-textarea
+              id="minInput"
+              class="min"
+              autocomplete="on"
+              placeholder="Min value"
+              bind-value="{{rule.value.min}}"
+              disabled$="[[!editing]]"></iron-autogrow-textarea>
+          <iron-autogrow-textarea
+              id="maxInput"
+              class="max"
+              autocomplete="on"
+              placeholder="Max value"
+              bind-value="{{rule.value.max}}"
+              disabled$="[[!editing]]"></iron-autogrow-textarea>
+        </template>
         <a class="groupPath" href$="[[_computeGroupPath(groupId)]]">
           [[groupName]]
         </a>
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index b99125c..06f703f 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -67,6 +67,7 @@
     is: 'gr-rule-editor',
 
     properties: {
+      hasRange: Boolean,
       /** @type {?} */
       label: Object,
       editing: {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 708a730..0ce622546 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -226,6 +226,7 @@
 
     _computeItemNeedsReview(account, change, showReviewedState) {
       return showReviewedState && !change.reviewed &&
+          !change.work_in_progress &&
           this.changeIsOpen(change.status) &&
           (!account || account._account_id != change.owner._account_id);
     },
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index d20d40a..d5b9aa9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -231,11 +231,17 @@
           status: 'ABANDONED',
           owner: {_account_id: 0},
         },
+        {
+          _number: 4,
+          status: 'NEW',
+          work_in_progress: true,
+          owner: {_account_id: 0},
+        },
       ];
       flushAsynchronousOperations();
       let elementItems = Polymer.dom(element.root).querySelectorAll(
           'gr-change-list-item');
-      assert.equal(elementItems.length, 4);
+      assert.equal(elementItems.length, 5);
       for (let i = 0; i < elementItems.length; i++) {
         assert.isFalse(elementItems[i].hasAttribute('needs-review'));
       }
@@ -243,20 +249,22 @@
       element.showReviewedState = true;
       elementItems = Polymer.dom(element.root).querySelectorAll(
           'gr-change-list-item');
-      assert.equal(elementItems.length, 4);
+      assert.equal(elementItems.length, 5);
       assert.isFalse(elementItems[0].hasAttribute('needs-review'));
       assert.isTrue(elementItems[1].hasAttribute('needs-review'));
       assert.isFalse(elementItems[2].hasAttribute('needs-review'));
       assert.isFalse(elementItems[3].hasAttribute('needs-review'));
+      assert.isFalse(elementItems[4].hasAttribute('needs-review'));
 
       element.account = {_account_id: 42};
       elementItems = Polymer.dom(element.root).querySelectorAll(
           'gr-change-list-item');
-      assert.equal(elementItems.length, 4);
+      assert.equal(elementItems.length, 5);
       assert.isFalse(elementItems[0].hasAttribute('needs-review'));
       assert.isTrue(elementItems[1].hasAttribute('needs-review'));
       assert.isFalse(elementItems[2].hasAttribute('needs-review'));
       assert.isFalse(elementItems[3].hasAttribute('needs-review'));
+      assert.isFalse(elementItems[4].hasAttribute('needs-review'));
     });
 
     test('no changes', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index f4328f9..bc072b1 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -22,6 +22,7 @@
     'application/json': 'json',
     'application/x-powershell': 'powershell',
     'application/typescript': 'typescript',
+    'application/xml': 'xml',
     'application/xquery': 'xquery',
     'application/x-erb': 'erb',
     'text/css': 'css',
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
index 8efd309..3afbe54 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -26,8 +26,8 @@
   };
 
   const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
-      'It will not appear in dashboards, and email notifications will be ' +
-      'silenced until the review is started.';
+      'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
+      'and email notifications will be silenced until the review is started.';
 
   const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
       'current reviewers (or anyone with "View Private Changes" permission).';