Merge "Show "File X of Y" next to file navigation links in diff view"
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
index baf08e7..2996a98 100755
--- a/Documentation/replace_macros.py
+++ b/Documentation/replace_macros.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python2
 # coding=utf-8
 # Copyright (C) 2013 The Android Open Source Project
 #
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
index 4b928f3..3c922ed 100644
--- a/Documentation/user-notify.txt
+++ b/Documentation/user-notify.txt
@@ -244,6 +244,17 @@
 been posted in that notification using "Yes" or "No", for
 example `Gerrit-HasLabels: No`.
 
+[[Gerrit-Comment-In-Reply-To]]Gerrit-Comment-In-Reply-To::
+
+In comment emails, a comment-in-reply-to footer is present for each
+account who has a comment that is replied-to in that set of comments.
+For example, to apply a filter to Gerrit messages in which your own diff
+comments are responded to, you might search for the following:
+
+----
+  Gerrit-Comment-In-Reply-To: User Name <user@example.com>
+----
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 31b601f..db673dd 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -18,7 +18,6 @@
 import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.acceptance.GitUtil.initSsh;
 import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
 import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
 import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST;
@@ -129,6 +128,7 @@
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.jcraft.jsch.JSchException;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -201,8 +201,10 @@
               }
               beforeTest(description);
               try (ProjectResetter resetter = resetProjects(projectResetter.builder())) {
+                AbstractDaemonTest.this.resetter = resetter;
                 base.evaluate();
               } finally {
+                AbstractDaemonTest.this.resetter = null;
                 afterTest();
               }
             }
@@ -267,6 +269,7 @@
   @Inject private SchemaFactory<ReviewDb> reviewDbProvider;
   @Inject private Groups groups;
 
+  private ProjectResetter resetter;
   private List<Repository> toClose;
 
   @Before
@@ -330,6 +333,16 @@
         .build();
   }
 
+  protected void restartAsSlave() throws Exception {
+    closeSsh();
+    server = GerritServer.restartAsSlave(server);
+    server.getTestInjector().injectMembers(this);
+    if (resetter != null) {
+      server.getTestInjector().injectMembers(resetter);
+    }
+    initSsh();
+  }
+
   protected static Config submitWholeTopicEnabledConfig() {
     Config cfg = new Config();
     cfg.setBoolean("change", null, "submitWholeTopic", true);
@@ -396,20 +409,7 @@
     adminRestSession = new RestSession(server, admin);
     userRestSession = new RestSession(server, user);
 
-    if (testRequiresSsh
-        && SshMode.useSsh()
-        && (adminSshSession == null || userSshSession == null)) {
-      // Create Ssh sessions
-      initSsh(admin);
-      Context ctx = newRequestContext(user);
-      atrScope.set(ctx);
-      userSshSession = ctx.getSession();
-      userSshSession.open();
-      ctx = newRequestContext(admin);
-      atrScope.set(ctx);
-      adminSshSession = ctx.getSession();
-      adminSshSession.open();
-    }
+    initSsh();
 
     resourcePrefix =
         UNSAFE_PROJECT_NAME
@@ -422,6 +422,23 @@
     testRepo = cloneProject(project, getCloneAsAccount(description));
   }
 
+  protected void initSsh() throws JSchException {
+    if (testRequiresSsh
+        && SshMode.useSsh()
+        && (adminSshSession == null || userSshSession == null)) {
+      // Create Ssh sessions
+      GitUtil.initSsh(admin);
+      Context ctx = newRequestContext(user);
+      atrScope.set(ctx);
+      userSshSession = ctx.getSession();
+      userSshSession.open();
+      ctx = newRequestContext(admin);
+      atrScope.set(ctx);
+      adminSshSession = ctx.getSession();
+      adminSshSession.open();
+    }
+  }
+
   private TestAccount getCloneAsAccount(Description description) {
     TestProjectInput ann = description.getAnnotation(TestProjectInput.class);
     return accountCreator.get(ann != null ? ann.cloneAs() : "admin");
@@ -557,12 +574,7 @@
       repo.close();
     }
     db.close();
