Merge "OnlineNoteDbMigrationIT: Use Java 8 streamified Files#walk"
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
index 8a479dd..de25ef0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.api;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
@@ -28,7 +30,6 @@
 import com.google.gwt.user.client.ui.DialogBox;
 import com.google.gwtexpui.progress.client.ProgressBar;
 import java.util.List;
-import java.util.stream.Collectors;
 
 /** Loads JavaScript plugins with a progress meter visible. */
 public class PluginLoader extends DialogBox {
@@ -39,7 +40,7 @@
     if (plugins == null || plugins.isEmpty()) {
       callback.onSuccess(VoidResult.create());
     } else {
-      plugins = plugins.stream().filter(p -> p.endsWith(".js")).collect(Collectors.toList());
+      plugins = plugins.stream().filter(p -> p.endsWith(".js")).collect(toList());
       if (plugins.isEmpty()) {
         callback.onSuccess(VoidResult.create());
       } else {
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index f42b622..ab887fe 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.extensions.api.changes.RecipientType.BCC;
 import static com.google.gerrit.extensions.api.changes.RecipientType.CC;
 import static com.google.gerrit.extensions.api.changes.RecipientType.TO;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
@@ -48,7 +49,6 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.junit.TestRepository;
 import org.junit.After;
 import org.junit.Before;
@@ -121,7 +121,7 @@
               .stream()
               .map(Address::getEmail)
               .filter(e -> !recipients.get(TO).contains(e) && !recipients.get(CC).contains(e))
-              .collect(Collectors.toList()));
+              .collect(toList()));
       this.users = users;
       if (!message.headers().containsKey("X-Gerrit-MessageType")) {
         fail("a message was sent with X-Gerrit-MessageType header");
@@ -162,7 +162,7 @@
       }
       Truth.assertThat(header).isInstanceOf(AddressList.class);
       AddressList addrList = (AddressList) header;
-      return addrList.getAddressList().stream().map(Address::getEmail).collect(Collectors.toList());
+      return addrList.getAddressList().stream().map(Address::getEmail).collect(toList());
     }
 
     public FakeEmailSenderSubject to(String... emails) {
diff --git a/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java b/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java
index 58a9238..7f932c3 100644
--- a/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java
@@ -20,12 +20,16 @@
 import com.google.inject.Inject;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
 
 public class LightweightPluginDaemonTest extends AbstractDaemonTest {
   @Inject private PluginGuiceEnvironment env;
 
   @Inject private PluginUser.Factory pluginUserFactory;
 
+  @Rule public TemporaryFolder tempDataDir = new TemporaryFolder();
+
   private TestServerPlugin plugin;
 
   @Before
@@ -40,7 +44,8 @@
             getClass().getClassLoader(),
             testPlugin.sysModule(),
             testPlugin.httpModule(),
-            testPlugin.sshModule());
+            testPlugin.sshModule(),
+            tempDataDir.getRoot().toPath());
 
     plugin.start(env);
     env.onStartPlugin(plugin);
diff --git a/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java b/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java
index 3fc786b..1e5088be 100644
--- a/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java
+++ b/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.fixes;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.stream.Collectors.groupingBy;
 
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -33,7 +34,6 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 
@@ -73,9 +73,7 @@
     checkNotNull(fixReplacements, "Fix replacements must not be null");
 
     Map<String, List<FixReplacement>> fixReplacementsPerFilePath =
-        fixReplacements
-            .stream()
-            .collect(Collectors.groupingBy(fixReplacement -> fixReplacement.path));
+        fixReplacements.stream().collect(groupingBy(fixReplacement -> fixReplacement.path));
 
     List<TreeModification> treeModifications = new ArrayList<>();
     for (Map.Entry<String, List<FixReplacement>> entry : fixReplacementsPerFilePath.entrySet()) {
diff --git a/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java b/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
index c76a59a..3a954fb 100644
--- a/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
+++ b/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git.strategy;
 
+import static java.util.stream.Collectors.toSet;
+
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Streams;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -31,7 +33,6 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -68,7 +69,7 @@
             repo.getRefDatabase().getRefs(Constants.R_TAGS).values().stream())
         .map(Ref::getObjectId)
         .filter(o -> o != null)
