Merge "Permit some diff cursor keyboard shortcuts when any diffs are expanded"
diff --git a/Documentation/user-search-accounts.txt b/Documentation/user-search-accounts.txt
index 04f40c0..6bcd18e 100644
--- a/Documentation/user-search-accounts.txt
+++ b/Documentation/user-search-accounts.txt
@@ -23,6 +23,11 @@
 returned results. Search can also be performed by typing only a
 text with no operator, which will match against a variety of fields.
 
+[[cansee]]
+cansee:'CHANGE'::
++
+Matches accounts that can see the change 'CHANGE'.
+
 [[email]]
 email:'EMAIL'::
 +
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
index 06ceb49..7f4522f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -27,6 +27,7 @@
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtorm.client.KeyUtil;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -49,7 +50,7 @@
   public static void suggest(String query, int limit, AsyncCallback<JsArray<AccountInfo>> cb) {
     new RestApi("/accounts/")
         .addParameterTrue("suggest")
-        .addParameter("q", query)
+        .addParameterRaw("q", KeyUtil.encode(query))
         .addParameter("n", limit)
         .background()
         .get(cb);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
index 0fd85f1..a376782 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
@@ -102,6 +102,7 @@
     this.changeId = info.legacyId();
     this.project = info.projectNameKey();
     this.canEdit = info.hasActions() && info.actions().containsKey("assignee");
+    assigneeSuggestOracle.setChange(info);
     setAssignee(info.assignee());
     editAssigneeIcon.setVisible(canEdit);
     if (!canEdit) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java
index 964f7ad..c8bbfc3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.account.AccountApi;
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountSuggestOracle.AccountSuggestion;
@@ -27,10 +28,17 @@
 
 /** REST API based suggestion Oracle for assignee */
 public class AssigneeSuggestOracle extends SuggestAfterTypingNCharsOracle {
+
+  private ChangeInfo change;
+
+  public void setChange(ChangeInfo change) {
+    this.change = change;
+  }
+
   @Override
   protected void _onRequestSuggestions(Request req, Callback cb) {
     AccountApi.suggest(
-        req.getQuery(),
+        getQuery(req),
         req.getLimit(),
         new GerritCallback<JsArray<AccountInfo>>() {
           @Override
@@ -49,4 +57,13 @@
           }
         });
   }
+
+  private String getQuery(Request req) {
+    StringBuilder query = new StringBuilder();
+    query.append(req.getQuery());
+    if (change != null) {
+      query.append(" cansee:").append(change._number());
+    }
+    return query.toString();
+  }
 }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java b/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
index a470696..6905cf4 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.index.query.IntegerRangePredicate;
 import com.google.gerrit.index.query.NotPredicate;
 import com.google.gerrit.index.query.OrPredicate;
+import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.RegexPredicate;
@@ -43,6 +44,8 @@
       return not(p);
     } else if (p instanceof IndexPredicate) {
       return fieldQuery((IndexPredicate<T>) p);
+    } else if (p instanceof PostFilterPredicate) {
+      return QueryBuilders.matchAllQuery();
     } else {
       throw new QueryParseException("cannot create query for index: " + p);
     }
diff --git a/java/com/google/gerrit/index/query/PostFilterPredicate.java b/java/com/google/gerrit/index/query/PostFilterPredicate.java
new file mode 100644
index 0000000..3e780bf
--- /dev/null
+++ b/java/com/google/gerrit/index/query/PostFilterPredicate.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2016 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.index.query;
+
+/**
+ * Matches all documents in the index, with additional filtering done in the subclass's {@code
+ * match} method.
+ */
+public abstract class PostFilterPredicate<T> extends Predicate<T> implements Matchable<T> {}
diff --git a/java/com/google/gerrit/lucene/QueryBuilder.java b/java/com/google/gerrit/lucene/QueryBuilder.java
index 2f2a1cd..4500942 100644
--- a/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.index.query.IntegerRangePredicate;
 import com.google.gerrit.index.query.NotPredicate;
 import com.google.gerrit.index.query.OrPredicate;