-    if (adminSshSession != null) {
-      adminSshSession.close();
-    }
-    if (userSshSession != null) {
-      userSshSession.close();
-    }
+    closeSsh();
     if (server != commonServer) {
       server.close();
       server = null;
@@ -570,6 +582,17 @@
     NoteDbMode.resetFromEnv(notesMigration);
   }
 
+  protected void closeSsh() {
+    if (adminSshSession != null) {
+      adminSshSession.close();
+      adminSshSession = null;
+    }
+    if (userSshSession != null) {
+      userSshSession.close();
+      userSshSession = null;
+    }
+  }
+
   protected TestRepository<?>.CommitBuilder commitBuilder() throws Exception {
     return testRepo.branch("HEAD").commit().insertChangeId();
   }
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 1b9e8aa..5262b74 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
@@ -27,6 +28,7 @@
 import com.google.gerrit.pgm.Daemon;
 import com.google.gerrit.pgm.Init;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
 import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.server.util.ManualRequestContext;
@@ -35,6 +37,8 @@
 import com.google.gerrit.server.util.SystemLog;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.GroupNoteDbMode;
+import com.google.gerrit.testing.InMemoryDatabase;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.NoteDbChecker;
 import com.google.gerrit.testing.NoteDbMode;
 import com.google.gerrit.testing.SshMode;
@@ -261,7 +265,7 @@
       if (!desc.memory()) {
         init(desc, baseConfig, site);
       }
-      return start(desc, baseConfig, site, null);
+      return start(desc, baseConfig, site, null, null, null);
     } catch (Exception e) {
       TempFileUtil.recursivelyDelete(site.toFile());
       throw e;
@@ -278,6 +282,10 @@
    *     initialize this directory. Can be retrieved from the returned instance via {@link
    *     #getSitePath()}.
    * @param testSysModule optional additional module to add to the system injector.
+   * @param inMemoryRepoManager {@link InMemoryRepositoryManager} that should be used if the site is
+   *     started in memory
+   * @param inMemoryDatabaseInstance {@link com.google.gerrit.testing.InMemoryDatabase.Instance}
+   *     that should be used if the site is started in memory
    * @param additionalArgs additional command-line arguments for the daemon program; only allowed if
    *     the test is not in-memory.
    * @return started server.
@@ -288,6 +296,8 @@
       Config baseConfig,
       Path site,
       @Nullable Module testSysModule,
+      @Nullable InMemoryRepositoryManager inMemoryRepoManager,
+      @Nullable InMemoryDatabase.Instance inMemoryDatabaseInstance,
       String... additionalArgs)
       throws Exception {
     checkArgument(site != null, "site is required (even for in-memory server");
@@ -307,16 +317,24 @@
     daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
     daemon.setAdditionalSysModuleForTesting(testSysModule);
     daemon.setEnableSshd(desc.useSsh());
+    daemon.setSlave(baseConfig.getBoolean("container", "slave", false));
 
     if (desc.memory()) {
       checkArgument(additionalArgs.length == 0, "cannot pass args to in-memory server");
-      return startInMemory(desc, site, baseConfig, daemon);
+      return startInMemory(
+          desc, site, baseConfig, daemon, inMemoryRepoManager, inMemoryDatabaseInstance);
     }
     return startOnDisk(desc, site, daemon, serverStarted, additionalArgs);
   }
 
   private static GerritServer startInMemory(
-      Description desc, Path site, Config baseConfig, Daemon daemon) throws Exception {
+      Description desc,
+      Path site,
+      Config baseConfig,
+      Daemon daemon,
+      @Nullable InMemoryRepositoryManager inMemoryRepoManager,
+      @Nullable InMemoryDatabase.Instance inMemoryDatabaseInstance)
+      throws Exception {
     Config cfg = desc.buildConfig(baseConfig);
     mergeTestConfig(cfg);
     // Set the log4j configuration to an invalid one to prevent system logs
@@ -329,7 +347,9 @@
     daemon.setEnableHttpd(desc.httpd());
     daemon.setLuceneModule(LuceneIndexModule.singleVersionAllLatest(0));
     daemon.setDatabaseForTesting(
-        ImmutableList.<Module>of(new InMemoryTestingDatabaseModule(cfg, site)));
+        ImmutableList.<Module>of(
+            new InMemoryTestingDatabaseModule(
+                cfg, site, inMemoryRepoManager, inMemoryDatabaseInstance)));
     daemon.start();
     return new GerritServer(desc, null, createTestInjector(daemon), daemon, null);
   }