-        .collect(Collectors.toSet());
+        .collect(toSet());
   }
 
   public static Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw) throws IOException {
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index c528f8e..e57f5ce 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.util.stream.Collectors.joining;
 
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
@@ -38,7 +39,6 @@
 import java.util.StringJoiner;
 import java.util.function.Function;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -307,7 +307,7 @@
   private <E> void saveToFile(
       String filePath, ImmutableSet<E> elements, Function<E, String> toStringFunction)
       throws IOException {
-    String fileContent = elements.stream().map(toStringFunction).collect(Collectors.joining("\n"));
+    String fileContent = elements.stream().map(toStringFunction).collect(joining("\n"));
     saveUTF8(filePath, fileContent);
   }
 
diff --git a/java/com/google/gerrit/server/mail/ListMailFilter.java b/java/com/google/gerrit/server/mail/ListMailFilter.java
index a88a0e4..21347cb 100644
--- a/java/com/google/gerrit/server/mail/ListMailFilter.java
+++ b/java/com/google/gerrit/server/mail/ListMailFilter.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.server.mail;
 
+import static java.util.stream.Collectors.joining;
+
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.mail.receive.MailMessage;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Arrays;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -42,7 +43,7 @@
   ListMailFilter(@GerritServerConfig Config cfg) {
     this.mode = cfg.getEnum("receiveemail", "filter", "mode", ListFilterMode.OFF);
     String[] addresses = cfg.getStringList("receiveemail", "filter", "patterns");
-    String concat = Arrays.asList(addresses).stream().collect(Collectors.joining("|"));
+    String concat = Arrays.asList(addresses).stream().collect(joining("|"));
     this.mailPattern = Pattern.compile(concat);
   }
 
diff --git a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
index 3264be2..db7b86a 100644
--- a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
+++ b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.reviewdb.client.PatchLineComment.Status.PUBLISHED;
 import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -31,7 +32,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
@@ -139,7 +139,7 @@
         .values()
         .stream()
         .flatMap(n -> n.getComments().stream())
-        .collect(Collectors.toMap(c -> c.key.uuid, c -> c));
+        .collect(toMap(c -> c.key.uuid, c -> c));
   }
 
   /**
diff --git a/java/com/google/gerrit/server/patch/IntraLineDiff.java b/java/com/google/gerrit/server/patch/IntraLineDiff.java
index ee8b88b..a182335 100644
--- a/java/com/google/gerrit/server/patch/IntraLineDiff.java
+++ b/java/com/google/gerrit/server/patch/IntraLineDiff.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeEnum;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.reviewdb.client.CodedEnum;
@@ -30,7 +31,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.ReplaceEdit;
 
@@ -109,7 +109,7 @@
         for (int j = 0; j < innerCount; j++) {
           inner[j] = readEdit(in);
         }
-        editArray[i] = new ReplaceEdit(editArray[i], toList(inner));
+        editArray[i] = new ReplaceEdit(editArray[i], asList(inner));
       }
     }
     edits = ImmutableList.copyOf(editArray);
@@ -128,7 +128,7 @@
 
   private static ReplaceEdit copy(ReplaceEdit edit) {
     List<Edit> internalEdits =
-        edit.getInternalEdits().stream().map(IntraLineDiff::copy).collect(Collectors.toList());
+        edit.getInternalEdits().stream().map(IntraLineDiff::copy).collect(toList());
     return new ReplaceEdit(
         edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB(), internalEdits);
   }
@@ -148,7 +148,7 @@
     return new Edit(beginA, endA, beginB, endB);
   }
 
-  private static List<Edit> toList(Edit[] l) {
+  private static List<Edit> asList(Edit[] l) {
     return Collections.unmodifiableList(Arrays.asList(l));
   }
 }
diff --git a/java/com/google/gerrit/server/plugins/ListPlugins.java b/java/com/google/gerrit/server/plugins/ListPlugins.java
index 9a42bb9..989e839 100644
--- a/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.plugins;
 
 import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toMap;
 
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -30,7 +31,6 @@
 import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.kohsuke.args4j.Option;
 
@@ -137,7 +137,7 @@
     if (limit > 0) {
       s = s.limit(limit);
     }
-    return new TreeMap<>(s.collect(Collectors.toMap(p -> p.getName(), p -> toPluginInfo(p))));
+    return new TreeMap<>(s.collect(toMap(p -> p.getName(), p -> toPluginInfo(p))));
   }
 
   private void checkMatchOptions(boolean cond) throws BadRequestException {
diff --git a/java/com/google/gerrit/server/plugins/TestServerPlugin.java b/java/com/google/gerrit/server/plugins/TestServerPlugin.java
index 6897b9a..dbdc576 100644
--- a/java/com/google/gerrit/server/plugins/TestServerPlugin.java
+++ b/java/com/google/gerrit/server/plugins/TestServerPlugin.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.plugins;
 
 import com.google.gerrit.server.PluginUser;
+import java.nio.file.Path;
 
 public class TestServerPlugin extends ServerPlugin {
   private final ClassLoader classLoader;
@@ -29,9 +30,10 @@
       ClassLoader classloader,
       String sysName,
       String httpName,
-      String sshName)
+      String sshName,
+      Path dataDir)
       throws InvalidPluginException {
-    super(name, pluginCanonicalWebUrl, user, null, null, null, null, classloader);
+    super(name, pluginCanonicalWebUrl, user, null, null, null, dataDir, classloader);
     this.classLoader = classloader;
     this.sysName = sysName;
     this.httpName = httpName;
diff --git a/java/com/google/gerrit/server/project/ProjectJson.java b/java/com/google/gerrit/server/project/ProjectJson.java
index 6f15c7d..f2a93d3 100644
--- a/java/com/google/gerrit/server/project/ProjectJson.java
+++ b/java/com/google/gerrit/server/project/ProjectJson.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import static java.util.stream.Collectors.toMap;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelValue;
@@ -28,7 +30,6 @@
 import com.google.inject.Singleton;
 import java.util.HashMap;
 import java.util.List;
-import java.util.stream.Collectors;
 
 @Singleton
 public class ProjectJson {
@@ -48,9 +49,7 @@
     for (LabelType t : projectState.getLabelTypes().getLabelTypes()) {
       LabelTypeInfo labelInfo = new LabelTypeInfo();
       labelInfo.values =
-          t.getValues()
-              .stream()
-              .collect(Collectors.toMap(LabelValue::formatValue, LabelValue::getText));
+          t.getValues().stream().collect(toMap(LabelValue::formatValue, LabelValue::getText));
       labelInfo.defaultValue = t.getDefaultValue();
       info.labels.put(t.getName(), labelInfo);
     }
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index bcfd53c..ebb3e65 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -89,7 +89,6 @@
 import java.util.Set;
 import java.util.function.Function;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
@@ -1244,7 +1243,7 @@
     }
 
     List<Predicate<ChangeData>> predicates =
-        parts.stream().map(fullPredicateFunc).collect(Collectors.toList());
+        parts.stream().map(fullPredicateFunc).collect(toList());
     return Predicate.and(predicates);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 2b1b313..cc4e2a7 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -49,6 +49,7 @@
 import static com.google.gerrit.server.project.testing.Util.value;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
@@ -154,7 +155,6 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -461,7 +461,7 @@
     assertThat(result.reviewers).isNotEmpty();
     ChangeInfo info = gApi.changes().id(changeId).get();
     Function<Collection<AccountInfo>, Collection<String>> toEmails =
-        ais -> ais.stream().map(ai -> ai.email).collect(Collectors.toSet());
+        ais -> ais.stream().map(ai -> ai.email).collect(toSet());
     assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
         .containsExactly(
             admin.email, user1.email, user2.email, "byemail1@example.com", "byemail2@example.com");
@@ -3230,7 +3230,7 @@
   @Test
   public void putTopicExceedLimitFails() throws Exception {
     String changeId = createChange().getChangeId();
-    String topic = Stream.generate(() -> "t").limit(2049).collect(Collectors.joining());
+    String topic = Stream.generate(() -> "t").limit(2049).collect(joining());
 
     exception.expect(BadRequestException.class);
     exception.expectMessage("topic length exceeds the limit");
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index f3e9a19..060cef5 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -19,6 +19,8 @@
 import static com.google.gerrit.extensions.common.testing.DiffInfoSubject.assertThat;
 import static com.google.gerrit.extensions.common.testing.FileInfoSubject.assertThat;
 import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toMap;
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableMap;
@@ -44,7 +46,6 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import javax.imageio.ImageIO;
 import org.eclipse.jgit.lib.Config;
@@ -64,7 +65,7 @@
   private static final String FILE_CONTENT =
       IntStream.rangeClosed(1, 100)
           .mapToObj(number -> String.format("Line %d\n", number))
-          .collect(Collectors.joining());
+          .collect(joining());
   private static final String FILE_CONTENT2 = "1st line\n2nd line\n3rd line\n";
 
   private boolean intraline;
@@ -1286,7 +1287,7 @@
     testRepo.reset(parentCommit);
     Map<String, String> files =
         Arrays.stream(removedFilePaths)
-            .collect(Collectors.toMap(Function.identity(), path -> "Irrelevant content"));
+            .collect(toMap(Function.identity(), path -> "Irrelevant content"));
     PushOneCommit push =
         pushFactory.create(db, admin.getIdent(), testRepo, "Remove files from repo", files);
     PushOneCommit.Result result = push.rm("refs/for/master");
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 7b86a96..9670b34 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -93,7 +93,6 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.junit.TestRepository;
@@ -353,14 +352,14 @@
 
   @Test
   public void pushForMasterWithTopicInRefExceedLimitFails() throws Exception {
-    String topic = Stream.generate(() -> "t").limit(2049).collect(Collectors.joining());
+    String topic = Stream.generate(() -> "t").limit(2049).collect(joining());
     PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
     r.assertErrorStatus("topic length exceeds the limit (2048)");
   }
 
   @Test
   public void pushForMasterWithTopicAsOptionExceedLimitFails() throws Exception {
-    String topic = Stream.generate(() -> "t").limit(2049).collect(Collectors.joining());
+    String topic = Stream.generate(() -> "t").limit(2049).collect(joining());
     PushOneCommit.Result r = pushTo("refs/for/master%topic=" + topic);
     r.assertErrorStatus("topic length exceeds the limit (2048)");
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 4c5f231..7b36126 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -18,6 +18,8 @@
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
@@ -68,7 +70,6 @@
 import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -962,7 +963,7 @@
         .values()
         .stream()
         .flatMap(List::stream)
-        .collect(Collectors.toList());
+        .collect(toList());
   }
 
   private CommentInput addComment(String changeId, String message) throws Exception {
@@ -976,7 +977,7 @@
   private void addComments(String changeId, String revision, CommentInput... commentInputs)
       throws Exception {
     ReviewInput input = new ReviewInput();
-    input.comments = Arrays.stream(commentInputs).collect(Collectors.groupingBy(c -> c.path));
+    input.comments = Arrays.stream(commentInputs).collect(groupingBy(c -> c.path));
     gApi.changes().id(changeId).revision(revision).review(input);
   }
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
index 152ef3d..27d3a42 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -22,6 +22,7 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-change-list-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
+<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-change-list-item/gr-change-list-item.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -101,11 +102,17 @@
               visible-change-table-columns="[[visibleChangeTableColumns]]"
               show-number="[[showNumber]]"
               show-star="[[showStar]]"
+              tabindex="0"
               label-names="[[labelNames]]"></gr-change-list-item>
         </template>
       </template>
     </table>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-cursor-manager
+        id="cursor"
+        index="{{selectedIndex}}"
+        scroll-behavior="keep-visible"
+        focus-on-move></gr-cursor-manager>
   </template>
   <script src="gr-change-list.js"></script>
 </dom-module>
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 262d1dc..f66d8c8 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
@@ -113,6 +113,10 @@
       keydown: '_scopedKeydownHandler',
     },
 
+    observers: [
+      '_sectionsChanged(sections.*)',
+    ],
+
     /**
      * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
      * events must be scoped to a component level (e.g. `enter`) in order to not
@@ -194,7 +198,7 @@
     },
 
     _sectionHref(query) {
-      return `${this.getBaseUrl()}/q/${this.encodeURL(query, true)}`;
+      return Gerrit.Nav.getUrlForSearchQuery(query);
     },
 
     /**
@@ -234,10 +238,7 @@
           this.modifierPressed(e)) { return; }
 
       e.preventDefault();
-      // Compute absolute index of item that would come after final item.
-      const len = this._computeItemAbsoluteIndex(this.sections.length, 0);
-      if (this.selectedIndex === len - 1) { return; }
-      this.selectedIndex += 1;
+      this.$.cursor.next();
     },
 
     _handleKKey(e) {
@@ -245,8 +246,7 @@
           this.modifierPressed(e)) { return; }
 
       e.preventDefault();
-      if (this.selectedIndex === 0) { return; }
-      this.selectedIndex -= 1;
+      this.$.cursor.previous();
     },
 
     _handleOKey(e) {
@@ -317,5 +317,12 @@
     _getListItems() {
       return Polymer.dom(this.root).querySelectorAll('gr-change-list-item');
     },
+
+    _sectionsChanged() {
+      // Flush DOM operations so that the list item elements will be loaded.
+      Polymer.dom.flush();
+      this.$.cursor.stops = this._getListItems();
+      this.$.cursor.moveToStart();
+    },
   });
 })();
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 bded5f6..7b9fadf 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
@@ -473,16 +473,6 @@
       }
     });
 
-    test('_sectionHref', () => {
-      assert.equal(
-          element._sectionHref('is:open owner:self'),
-          '/q/is:open+owner:self');
-      assert.equal(
-          element._sectionHref(
-              'is:open ((reviewer:self -is:ignored) OR assignee:self)'),
-          '/q/is:open+((reviewer:self+-is:ignored)+OR+assignee:self)');
-    });
-
     test('_computeItemAbsoluteIndex', () => {
       sandbox.stub(element, '_computeLabelNames');
       element.sections = [
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index f30cc1c..b59ad9d 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -26,6 +26,9 @@
     //    - `changeNum`, required, String: the numeric ID of the change.
     //
     // - Gerrit.Nav.View.SEARCH:
+    //    - `query`, optional, String: the literal search query. If provided,
+    //        the string will be used as the query, and all other params will be
+    //        ignored.
     //    - `owner`, optional, String: the owner name.
     //    - `project`, optional, String: the project name.
     //    - `branch`, optional, String: the branch name.
@@ -136,6 +139,13 @@
         return this._generateUrl(params);
       },
 
+      getUrlForSearchQuery(query) {
+        return this._getUrlFor({
+          view: Gerrit.Nav.View.SEARCH,
+          query,
+        });
+      },
+
       /**
        * @param {!string} project The name of the project.
        * @param {boolean=} opt_openOnly When true, only search open changes in
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 60fd109..5d26cb6 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -322,6 +322,10 @@
      * @return {string}
      */
     _generateSearchUrl(params) {
+      if (params.query) {
+        return '/q/' + this.encodeURL(params.query, true);
+      }
+
       const operators = [];
       if (params.owner) {
         operators.push('owner:' + this.encodeURL(params.owner, false));
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index 909235b..7159e0a 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -226,6 +226,10 @@
             '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
             'topic:"g%2525h"+status:op%2525en');
 
+        // The presence of the query param overrides other params.
+        params.query = 'foo$bar';
+        assert.equal(element._generateUrl(params), '/q/foo%2524bar');
+
         params = {
           view: Gerrit.Nav.View.SEARCH,
           statuses: ['a', 'b', 'c'],