+import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.RegexPredicate;
@@ -76,6 +77,8 @@
       return not(p);
     } else if (p instanceof IndexPredicate) {
       return fieldQuery((IndexPredicate<V>) p);
+    } else if (p instanceof PostFilterPredicate) {
+      return new MatchAllDocsQuery();
     } else {
       throw new QueryParseException("cannot create query for index: " + p);
     }
diff --git a/java/com/google/gerrit/server/ChangeFinder.java b/java/com/google/gerrit/server/ChangeFinder.java
index cb82778..677b091 100644
--- a/java/com/google/gerrit/server/ChangeFinder.java
+++ b/java/com/google/gerrit/server/ChangeFinder.java
@@ -110,6 +110,14 @@
     this.allowedIdTypes = ImmutableSet.copyOf(configuredChangeIdTypes);
   }
 
+  public ChangeNotes findOne(String id) throws OrmException {
+    List<ChangeNotes> ctls = find(id);
+    if (ctls.size() != 1) {
+      return null;
+    }
+    return ctls.get(0);
+  }
+
   /**
    * Find changes matching the given identifier.
    *
diff --git a/java/com/google/gerrit/server/config/PluginConfigFactory.java b/java/com/google/gerrit/server/config/PluginConfigFactory.java
index 41230b2..a46efb8 100644
--- a/java/com/google/gerrit/server/config/PluginConfigFactory.java
+++ b/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.server.config;
 
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.git.ProjectLevelConfig;
 import com.google.gerrit.server.plugins.Plugin;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.securestore.SecureStore;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/git/SubmoduleOp.java b/java/com/google/gerrit/server/git/SubmoduleOp.java
index 0e3e146..d7af141 100644
--- a/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateListener;
 import com.google.gerrit.server.update.RepoContext;
@@ -98,7 +97,6 @@
     private final PersonIdent myIdent;
     private final Config cfg;
     private final ProjectCache projectCache;
-    private final ProjectState.Factory projectStateFactory;
     private final BatchUpdate.Factory batchUpdateFactory;
 
     @Inject
@@ -107,27 +105,18 @@
         @GerritPersonIdent PersonIdent myIdent,
         @GerritServerConfig Config cfg,
         ProjectCache projectCache,
-        ProjectState.Factory projectStateFactory,
         BatchUpdate.Factory batchUpdateFactory) {
       this.gitmodulesFactory = gitmodulesFactory;
       this.myIdent = myIdent;
       this.cfg = cfg;
       this.projectCache = projectCache;
-      this.projectStateFactory = projectStateFactory;
       this.batchUpdateFactory = batchUpdateFactory;
     }
 
     public SubmoduleOp create(Set<Branch.NameKey> updatedBranches, MergeOpRepoManager orm)
         throws SubmoduleException {
       return new SubmoduleOp(
-          gitmodulesFactory,
-          myIdent,
-          cfg,
-          projectCache,
-          projectStateFactory,
-          batchUpdateFactory,
-          updatedBranches,
-          orm);
+          gitmodulesFactory, myIdent, cfg, projectCache, batchUpdateFactory, updatedBranches, orm);
     }
   }
 
@@ -136,7 +125,6 @@
   private final GitModules.Factory gitmodulesFactory;
   private final PersonIdent myIdent;
   private final ProjectCache projectCache;
-  private final ProjectState.Factory projectStateFactory;
   private final BatchUpdate.Factory batchUpdateFactory;
   private final VerboseSuperprojectUpdate verboseSuperProject;
   private final boolean enableSuperProjectSubscriptions;
@@ -163,7 +151,6 @@
       PersonIdent myIdent,
       Config cfg,
       ProjectCache projectCache,
-      ProjectState.Factory projectStateFactory,
       BatchUpdate.Factory batchUpdateFactory,
       Set<Branch.NameKey> updatedBranches,
       MergeOpRepoManager orm)
@@ -171,7 +158,6 @@
     this.gitmodulesFactory = gitmodulesFactory;
     this.myIdent = myIdent;
     this.projectCache = projectCache;
-    this.projectStateFactory = projectStateFactory;
     this.batchUpdateFactory = batchUpdateFactory;
     this.verboseSuperProject =
         cfg.getEnum("submodule", null, "verboseSuperprojectUpdate", VerboseSuperprojectUpdate.TRUE);
@@ -335,8 +321,7 @@
     logDebug("Calculating possible superprojects for " + srcBranch);
     Collection<SubmoduleSubscription> ret = new ArrayList<>();
     Project.NameKey srcProject = srcBranch.getParentKey();
-    ProjectConfig cfg = projectCache.get(srcProject).getConfig();
-    for (SubscribeSection s : projectStateFactory.create(cfg).getSubscribeSections(srcBranch)) {
+    for (SubscribeSection s : projectCache.get(srcProject).getSubscribeSections(srcBranch)) {
       logDebug("Checking subscribe section " + s);
       Collection<Branch.NameKey> branches = getDestinationBranches(srcBranch, s);
       for (Branch.NameKey targetBranch : branches) {
diff --git a/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java b/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
index e8b1861..644f1eb 100644
--- a/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
+++ b/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
@@ -14,21 +14,41 @@
 
 package com.google.gerrit.server.index.account;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexedQuery;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.query.DataSource;
+import com.google.gerrit.index.query.Matchable;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gwtorm.server.OrmException;
 
 public class IndexedAccountQuery extends IndexedQuery<Account.Id, AccountState>
-    implements DataSource<AccountState> {
+    implements DataSource<AccountState>, Matchable<AccountState> {
 
   public IndexedAccountQuery(
       Index<Account.Id, AccountState> index, Predicate<AccountState> pred, QueryOptions opts)
       throws QueryParseException {
     super(index, pred, opts.convertForBackend());
   }
+
+  @Override
+  public boolean match(AccountState accountState) throws OrmException {
+    Predicate<AccountState> pred = getChild(0);
+    checkState(
+        pred.isMatchable(),
+        "match invoked, but child predicate %s doesn't implement %s",
+        pred,
+        Matchable.class.getName());
+    return pred.asMatchable().match(accountState);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
 }
diff --git a/java/com/google/gerrit/server/git/ProjectLevelConfig.java b/java/com/google/gerrit/server/project/ProjectLevelConfig.java
similarity index 97%
rename from java/com/google/gerrit/server/git/ProjectLevelConfig.java
rename to java/com/google/gerrit/server/project/ProjectLevelConfig.java
index 2044db0..ee1bbef 100644
--- a/java/com/google/gerrit/server/git/ProjectLevelConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectLevelConfig.java
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.project;
 
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.git.VersionedMetaData;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Set;
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 48a8b9a..2ef7891 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -47,7 +47,6 @@
 import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.ProjectLevelConfig;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.rules.PrologEnvironment;
 import com.google.gerrit.server.rules.RulesCache;
diff --git a/java/com/google/gerrit/server/query/account/AccountPredicates.java b/java/com/google/gerrit/server/query/account/AccountPredicates.java
index e643470..acb963c 100644
--- a/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -19,12 +19,15 @@
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.IndexPredicate;
+import com.google.gerrit.index.query.Matchable;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryBuilder;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.index.account.AccountField;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gwtorm.server.OrmException;
 import java.util.List;
 
 public class AccountPredicates {
@@ -121,7 +124,14 @@
     return new AccountPredicate(AccountField.WATCHED_PROJECT, project.get());
   }
 
-  static class AccountPredicate extends IndexPredicate<AccountState> {
+  public static Predicate<AccountState> cansee(
+      AccountQueryBuilder.Arguments args, ChangeNotes changeNotes) {
+    return new CanSeeChangePredicate(
+        args.db, args.permissionBackend, args.userFactory, changeNotes);
+  }
+
+  static class AccountPredicate extends IndexPredicate<AccountState>
+      implements Matchable<AccountState> {
     AccountPredicate(FieldDef<AccountState, ?> def, String value) {
       super(def, value);
     }
@@ -129,6 +139,16 @@
     AccountPredicate(FieldDef<AccountState, ?> def, String name, String value) {
       super(def, name, value);
     }
+
+    @Override
+    public boolean match(AccountState object) throws OrmException {
+      return true;
+    }
+
+    @Override
+    public int getCost() {
+      return 1;
+    }
   }
 
   private AccountPredicates() {}
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index 8f6ec8b..8b6e1e4 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -25,14 +25,19 @@
 import com.google.gerrit.index.query.QueryBuilder;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeFinder;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -56,17 +61,27 @@
       new QueryBuilder.Definition<>(AccountQueryBuilder.class);
 
   public static class Arguments {
+    final Provider<ReviewDb> db;
+    final ChangeFinder changeFinder;
+    final IdentifiedUser.GenericFactory userFactory;
+    final PermissionBackend permissionBackend;
+
     private final Provider<CurrentUser> self;
     private final AccountIndexCollection indexes;
-    private final PermissionBackend permissionBackend;
 
     @Inject
     public Arguments(
         Provider<CurrentUser> self,
         AccountIndexCollection indexes,
+        Provider<ReviewDb> db,
+        ChangeFinder changeFinder,
+        IdentifiedUser.GenericFactory userFactory,
         PermissionBackend permissionBackend) {
       this.self = self;
       this.indexes = indexes;
+      this.db = db;
+      this.changeFinder = changeFinder;
+      this.userFactory = userFactory;
       this.permissionBackend = permissionBackend;
     }
 
@@ -105,6 +120,22 @@
   }
 
   @Operator
+  public Predicate<AccountState> cansee(String change)
+      throws QueryParseException, OrmException, PermissionBackendException {
+    ChangeNotes changeNotes = args.changeFinder.findOne(change);
+    if (changeNotes == null
+        || !args.permissionBackend
+            .user(args.getUser())
+            .database(args.db)
+            .change(changeNotes)
+            .test(ChangePermission.READ)) {
+      throw error(String.format("change %s not found", change));
+    }
+
+    return AccountPredicates.cansee(args, changeNotes);
+  }
+
+  @Operator
   public Predicate<AccountState> email(String email)
       throws PermissionBackendException, QueryParseException {
     if (canSeeSecondaryEmails()) {
@@ -167,6 +198,14 @@
   protected Predicate<AccountState> defaultField(String query) {
     Predicate<AccountState> defaultPredicate =
         AccountPredicates.defaultPredicate(args.schema(), checkedCanSeeSecondaryEmails(), query);
+    if (query.startsWith("cansee:")) {
+      try {
+        return cansee(query.substring(7));
+      } catch (OrmException | QueryParseException | PermissionBackendException e) {
+        // Ignore, fall back to default query
+      }
+    }
+
     if ("self".equalsIgnoreCase(query) || "me".equalsIgnoreCase(query)) {
       try {
         return Predicate.or(defaultPredicate, AccountPredicates.id(self()));
diff --git a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
new file mode 100644
index 0000000..f8b8cc7
--- /dev/null
+++ b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
@@ -0,0 +1,71 @@
+package com.google.gerrit.server.query.account;
+
+import com.google.gerrit.index.query.PostFilterPredicate;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+import java.util.Collection;
+import java.util.Objects;
+
+public class CanSeeChangePredicate extends PostFilterPredicate<AccountState> {
+  private final Provider<ReviewDb> db;
+  private final PermissionBackend permissionBackend;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final ChangeNotes changeNotes;
+
+  CanSeeChangePredicate(
+      Provider<ReviewDb> db,
+      PermissionBackend permissionBackend,
+      IdentifiedUser.GenericFactory userFactory,
+      ChangeNotes changeNotes) {
+    this.db = db;
+    this.permissionBackend = permissionBackend;
+    this.userFactory = userFactory;
+    this.changeNotes = changeNotes;
+  }
+
+  @Override
+  public boolean match(AccountState accountState) throws OrmException {
+    try {
+      return permissionBackend
+          .user(userFactory.create(accountState.getAccount().getId()))
+          .database(db)
+          .change(changeNotes)
+          .test(ChangePermission.READ);
+    } catch (PermissionBackendException e) {
+      throw new OrmException("Failed to check if account can see change", e);
+    }
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+
+  @Override
+  public Predicate<AccountState> copy(Collection<? extends Predicate<AccountState>> children) {
+    return new CanSeeChangePredicate(db, permissionBackend, userFactory, changeNotes);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(changeNotes.getChange().getChangeId());
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other == null) {
+      return false;
+    }
+    return getClass() == other.getClass()
+        && changeNotes.getChange().getChangeId()
+            == ((CanSeeChangePredicate) other).changeNotes.getChange().getChangeId();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 019338e..e930f11 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -19,14 +19,24 @@
 import static java.util.stream.Collectors.toList;
 import static org.junit.Assert.fail;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
 import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.access.AccessSectionInfo;
+import com.google.gerrit.extensions.api.access.PermissionInfo;
+import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
+import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.api.accounts.Accounts.QueryRequest;
+import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.common.AccountExternalIdInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -76,6 +86,7 @@
 import com.google.inject.util.Providers;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
@@ -396,6 +407,22 @@
   }
 
   @Test
+  public void byCansee() throws Exception {
+    String domain = name("test.com");
+    AccountInfo user1 = newAccountWithEmail("account1", "account1@" + domain);
+    AccountInfo user2 = newAccountWithEmail("account2", "account2@" + domain);
+    AccountInfo user3 = newAccountWithEmail("account3", "account3@" + domain);
+
+    Project.NameKey p = createProject(name("p"));
+    ChangeInfo c = createChange(p);
+    assertQuery("name:" + domain + " cansee:" + c.changeId, user1, user2, user3);
+
+    GroupInfo group = createGroup(name("group"), user1, user2);
+    blockRead(p, group);
+    assertQuery("name:" + domain + " cansee:" + c.changeId, user3);
+  }
+
+  @Test
   public void byWatchedProject() throws Exception {
     Project.NameKey p = createProject(name("p"));
     Project.NameKey p2 = createProject(name("p2"));
@@ -617,10 +644,43 @@
   }
 
   protected Project.NameKey createProject(String name) throws RestApiException {
-    gApi.projects().create(name);
+    ProjectInput in = new ProjectInput();
+    in.name = name;
+    in.createEmptyCommit = true;
+    gApi.projects().create(in);
     return new Project.NameKey(name);
   }
 
+  protected void blockRead(Project.NameKey project, GroupInfo group) throws RestApiException {
+    ProjectAccessInput in = new ProjectAccessInput();
+    in.add = new HashMap<>();
+
+    AccessSectionInfo a = new AccessSectionInfo();
+    PermissionInfo p = new PermissionInfo(null, null);
+    p.rules =
+        ImmutableMap.of(group.id, new PermissionRuleInfo(PermissionRuleInfo.Action.BLOCK, false));
+    a.permissions = ImmutableMap.of("read", p);
+    in.add = ImmutableMap.of("refs/*", a);
+
+    gApi.projects().name(project.get()).access(in);
+  }
+
+  protected ChangeInfo createChange(Project.NameKey project) throws RestApiException {
+    ChangeInput in = new ChangeInput();
+    in.subject = "A change";
+    in.project = project.get();
+    in.branch = "master";
+    return gApi.changes().create(in).get();
+  }
+
+  protected GroupInfo createGroup(String name, AccountInfo... members) throws RestApiException {
+    GroupInput in = new GroupInput();
+    in.name = name;
+    in.members =
+        Arrays.asList(members).stream().map(a -> String.valueOf(a._accountId)).collect(toList());
+    return gApi.groups().create(in).get();
+  }
+
   protected void watch(AccountInfo account, Project.NameKey project, String filter)
       throws RestApiException {
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
diff --git a/javatests/com/google/gerrit/server/query/account/BUILD b/javatests/com/google/gerrit/server/query/account/BUILD
index e0b59d5..497fc22 100644
--- a/javatests/com/google/gerrit/server/query/account/BUILD
+++ b/javatests/com/google/gerrit/server/query/account/BUILD
@@ -19,6 +19,7 @@
         "//lib:truth-java8-extension",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
+        "//prolog:gerrit-prolog-common",
     ],
 )
 
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 3489db0..53f9ef5 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
@@ -285,10 +285,13 @@
     },
 
     _handleRKey(e) {
-      if (this.shouldSuppressKeyboardShortcut(e) ||
-          this.modifierPressed(e)) { return; }
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
 
       e.preventDefault();
+      this._reloadWindow();
+    },
+
+    _reloadWindow() {
       window.location.reload();
     },
 
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 aed48e3..265e34e 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
@@ -190,6 +190,10 @@
         MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
         assert.equal(element.selectedIndex, 0);
 
+        const reloadStub = sandbox.stub(element, '_reloadWindow');
+        MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
+        assert.isTrue(reloadStub.called);
+
         done();
       });
     });
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index 0898a60..7c6032d 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -123,6 +123,7 @@
       }
       .expanded .author {
         cursor: pointer;
+        margin-bottom: .4em;
       }
       .date {
         color: #666;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
index 9b830cf..c4c5ee6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -101,8 +101,7 @@
               id="lineWrappingInput"
               on-tap="_handlelineWrappingTap">
         </div>
-        <div class="pref" id="columnsPref"
-            hidden$="[[_newPrefs.line_wrapping]]">
+        <div class="pref" id="columnsPref">
           <label for="columnsInput">Diff width</label>
           <input is="iron-input" type="number" id="columnsInput"
               prevent-invalid-input
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
index f06cd3a..f157640 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
@@ -77,18 +77,6 @@
       assert.isFalse(element._newPrefs.syntax_highlighting);
     });
 
-    test('clicking fit to screen hides line length input', () => {
-      element.prefs = {line_wrapping: false};
-
-      assert.isFalse(element.$.columnsPref.hidden);
-
-      MockInteractions.tap(element.$.lineWrappingInput);
-      assert.isTrue(element.$.columnsPref.hidden);
-
-      MockInteractions.tap(element.$.lineWrappingInput);
-      assert.isFalse(element.$.columnsPref.hidden);
-    });
-
     test('clicking save button calls _handleSave function', () => {
       const savePrefs = sinon.stub(element, '_handleSave');
       MockInteractions.tap(element.$.saveButton);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 5ba62af..c166695 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -239,6 +239,19 @@
         overflow: hidden;
         width: 200px;
       }
+      /** Since the line limit position is determined by charachter size, blank
+       lines also need to have the same font size as everything else */
+      .full-width .blank {
+        font-size: var(--font-size, var(--font-size-small));
+      }
+      /** Support the line length indicator **/
+      .full-width td.content,
+      .full-width td.blank {
+        /* Base 64 encoded 1x1px of #ddd */
+        background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mO8+x8AAr8B3gzOjaQAAAAASUVORK5CYII=');
+        background-position: var(--line-limit) 0;
+        background-repeat: repeat-y;
+      }
     </style>
     <style include="gr-theme-default"></style>
     <div id="diffHeader" hidden$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 19b3c6d..987c108 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -623,6 +623,7 @@
         this._diffTableClass = 'full-width';
         if (this.viewMode === 'SIDE_BY_SIDE') {
           stylesToUpdate['--content-width'] = 'none';
+          stylesToUpdate['--line-limit'] = prefs.line_length + 'ch';
         }
       } else {
         this._diffTableClass = '';
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 7526141..e2e9c84 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -70,6 +70,20 @@
       assert.equal(element._diffLength(mock.diffResponse), 52);
     });
 
+    test('line limit with line_wrapping', () => {
+      element = fixture('basic');
+      element.prefs = {line_wrapping: true, line_length: 80, tab_size: 2};
+      flushAsynchronousOperations();
+      assert.equal(element.customStyle['--line-limit'], '80ch');
+    });
+
+    test('line limit without line_wrapping', () => {
+      element = fixture('basic');
+      element.prefs = {line_wrapping: false, line_length: 80, tab_size: 2};
+      flushAsynchronousOperations();
+      assert.isNotOk(element.customStyle['--line-limit']);
+    });
+
     suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => {
       let lineEl;
       let contentEl;