@@ -483,6 +503,40 @@
     return desc;
   }
 
+  public static GerritServer restartAsSlave(GerritServer server) throws Exception {
+    checkState(server.desc.sandboxed(), "restarting as slave requires @Sandboxed");
+
+    Path site = server.testInjector.getInstance(Key.get(Path.class, SitePath.class));
+
+    Config cfg = server.testInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
+    cfg.setBoolean("container", null, "slave", true);
+
+    InMemoryRepositoryManager inMemoryRepoManager = null;
+    if (hasBinding(server.testInjector, InMemoryRepositoryManager.class)) {
+      inMemoryRepoManager = server.testInjector.getInstance(InMemoryRepositoryManager.class);
+    }
+
+    InMemoryDatabase.Instance dbInstance = null;
+    if (hasBinding(server.testInjector, InMemoryDatabase.class)) {
+      InMemoryDatabase inMemoryDatabase = server.testInjector.getInstance(InMemoryDatabase.class);
+      dbInstance = inMemoryDatabase.getDbInstance();
+      dbInstance.setKeepOpen(true);
+    }
+    try {
+      server.close();
+      server.daemon.stop();
+      return start(server.desc, cfg, site, null, inMemoryRepoManager, dbInstance);
+    } finally {
+      if (dbInstance != null) {
+        dbInstance.setKeepOpen(false);
+      }
+    }
+  }
+
+  private static boolean hasBinding(Injector injector, Class<?> clazz) {
+    return injector.getExistingBinding(Key.get(clazz)) != null;
+  }
+
   @Override
   public void close() throws Exception {
     try {
diff --git a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index 4b1211b..58dfa94 100644
--- a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -16,6 +16,7 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.metrics.DisabledMetricMaker;
@@ -48,6 +49,7 @@
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
+import com.google.inject.util.Providers;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -58,10 +60,18 @@
 class InMemoryTestingDatabaseModule extends LifecycleModule {
   private final Config cfg;
   private final Path sitePath;
+  @Nullable private final InMemoryRepositoryManager repoManager;
+  @Nullable private final InMemoryDatabase.Instance inMemoryDatabaseInstance;
 
-  InMemoryTestingDatabaseModule(Config cfg, Path sitePath) {
+  InMemoryTestingDatabaseModule(
+      Config cfg,
+      Path sitePath,
+      @Nullable InMemoryRepositoryManager repoManager,
+      @Nullable InMemoryDatabase.Instance inMemoryDatabaseInstance) {
     this.cfg = cfg;
     this.sitePath = sitePath;
+    this.repoManager = repoManager;
+    this.inMemoryDatabaseInstance = inMemoryDatabaseInstance;
     makeSiteDirs(sitePath);
   }
 
@@ -72,8 +82,12 @@
     // TODO(dborowitz): Use jimfs.
     bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
 
-    bind(GitRepositoryManager.class).to(InMemoryRepositoryManager.class);
-    bind(InMemoryRepositoryManager.class).in(SINGLETON);
+    if (repoManager != null) {
+      bind(GitRepositoryManager.class).toInstance(repoManager);
+    } else {
+      bind(GitRepositoryManager.class).to(InMemoryRepositoryManager.class);
+      bind(InMemoryRepositoryManager.class).in(SINGLETON);
+    }
 
     bind(MetricMaker.class).to(DisabledMetricMaker.class);
     bind(DataSourceType.class).to(InMemoryH2Type.class);
@@ -83,6 +97,7 @@
     TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory =
         new TypeLiteral<SchemaFactory<ReviewDb>>() {};
     bind(schemaFactory).to(NotesMigrationSchemaFactory.class);
+    bind(InMemoryDatabase.Instance.class).toProvider(Providers.of(inMemoryDatabaseInstance));
     bind(Key.get(schemaFactory, ReviewDbFactory.class)).to(InMemoryDatabase.class);
     bind(InMemoryDatabase.class).in(SINGLETON);
     bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class);
@@ -132,7 +147,7 @@
 
     @Override
     public void stop() {
-      mem.drop();
+      mem.getDbInstance().drop();
     }
   }
 
diff --git a/java/com/google/gerrit/acceptance/ProjectResetter.java b/java/com/google/gerrit/acceptance/ProjectResetter.java
index 637fb2a..569a0c1 100644
--- a/java/com/google/gerrit/acceptance/ProjectResetter.java
+++ b/java/com/google/gerrit/acceptance/ProjectResetter.java
@@ -120,11 +120,12 @@
     }
   }
 
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsersName;
-  @Nullable private final AccountCreator accountCreator;
-  @Nullable private final AccountCache accountCache;
-  @Nullable private final ProjectCache projectCache;
+  @Inject private GitRepositoryManager repoManager;
+  @Inject private AllUsersName allUsersName;
+  @Inject @Nullable private AccountCreator accountCreator;
+  @Inject @Nullable private AccountCache accountCache;
+  @Inject @Nullable private ProjectCache projectCache;
+
   private final Multimap<Project.NameKey, String> refsPatternByProject;
   private final Multimap<Project.NameKey, RefState> savedRefStatesByProject;
 
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index 8790e78..09ffe9d 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -149,7 +149,7 @@
   private GerritServer startImpl(@Nullable Module testSysModule, String... additionalArgs)
       throws Exception {
     return GerritServer.start(
-        serverDesc, baseConfig, sitePaths.site_path, testSysModule, additionalArgs);
+        serverDesc, baseConfig, sitePaths.site_path, testSysModule, null, null, additionalArgs);
   }
 
   protected static void runGerrit(String... args) throws Exception {
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 1c0ce9c..3e7ea1b 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -222,6 +222,10 @@
     httpd = enable;
   }
 
+  public void setSlave(boolean slave) {
+    this.slave = slave;
+  }
+
   @Override
   public int run() throws Exception {
     if (stopOnly) {
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 0247fc3..5df0d62 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -243,6 +244,34 @@
     return groups;
   }
 
+  /** Get the set of accounts whose comments have been replied to in this email. */
+  private HashSet<Account.Id> getReplyAccounts() {
+    HashSet<Account.Id> replyAccounts = new HashSet<>();
+
+    // Track visited parent UUIDs to avoid cycles.
+    HashSet<String> visitedUuids = new HashSet<>();
+
+    for (Comment comment : inlineComments) {
+      visitedUuids.add(comment.key.uuid);
+
+      // Traverse the parent relation to the top of the comment thread.
+      Comment current = comment;
+      while (current.parentUuid != null && !visitedUuids.contains(current.parentUuid)) {
+        Optional<Comment> optParent = getParent(current);
+        if (!optParent.isPresent()) {
+          // There is a parent UUID, but it cannot be loaded, break from the comment thread.
+          break;
+        }
+
+        Comment parent = optParent.get();
+        replyAccounts.add(parent.author.getId());
+        visitedUuids.add(current.parentUuid);
+        current = parent;
+      }
+    }
+    return replyAccounts;
+  }
+
   private String getCommentLinePrefix(Comment comment) {
     int lineNbr = comment.range == null ? comment.lineNbr : comment.range.startLine;
     StringBuilder sb = new StringBuilder();
@@ -497,6 +526,10 @@
     footers.add("Gerrit-Comment-Date: " + getCommentTimestamp());
     footers.add("Gerrit-HasComments: " + (hasComments ? "Yes" : "No"));
     footers.add("Gerrit-HasLabels: " + (labels.isEmpty() ? "No" : "Yes"));
+
+    for (Account.Id account : getReplyAccounts()) {
+      footers.add("Gerrit-Comment-In-Reply-To: " + getNameEmailFor(account));
+    }
   }
 
   private String getLine(PatchFile fileInfo, short side, int lineNbr) {
diff --git a/java/com/google/gerrit/testing/InMemoryDatabase.java b/java/com/google/gerrit/testing/InMemoryDatabase.java
index ec98e16..a3d7c17 100644
--- a/java/com/google/gerrit/testing/InMemoryDatabase.java
+++ b/java/com/google/gerrit/testing/InMemoryDatabase.java
@@ -55,26 +55,16 @@
     return injector.getInstance(InMemoryDatabase.class);
   }
 
-  private static int dbCnt;
-
-  private static synchronized DataSource newDataSource() throws SQLException {
-    final Properties p = new Properties();
-    p.setProperty("driver", org.h2.Driver.class.getName());
-    p.setProperty("url", "jdbc:h2:mem:Test_" + (++dbCnt));
-    return new SimpleDataSource(p);
-  }
-
   /** Drop the database from memory; does nothing if the instance was null. */
   public static void drop(InMemoryDatabase db) {
     if (db != null) {
-      db.drop();
+      db.dbInstance.drop();
     }
   }
 
   private final SchemaCreator schemaCreator;
+  private final Instance dbInstance;
 
-  private Connection openHandle;
-  private Database<ReviewDb> database;
   private boolean created;
 
   @Inject
@@ -97,35 +87,26 @@
               }
             });
     this.schemaCreator = childInjector.getInstance(SchemaCreator.class);
-    initDatabase();
+    Instance dbInstanceFromInjector = childInjector.getInstance(Instance.class);
+    if (dbInstanceFromInjector != null) {
+      this.dbInstance = dbInstanceFromInjector;
+      this.created = true;
+    } else {
+      this.dbInstance = new Instance();
+    }
   }
 
   InMemoryDatabase(SchemaCreator schemaCreator) throws OrmException {
     this.schemaCreator = schemaCreator;
-    initDatabase();
+    this.dbInstance = new Instance();
   }
 
-  private void initDatabase() throws OrmException {
-    try {
-      DataSource dataSource = newDataSource();
-
-      // Open one connection. This will peg the database into memory
-      // until someone calls drop on us, allowing subsequent connections
-      // opened against the same URL to go to the same set of tables.
-      //
-      openHandle = dataSource.getConnection();
-
-      // Build the access layer around the connection factory.
-      //
-      database = new Database<>(dataSource, ReviewDb.class);
-
-    } catch (SQLException e) {
-      throw new OrmException(e);
-    }
+  public Instance getDbInstance() {
+    return dbInstance;
   }
 
   public Database<ReviewDb> getDatabase() {
-    return database;
+    return dbInstance.database;
   }
 
   @Override
@@ -146,20 +127,6 @@
     return this;
   }
 
-  /** Drop this database from memory so it no longer exists. */
-  public void drop() {
-    if (openHandle != null) {
-      try {
-        openHandle.close();
-      } catch (SQLException e) {
-        System.err.println("WARNING: Cannot close database connection");
-        e.printStackTrace(System.err);
-      }
-      openHandle = null;
-      database = null;
-    }
-  }
-
   public SystemConfig getSystemConfig() throws OrmException {
     try (ReviewDb c = open()) {
       return c.systemConfig().get(new SystemConfig.Key());
@@ -175,4 +142,60 @@
   public void assertSchemaVersion() throws OrmException {
     assertThat(getSchemaVersion().versionNbr).isEqualTo(SchemaVersion.getBinaryVersion());
   }
+
+  public static class Instance {
+    private static int dbCnt;
+
+    private Connection openHandle;
+    private Database<ReviewDb> database;
+    private boolean keepOpen;
+
+    private static synchronized DataSource newDataSource() throws SQLException {
+      final Properties p = new Properties();
+      p.setProperty("driver", org.h2.Driver.class.getName());
+      p.setProperty("url", "jdbc:h2:mem:Test_" + (++dbCnt));
+      return new SimpleDataSource(p);
+    }
+
+    private Instance() throws OrmException {
+      try {
+        DataSource dataSource = newDataSource();
+
+        // Open one connection. This will peg the database into memory
+        // until someone calls drop on us, allowing subsequent connections
+        // opened against the same URL to go to the same set of tables.
+        //
+        openHandle = dataSource.getConnection();
+
+        // Build the access layer around the connection factory.
+        //
+        database = new Database<>(dataSource, ReviewDb.class);
+
+      } catch (SQLException e) {
+        throw new OrmException(e);
+      }
+    }
+
+    public void setKeepOpen(boolean keepOpen) {
+      this.keepOpen = keepOpen;
+    }
+
+    /** Drop this database from memory so it no longer exists. */
+    public void drop() {
+      if (keepOpen) {
+        return;
+      }
+
+      if (openHandle != null) {
+        try {
+          openHandle.close();
+        } catch (SQLException e) {
+          System.err.println("WARNING: Cannot close database connection");
+          e.printStackTrace(System.err);
+        }
+        openHandle = null;
+        database = null;
+      }
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 869a4d4..39d927e 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.acceptance.api.group;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.GitUtil.deleteRef;
@@ -1223,6 +1224,25 @@
     }
   }
 
+  @Test
+  @Sandboxed
+  public void groupsOfUserCanBeListedInSlaveMode() throws Exception {
+    // TODO(aliceks): Remove this line when we have a group index in slave mode.
+    assume().that(readGroupsFromNoteDb()).isFalse();
+
+    GroupInput groupInput = new GroupInput();
+    groupInput.name = name("contributors");
+    groupInput.members = ImmutableList.of(user.username);
+    gApi.groups().create(groupInput).get();
+    restartAsSlave();
+
+    setApiUser(user);
+    List<GroupInfo> groups = gApi.groups().list().withUser(user.username).get();
+    ImmutableList<String> groupNames =
+        groups.stream().map(group -> group.name).collect(toImmutableList());
+    assertThat(groupNames).contains(groupInput.name);
+  }
+
   private void assertStaleGroupAndReindex(AccountGroup.UUID groupUuid) throws IOException {
     // Evict group from cache to be sure that we use the index state for staleness checks. This has
     // to happen directly on the groupsByUUID cache because GroupsCacheImpl triggers a reindex for
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
index e1c7b6b..ea42f47 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
@@ -22,9 +22,14 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.sshd.Commands;
+import com.jcraft.jsch.JSchException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import org.junit.Test;
@@ -36,44 +41,59 @@
 public class SshCommandsIT extends AbstractDaemonTest {
   private static final Logger log = LoggerFactory.getLogger(SshCommandsIT.class);
 
-  //TODO: It would be better to dynamically generate this list
-  private static final Map<String, List<String>> COMMANDS =
+  // TODO: It would be better to dynamically generate these lists
+  private static final List<String> COMMON_ROOT_COMMANDS =
+      ImmutableList.of(
+          "apropos",
+          "close-connection",
+          "flush-caches",
+          "gc",
+          "logging",
+          "ls-groups",
+          "ls-members",
+          "ls-projects",
+          "ls-user-refs",
+          "plugin",
+          "show-caches",
+          "show-connections",
+          "show-queue",
+          "version");
+
+  private static final List<String> MASTER_ONLY_ROOT_COMMANDS =
+      ImmutableList.of(
+          "ban-commit",
+          "create-account",
+          "create-branch",
+          "create-group",
+          "create-project",
+          "gsql",
+          "index",
+          "query",
+          "receive-pack",
+          "rename-group",
+          "review",
+          "set-account",
+          "set-head",
+          "set-members",
+          "set-project",
+          "set-project-parent",
+          "set-reviewers",
+          "stream-events",
+          "test-submit");
+
+  private static final Map<String, List<String>> MASTER_COMMANDS =
       ImmutableMap.of(
           Commands.ROOT,
-          ImmutableList.of(
-              "apropos",
-              "ban-commit",
-              "close-connection",
-              "create-account",
-              "create-branch",
-              "create-group",
-              "create-project",
-              "flush-caches",
-              "gc",
-              "gsql",
-              "index",
-              "logging",
-              "ls-groups",
-              "ls-members",
-              "ls-projects",
-              "ls-user-refs",
-              "plugin",
-              "query",
-              "receive-pack",
-              "rename-group",
-              "review",
-              "set-account",
-              "set-head",
-              "set-members",
-              "set-project",
-              "set-project-parent",
-              "set-reviewers",
-              "show-caches",
-              "show-connections",
-              "show-queue",
-              "stream-events",
-              "test-submit",
-              "version"),
+          ImmutableList.copyOf(
+              new ArrayList<String>() {
+                private static final long serialVersionUID = 1L;
+
+                {
+                  addAll(COMMON_ROOT_COMMANDS);
+                  addAll(MASTER_ONLY_ROOT_COMMANDS);
+                  Collections.sort(this);
+                }
+              }),
           "index",
           ImmutableList.of("activate", "changes", "project", "start"),
           "plugin",
@@ -81,13 +101,29 @@
           "test-submit",
           ImmutableList.of("rule", "type"));
 
+  private static final Map<String, List<String>> SLAVE_COMMANDS =
+      ImmutableMap.of(
+          Commands.ROOT,
+          COMMON_ROOT_COMMANDS,
+          "plugin",
+          ImmutableList.of("add", "enable", "install", "ls", "reload", "remove", "rm"));
+
   @Test
+  @Sandboxed
   public void sshCommandCanBeExecuted() throws Exception {
     // Access Database capability is required to run the "gerrit gsql" command
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
 
-    for (String root : COMMANDS.keySet()) {
-      for (String command : COMMANDS.get(root)) {
+    testCommandExecution(MASTER_COMMANDS);
+
+    restartAsSlave();
+    testCommandExecution(SLAVE_COMMANDS);
+  }
+
+  private void testCommandExecution(Map<String, List<String>> commands)
+      throws JSchException, IOException {
+    for (String root : commands.keySet()) {
+      for (String command : commands.get(root)) {
         // We can't assert that adminSshSession.hasError() is false, because using the --help
         // option causes the usage info to be written to stderr. Instead, we assert on the
         // content of the stderr, which will always start with "gerrit command" when the --help
@@ -109,4 +145,50 @@
     assertThat(adminSshSession.getError())
         .startsWith("fatal: gerrit: non-existing-command: not found");
   }
+
+  @Test
+  @Sandboxed
+  public void listCommands() throws Exception {
+    adminSshSession.exec("gerrit --help");
+    List<String> commands = parseCommandsFromGerritHelpText(adminSshSession.getError());
+    assertThat(commands).containsExactlyElementsIn(MASTER_COMMANDS.get(Commands.ROOT)).inOrder();
+
+    restartAsSlave();
+    adminSshSession.exec("gerrit --help");
+    commands = parseCommandsFromGerritHelpText(adminSshSession.getError());
+    assertThat(commands).containsExactlyElementsIn(SLAVE_COMMANDS.get(Commands.ROOT)).inOrder();
+  }
+
+  private List<String> parseCommandsFromGerritHelpText(String helpText) {
+    List<String> commands = new ArrayList<>();
+
+    String[] lines = helpText.split("\\n");
+
+    // Skip all lines including the line starting with "Available commands"
+    int row = 0;
+    do {
+      row++;
+    } while (row < lines.length && !lines[row - 1].startsWith("Available commands"));
+
+    // Skip all empty lines
+    while (lines[row].trim().isEmpty()) {
+      row++;
+    }
+
+    // Parse commands from all lines that are indented (start with a space)
+    while (row < lines.length && lines[row].startsWith(" ")) {
+      String line = lines[row].trim();
+      // Abort on empty line
+      if (line.isEmpty()) {
+        break;
+      }
+
+      // Cut off command description if there is one
+      int endOfCommand = line.indexOf(' ');
+      commands.add(endOfCommand > 0 ? line.substring(0, line.indexOf(' ')) : line);
+      row++;
+    }
+
+    return commands;
+  }
 }
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 6442943..bac9b33 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 6442943d6a6de21b7d6d25b3fad2753a3c30d2d8
+Subproject commit bac9b336a326585accfc9a9eda7b5340e62783ac
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
index c13d69f..da9cae6 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
@@ -82,10 +82,10 @@
         </span>
       </section>
       <section>
-        <span class="title">Line numbers</span>
+        <span class="title">Hide line numbers</span>
         <span class="value">
           <input
-              id="showLineNumbers"
+              id="hideLineNumbers"
               type="checkbox"
               checked$="[[editPrefs.hide_line_numbers]]"
               on-change="_handleLineNumbersChanged">
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
index 0a791fd..5a97363 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
@@ -50,7 +50,7 @@
     },
 
     _handleLineNumbersChanged() {
-      this.set('editPrefs.hide_line_numbers', !this.$.showLineNumbers.checked);
+      this.set('editPrefs.hide_line_numbers', this.$.hideLineNumbers.checked);
       this._handleEditPrefsChanged();
     },
 
@@ -75,14 +75,10 @@
       this._handleEditPrefsChanged();
     },
 
-    _handleShowBaseVersionChanged() {
-      this.set('editPrefs.show_base', this.$.showShowBaseVersion.checked);
-      this._handleEditPrefsChanged();
-    },
-
     save() {
-      return this.$.restAPI.saveEditPreferences(this.editPrefs)
-          .then(() => { this.hasUnsavedChanges = false; });
+      return this.$.restAPI.saveEditPreferences(this.editPrefs).then(res => {
+        this.hasUnsavedChanges = false;
+      });
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
index 118ee4f..a0f95c1 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
@@ -93,7 +93,7 @@
           .firstElementChild.checked, editPreferences.syntax_highlighting);
       assert.equal(valueOf('Show tabs', 'editPreferences')
           .firstElementChild.checked, editPreferences.show_tabs);
-      assert.equal(valueOf('Line numbers', 'editPreferences')
+      assert.equal(valueOf('Hide line numbers', 'editPreferences')
           .firstElementChild.checked, editPreferences.hide_line_numbers);
       assert.equal(valueOf('Match brackets', 'editPreferences')
           .firstElementChild.checked, editPreferences.match_brackets);
diff --git a/tools/bzl/license.bzl b/tools/bzl/license.bzl
index 38dfbe5..3578173 100644
--- a/tools/bzl/license.bzl
+++ b/tools/bzl/license.bzl
@@ -25,7 +25,7 @@
   # post process the XML into our favorite format.
   native.genrule(
     name = "gen_license_txt_" + name,
-    cmd = "python $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
+    cmd = "python2 $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
     outs = [ name + ".txt" ],
     tools = tools,
     **kwargs
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 448d940..9f8b4b7 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -277,7 +277,7 @@
     doc.writexml(fd, addindent='\t', newl='\n', encoding='UTF-8')
 
 try:
-  ext_location = retrieve_ext_location()
+  ext_location = retrieve_ext_location().decode("utf-8")
   gen_project(args.project_name)
   gen_classpath(ext_location)
   gen_factorypath(ext_location)
diff --git a/tools/js/bowerutil.py b/tools/js/bowerutil.py
index 8e8e835..c2e11cd 100644
--- a/tools/js/bowerutil.py
+++ b/tools/js/bowerutil.py
@@ -40,7 +40,7 @@
       if f == '.bower.json':
         continue
       p = os.path.join(root, f)
-      hash_obj.update(p[len(path)+1:])
-      hash_obj.update(open(p).read())
+      hash_obj.update(p[len(path)+1:].encode("utf-8"))
+      hash_obj.update(open(p, "rb").read())
 
   return hash_obj
diff --git a/tools/js/download_bower.py b/tools/js/download_bower.py
index f5b7bf5..1c8d45e 100755
--- a/tools/js/download_bower.py
+++ b/tools/js/download_bower.py
@@ -68,7 +68,7 @@
   deps = info.get('dependencies')
   if deps:
     with open(os.path.join('.bowerrc'), 'w') as f:
-      json.dump({'ignoredDependencies': deps.keys()}, f)
+      json.dump({'ignoredDependencies': list(deps.keys())}, f)
 
 
 def cache_entry(name, package, version, sha1):
diff --git a/tools/js/npm_pack.py b/tools/js/npm_pack.py
index 9e8482e..de45083 100755
--- a/tools/js/npm_pack.py
+++ b/tools/js/npm_pack.py
@@ -42,7 +42,7 @@
 def bundle_dependencies():
   with open('package.json') as f:
     package = json.load(f)
-  package['bundledDependencies'] = package['dependencies'].keys()
+  package['bundledDependencies'] = list(package['dependencies'].keys())
   with open('package.json', 'w') as f:
     json.dump(package, f)