Merge "Test that deleting an external ID of another user from own account fails"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 6b5874b..a003d9f 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1742,7 +1742,7 @@
 If `true` enable the automatic mixed mode
 (see link:http://www.h2database.com/html/features.html#auto_mixed_mode[Automatic Mixed Mode]).
 This enables concurrent access to the embedded H2 database from command line
-utils (e.g. RebuildNoteDb).
+utils (e.g. MigrateToNoteDb).
 +
 Default is `false`.
 
diff --git a/Documentation/dev-note-db.txt b/Documentation/dev-note-db.txt
index e5bd54e..bacbeda 100644
--- a/Documentation/dev-note-db.txt
+++ b/Documentation/dev-note-db.txt
@@ -99,7 +99,7 @@
   inaccurate results, and writing to NoteDb would compound the problem. +
   Thus it is up to an admin of a previously-ReviewDb site to ensure
   MigratePrimaryStorage has been run for all changes. Note that the current
-  implementation of the `rebuild-note-db` program does not do this. +
+  implementation of the `migrate-to-note-db` program does not do this. +
   In this phase, it would be possible to delete the Changes tables out from
   under a running server with no effect.
 - `noteDb.changes.fuseUpdates=true`: Code and meta updates within a single
@@ -112,25 +112,25 @@
 == Migration
 
 Once configuration options are set, migration to NoteDb is primarily
-accomplished by running the `rebuild-note-db` program. Currently, this program
+accomplished by running the `migrate-to-note-db` program. Currently, this program
 bulk copies ReviewDb data into NoteDb, but leaves primary storage of these
 changes in ReviewDb, so the site is runnable with
 `noteDb.changes.{write,read}=true`, but ReviewDb is still required.
 
-Eventually, `rebuild-note-db` will set primary storage to NoteDb for all
+Eventually, `migrate-to-note-db` will set primary storage to NoteDb for all
 changes by default, so a site will be able to stop using ReviewDb for changes
 immediately after a successful run.
 
 There is code in `PrimaryStorageMigrator.java` to migrate individual changes
 from NoteDb primary to ReviewDb primary. This code is not intended to be used
 except in the event of a critical bug in NoteDb primary changes in production.
-It will likely never be used by `rebuild-note-db`, and in fact it's not
-recommended to run `rebuild-note-db` until the code is stable enough that the
+It will likely never be used by `migrate-to-note-db`, and in fact it's not
+recommended to run `migrate-to-note-db` until the code is stable enough that the
 reverse migration won't be necessary.
 
 === Zero-Downtime Multi-Master Migration
 
-Single-master Gerrit sites can use `rebuild-note-db` on an offline site to
+Single-master Gerrit sites can use `migrate-to-note-db` on an offline site to
 rebuild NoteDb, but this doesn't work in a zero-downtime environment like
 googlesource.com.
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index a274206..16e61f6 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -3130,6 +3130,28 @@
   HTTP/1.1 204 No Content
 ----
 
+[[set-message]]
+=== Set Commit Message
+--
+'PUT /changes/link:#change-id[\{change-id\}]/message'
+--
+
+Creates a new patch set with a new commit message.
+
+The new commit message must be provided in the request body inside a
+link:#commit-message-input[CommitMessageInput] entity and contain the change ID footer if
+link:project-configuration.html#require-change-id[Require Change-Id] was specified.
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/message HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "message": "New Commit message \n\nChange-Id: I10394472cbd17dd12454f229e4f6de00b143a444\n"
+  }
+----
+
 [[revision-endpoints]]
 == Revision Endpoints
 
@@ -5921,6 +5943,25 @@
 link:#web-link-info[WebLinkInfo] entities.
 |===========================
 
+[[commit-message-input]]
+=== CommitMessageInput
+The `CommitMessageInput` entity contains information for changing
+the commit message of a change.
+
+[options="header",cols="1,^1,5"]
+|=============================
+|Field Name      ||Description
+|`message`       ||New commit message.
+|`notify`        |optional|
+Notify handling that defines to whom email notifications should be sent
+after the commit message was updated. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `ALL`.
+|`notify_details`|optional|
+Additional information about whom to notify about the update as a map
+of recipient type to link:#notify-info[NotifyInfo] entity.
+|=============================
+
 [[delete-comment-input]]
 === DeleteCommentInput
 The `DeleteCommentInput` entity contains the option for deleting a comment.
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index d128cc6..d2b1f32 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -237,6 +237,7 @@
   protected TestRepository<InMemoryRepository> testRepo;
   protected String resourcePrefix;
   protected Description description;
+  protected boolean testRequiresSsh;
 
   @Inject private ChangeIndexCollection changeIndexes;
   @Inject private EventRecorder.Factory eventRecorderFactory;
@@ -245,7 +246,6 @@
   @Inject private SchemaFactory<ReviewDb> reviewDbProvider;
 
   private List<Repository> toClose;
-  private boolean useSsh;
 
   @Before
   public void clearSender() {
@@ -259,7 +259,7 @@
 
   @Before
   public void assumeSshIfRequired() {
-    if (useSsh) {
+    if (testRequiresSsh) {
       // If the test uses ssh, we use assume() to make sure ssh is enabled on
       // the test suite. JUnit will skip tests annotated with @UseSsh if we
       // disable them using the command line flag.
@@ -276,7 +276,7 @@
   public static void stopCommonServer() throws Exception {
     if (commonServer != null) {
       try {
-        commonServer.stop();
+        commonServer.close();
       } finally {
         commonServer = null;
       }
@@ -319,11 +319,11 @@
     baseConfig.setInt("receive", null, "changeUpdateThreads", 4);
     if (classDesc.equals(methodDesc) && !classDesc.sandboxed() && !methodDesc.sandboxed()) {
       if (commonServer == null) {
-        commonServer = GerritServer.start(classDesc, baseConfig);
+        commonServer = GerritServer.initAndStart(classDesc, baseConfig);
       }
       server = commonServer;
     } else {
-      server = GerritServer.start(methodDesc, baseConfig);
+      server = GerritServer.initAndStart(methodDesc, baseConfig);
     }
 
     server.getTestInjector().injectMembers(this);
@@ -341,22 +341,20 @@
 
     db = reviewDbProvider.open();
 
-    if (classDesc.useSsh() || methodDesc.useSsh()) {
-      useSsh = true;
-      if (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();
-      }
-    } else {
-      useSsh = false;
+    testRequiresSsh = classDesc.useSshAnnotation() || methodDesc.useSshAnnotation();
+    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();
     }
 
     resourcePrefix =
@@ -503,7 +501,7 @@
       userSshSession.close();
     }
     if (server != commonServer) {
-      server.stop();
+      server.close();
       server = null;
     }
     notesMigration.resetFromEnv();
@@ -1297,4 +1295,13 @@
       return revCommit;
     }
   }
+
+  protected RevCommit parseCurrentRevision(RevWalk rw, PushOneCommit.Result r) throws Exception {
+    return parseCurrentRevision(rw, r.getChangeId());
+  }
+
+  protected RevCommit parseCurrentRevision(RevWalk rw, String changeId) throws Exception {
+    return rw.parseCommit(
+        ObjectId.fromString(get(changeId, ListChangesOption.CURRENT_REVISION).currentRevision));
+  }
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index 00051b1..87105f8 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gerrit.testutil.SshMode;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -59,6 +58,7 @@
   private final AccountCache accountCache;
   private final AccountByEmailCache byEmailCache;
   private final ExternalIdsUpdate.Server externalIdsUpdate;
+  private final boolean sshEnabled;
 
   @Inject
   AccountCreator(
@@ -69,7 +69,8 @@
       SshKeyCache sshKeyCache,
       AccountCache accountCache,
       AccountByEmailCache byEmailCache,
-      ExternalIdsUpdate.Server externalIdsUpdate) {
+      ExternalIdsUpdate.Server externalIdsUpdate,
+      @SshEnabled boolean sshEnabled) {
     accounts = new HashMap<>();
     reviewDbProvider = schema;
     this.accountsUpdate = accountsUpdate;
@@ -79,6 +80,7 @@
     this.accountCache = accountCache;
     this.byEmailCache = byEmailCache;
     this.externalIdsUpdate = externalIdsUpdate;
+    this.sshEnabled = sshEnabled;
   }
 
   public synchronized TestAccount create(
@@ -124,7 +126,7 @@
       }
 
       KeyPair sshKey = null;
-      if (SshMode.useSsh() && username != null) {
+      if (sshEnabled && username != null) {
         sshKey = genSshKey();
         authorizedKeys.addKey(id, publicKey(sshKey, email));
         sshKeyCache.evict(username);
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index b31b17b..52dc4f2 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
@@ -33,16 +36,15 @@
 import com.google.gerrit.testutil.NoteDbChecker;
 import com.google.gerrit.testutil.NoteDbMode;
 import com.google.gerrit.testutil.SshMode;
-import com.google.gerrit.testutil.TempFileUtil;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
-import java.io.File;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Field;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.URI;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.HashMap;
 import java.util.Map;
@@ -58,14 +60,15 @@
 import org.eclipse.jgit.lib.RepositoryCache;
 import org.eclipse.jgit.util.FS;
 
-public class GerritServer {
+public class GerritServer implements AutoCloseable {
   @AutoValue
-  abstract static class Description {
-    static Description forTestClass(org.junit.runner.Description testDesc, String configName) {
+  public abstract static class Description {
+    public static Description forTestClass(
+        org.junit.runner.Description testDesc, String configName) {
       return new AutoValue_GerritServer_Description(
           testDesc,
           configName,
-          true, // @UseLocalDisk is only valid on methods.
+          !has(UseLocalDisk.class, testDesc.getTestClass()),
           !has(NoHttpd.class, testDesc.getTestClass()),
           has(Sandboxed.class, testDesc.getTestClass()),
           has(UseSsh.class, testDesc.getTestClass()),
@@ -75,11 +78,13 @@
           null); // @GlobalPluginConfigs is only valid on methods.
     }
 
-    static Description forTestMethod(org.junit.runner.Description testDesc, String configName) {
+    public static Description forTestMethod(
+        org.junit.runner.Description testDesc, String configName) {
       return new AutoValue_GerritServer_Description(
           testDesc,
           configName,
-          testDesc.getAnnotation(UseLocalDisk.class) == null,
+          testDesc.getAnnotation(UseLocalDisk.class) == null
+              && !has(UseLocalDisk.class, testDesc.getTestClass()),
           testDesc.getAnnotation(NoHttpd.class) == null
               && !has(NoHttpd.class, testDesc.getTestClass()),
           testDesc.getAnnotation(Sandboxed.class) != null
@@ -112,7 +117,11 @@
 
     abstract boolean sandboxed();
 
-    abstract boolean useSsh();
+    abstract boolean useSshAnnotation();
+
+    boolean useSsh() {
+      return useSshAnnotation() && SshMode.useSsh();
+    }
 
     @Nullable
     abstract GerritConfig config();
@@ -159,10 +168,81 @@
     }
   }
 
-  /** Returns fully started Gerrit server */
-  static GerritServer start(Description desc, Config baseConfig) throws Exception {
-    desc.checkValidAnnotations();
+  /**
+   * Initializes on-disk site but does not start server.
+   *
+   * @param desc server description
+   * @param baseConfig default config values; merged with config from {@code desc} and then written
+   *     into {@code site/etc/gerrit.config}.
+   * @param site temp directory where site will live.
+   * @throws Exception
+   */
+  public static void init(Description desc, Config baseConfig, Path site) throws Exception {
+    checkArgument(!desc.memory(), "can't initialize site path for in-memory test: %s", desc);
     Config cfg = desc.buildConfig(baseConfig);
+    Map<String, Config> pluginConfigs = desc.buildPluginConfigs();
+    Init init = new Init();
+    int rc =
+        init.main(
+            new String[] {
+              "-d", site.toString(), "--batch", "--no-auto-start", "--skip-plugins",
+            });
+    if (rc != 0) {
+      throw new RuntimeException("Couldn't initialize site");
+    }
+
+    MergeableFileBasedConfig gerritConfig =
+        new MergeableFileBasedConfig(
+            site.resolve("etc").resolve("gerrit.config").toFile(), FS.DETECTED);
+    gerritConfig.load();
+    gerritConfig.merge(cfg);
+    mergeTestConfig(gerritConfig);
+    gerritConfig.save();
+
+    for (String pluginName : pluginConfigs.keySet()) {
+      MergeableFileBasedConfig pluginCfg =
+          new MergeableFileBasedConfig(
+              site.resolve("etc").resolve(pluginName + ".config").toFile(), FS.DETECTED);
+      pluginCfg.load();
+      pluginCfg.merge(pluginConfigs.get(pluginName));
+      pluginCfg.save();
+    }
+  }
+
+  /**
+   * Initializes new Gerrit site and returns started server.
+   *
+   * @param desc server description.
+   * @param baseConfig default config values; merged with config from {@code desc}. Must contain a
+   *     value for {@code gerrit.tempSiteDir} pointing to a temporary directory. This directory is
+   *     only actually used for on-disk sites ({@link Description#memory()} returns false), but it
+   *     must nonetheless exist for in-memory sites.
+   * @return started server.
+   * @throws Exception
+   */
+  public static GerritServer initAndStart(Description desc, Config baseConfig) throws Exception {
+    Path site = Paths.get(baseConfig.getString("gerrit", null, "tempSiteDir"));
+    if (!desc.memory()) {
+      init(desc, baseConfig, site);
+    }
+    return start(desc, baseConfig, site);
+  }
+
+  /**
+   * Starts Gerrit server from existing on-disk site.
+   *
+   * @param desc server description.
+   * @param baseConfig default config values; merged with config from {@code desc}.
+   * @param site existing temporary directory for site. Required, but may be empty, for in-memory
+   *     servers. For on-disk servers, assumes that {@link #init} was previously called to
+   *     initialize this directory.
+   * @return started server.
+   * @throws Exception
+   */
+  public static GerritServer start(Description desc, Config baseConfig, Path site)
+      throws Exception {
+    checkArgument(site != null, "site is required (even for in-memory server");
+    desc.checkValidAnnotations();
     Logger.getLogger("com.google.gerrit").setLevel(Level.DEBUG);
     CyclicBarrier serverStarted = new CyclicBarrier(2);
     Daemon daemon =
@@ -174,82 +254,57 @@
                 throw new RuntimeException(e);
               }
             },
-            Paths.get(baseConfig.getString("gerrit", null, "tempSiteDir")));
+            site);
     daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
-    daemon.setEnableSshd(SshMode.useSsh());
+    daemon.setEnableSshd(desc.useSsh());
 
-    final File site;
-    ExecutorService daemonService = null;
     if (desc.memory()) {
-      site = null;
-      mergeTestConfig(cfg);
-      // Set the log4j configuration to an invalid one to prevent system logs
-      // from getting configured and creating log files.
-      System.setProperty(SystemLog.LOG4J_CONFIGURATION, "invalidConfiguration");
-      cfg.setBoolean("httpd", null, "requestLog", false);
-      cfg.setBoolean("sshd", null, "requestLog", false);
-      cfg.setBoolean("index", "lucene", "testInmemory", true);
-      cfg.setString("gitweb", null, "cgi", "");
-      daemon.setEnableHttpd(desc.httpd());
-      daemon.setLuceneModule(LuceneIndexModule.singleVersionAllLatest(0));
-      daemon.setDatabaseForTesting(
-          ImmutableList.<Module>of(new InMemoryTestingDatabaseModule(cfg)));
-      daemon.start();
-    } else {
-      site = initSite(cfg, desc.buildPluginConfigs());
-      daemonService = Executors.newSingleThreadExecutor();
-      @SuppressWarnings("unused")
-      Future<?> possiblyIgnoredError =
-          daemonService.submit(
-              () -> {
-                int rc =
-                    daemon.main(
-                        new String[] {
-                          "-d", site.getPath(), "--headless", "--console-log", "--show-stack-trace",
-                        });
-                if (rc != 0) {
-                  System.err.println("Failed to start Gerrit daemon");
-                  serverStarted.reset();
-                }
-                return null;
-              });
-      serverStarted.await();
-      System.out.println("Gerrit Server Started");
+      return startInMemory(desc, baseConfig, daemon);
     }
-
-    Injector i = createTestInjector(daemon);
-    return new GerritServer(desc, i, daemon, daemonService);
+    return startOnDisk(desc, site, daemon, serverStarted);
   }
 
-  private static File initSite(Config base, Map<String, Config> pluginConfigs) throws Exception {
-    File tmp = TempFileUtil.createTempDirectory();
-    Init init = new Init();
-    int rc =
-        init.main(
-            new String[] {
-              "-d", tmp.getPath(), "--batch", "--no-auto-start", "--skip-plugins",
-            });
-    if (rc != 0) {
-      throw new RuntimeException("Couldn't initialize site");
-    }
-
-    MergeableFileBasedConfig cfg =
-        new MergeableFileBasedConfig(new File(new File(tmp, "etc"), "gerrit.config"), FS.DETECTED);
-    cfg.load();
-    cfg.merge(base);
+  private static GerritServer startInMemory(Description desc, Config baseConfig, Daemon daemon)
+      throws Exception {
+    Config cfg = desc.buildConfig(baseConfig);
     mergeTestConfig(cfg);
-    cfg.save();
+    // Set the log4j configuration to an invalid one to prevent system logs
+    // from getting configured and creating log files.
+    System.setProperty(SystemLog.LOG4J_CONFIGURATION, "invalidConfiguration");
+    cfg.setBoolean("httpd", null, "requestLog", false);
+    cfg.setBoolean("sshd", null, "requestLog", false);
+    cfg.setBoolean("index", "lucene", "testInmemory", true);
+    cfg.setString("gitweb", null, "cgi", "");
+    daemon.setEnableHttpd(desc.httpd());
+    daemon.setLuceneModule(LuceneIndexModule.singleVersionAllLatest(0));
+    daemon.setDatabaseForTesting(ImmutableList.<Module>of(new InMemoryTestingDatabaseModule(cfg)));
+    daemon.start();
+    return new GerritServer(desc, createTestInjector(daemon), daemon, null);
+  }
 
-    for (String pluginName : pluginConfigs.keySet()) {
-      MergeableFileBasedConfig pluginCfg =
-          new MergeableFileBasedConfig(
-              new File(new File(tmp, "etc"), pluginName + ".config"), FS.DETECTED);
-      pluginCfg.load();
-      pluginCfg.merge(pluginConfigs.get(pluginName));
-      pluginCfg.save();
-    }
+  private static GerritServer startOnDisk(
+      Description desc, Path site, Daemon daemon, CyclicBarrier serverStarted) throws Exception {
+    checkNotNull(site);
+    ExecutorService daemonService = Executors.newSingleThreadExecutor();
+    @SuppressWarnings("unused")
+    Future<?> possiblyIgnoredError =
+        daemonService.submit(
+            () -> {
+              int rc =
+                  daemon.main(
+                      new String[] {
+                        "-d", site.toString(), "--headless", "--console-log", "--show-stack-trace",
+                      });
+              if (rc != 0) {
+                System.err.println("Failed to start Gerrit daemon");
+                serverStarted.reset();
+              }
+              return null;
+            });
+    serverStarted.await();
+    System.out.println("Gerrit Server Started");
 
-    return tmp;
+    return new GerritServer(desc, createTestInjector(daemon), daemon, daemonService);
   }
 
   private static void mergeTestConfig(Config cfg) {
@@ -279,6 +334,7 @@
         new FactoryModule() {
           @Override
           protected void configure() {
+            bindConstant().annotatedWith(SshEnabled.class).to(daemon.getEnableSshd());
             bind(AccountCreator.class);
             factory(PushOneCommit.Factory.class);
             install(InProcessProtocol.module());
@@ -338,7 +394,7 @@
     return httpAddress;
   }
 
-  Injector getTestInjector() {
+  public Injector getTestInjector() {
     return testInjector;
   }
 
@@ -346,7 +402,8 @@
     return desc;
   }
 
-  void stop() throws Exception {
+  @Override
+  public void close() throws Exception {
     try {
       checkNoteDbState();
     } finally {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshEnabled.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshEnabled.java
new file mode 100644
index 0000000..5349755
--- /dev/null
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshEnabled.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2017 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.acceptance;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface SshEnabled {}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java
index a649095..e177bb4 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.acceptance;
 
 import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 
-@Target({METHOD})
+@Target({TYPE, METHOD})
 @Retention(RUNTIME)
 public @interface UseLocalDisk {}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 80e1872..1c5f178 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -86,7 +86,6 @@
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
-import com.google.gerrit.testutil.SshMode;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.ByteArrayOutputStream;
@@ -212,28 +211,13 @@
 
   @Test
   public void create() throws Exception {
-    TestAccount foo = accountCreator.create("foo");
-    AccountInfo info = gApi.accounts().id(foo.id.get()).get();
-    assertThat(info.username).isEqualTo("foo");
-    if (SshMode.useSsh()) {
-      accountIndexedCounter.assertReindexOf(
-          foo, 3); // account creation + external ID creation + adding SSH keys
-    } else {
-      accountIndexedCounter.assertReindexOf(foo, 2); // account creation + external ID creation
-    }
+    create(2); // account creation + external ID creation
+  }
 
-    // check user branch
-    try (Repository repo = repoManager.openRepository(allUsers);
-        RevWalk rw = new RevWalk(repo)) {
-      Ref ref = repo.exactRef(RefNames.refsUsers(foo.getId()));
-      assertThat(ref).isNotNull();
-      RevCommit c = rw.parseCommit(ref.getObjectId());
-      long timestampDiffMs =
-          Math.abs(
-              c.getCommitTime() * 1000L
-                  - accountCache.get(foo.getId()).getAccount().getRegisteredOn().getTime());
-      assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS);
-    }
+  @Test
+  @UseSsh
+  public void createWithSshKeys() throws Exception {
+    create(3); // account creation + external ID creation + adding SSH keys
   }
 
   @Test
@@ -1087,6 +1071,26 @@
     assertThat(checkInfo.checkAccountsResult.problems).containsExactlyElementsIn(expectedProblems);
   }
 
+  public void create(int expectedAccountReindexCalls) throws Exception {
+    TestAccount foo = accountCreator.create("foo");
+    AccountInfo info = gApi.accounts().id(foo.id.get()).get();
+    assertThat(info.username).isEqualTo("foo");
+    accountIndexedCounter.assertReindexOf(foo, expectedAccountReindexCalls);
+
+    // check user branch
+    try (Repository repo = repoManager.openRepository(allUsers);
+        RevWalk rw = new RevWalk(repo)) {
+      Ref ref = repo.exactRef(RefNames.refsUsers(foo.getId()));
+      assertThat(ref).isNotNull();
+      RevCommit c = rw.parseCommit(ref.getObjectId());
+      long timestampDiffMs =
+          Math.abs(
+              c.getCommitTime() * 1000L
+                  - accountCache.get(foo.getId()).getAccount().getRegisteredOn().getTime());
+      assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS);
+    }
+  }
+
   private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) {
     int seq = 1;
     for (SshKeyInfo key : sshKeys) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index e92d255..429ee0f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -52,7 +52,9 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -75,6 +77,7 @@
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.reviewdb.client.Account;
@@ -82,11 +85,13 @@
 import com.google.gerrit.reviewdb.client.Branch.NameKey;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.GetRevisionActions;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.sql.Timestamp;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
@@ -99,6 +104,8 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -1203,6 +1210,84 @@
         .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
   }
 
+  @Test
+  public void changeCommitMessage() throws Exception {
+    // Tests mutating the commit message as both the owner of the change and a regular user with
+    // addPatchSet permission. Asserts that both cases succeed.
+    PushOneCommit.Result r = createChange();
+    r.assertOkStatus();
+    assertThat(getCommitMessage(r.getChangeId()))
+        .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
+
+    for (TestAccount acc : ImmutableList.of(admin, user)) {
+      setApiUser(acc);
+      String newMessage =
+          "modified commit by " + acc.username + "\n\nChange-Id: " + r.getChangeId() + "\n";
+      gApi.changes().id(r.getChangeId()).setMessage(newMessage);
+      RevisionApi rApi = gApi.changes().id(r.getChangeId()).current();
+      assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt");
+      assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage);
+    }
+  }
+
+  @Test
+  public void changeCommitMessageWithNoChangeIdSucceedsIfChangeIdNotRequired() throws Exception {
+    ConfigInput configInput = new ConfigInput();
+    configInput.requireChangeId = InheritableBoolean.FALSE;
+    gApi.projects().name(project.get()).config(configInput);
+
+    PushOneCommit.Result r = createChange();
+    r.assertOkStatus();
+    assertThat(getCommitMessage(r.getChangeId()))
+        .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
+
+    String newMessage = "modified commit\n";
+    gApi.changes().id(r.getChangeId()).setMessage(newMessage);
+    RevisionApi rApi = gApi.changes().id(r.getChangeId()).current();
+    assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt");
+    assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage);
+  }
+
+  @Test
+  public void changeCommitMessageWithNoChangeIdFails() throws Exception {
+    PushOneCommit.Result r = createChange();
+    assertThat(getCommitMessage(r.getChangeId()))
+        .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("missing Change-Id footer");
+    gApi.changes().id(r.getChangeId()).setMessage("modified commit\n");
+  }
+
+  @Test
+  public void changeCommitMessageWithWrongChangeIdFails() throws Exception {
+    PushOneCommit.Result otherChange = createChange();
+    PushOneCommit.Result r = createChange();
+    assertThat(getCommitMessage(r.getChangeId()))
+        .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("wrong Change-Id footer");
+    gApi.changes()
+        .id(r.getChangeId())
+        .setMessage("modified commit\n\nChange-Id: " + otherChange.getChangeId() + "\n");
+  }
+
+  @Test
+  public void changeCommitMessageWithoutPermissionFails() throws Exception {
+    // Create new project with clean permissions
+    Project.NameKey p = createProject("addPatchSetEdit");
+    TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user);
+    // Block default permission
+    block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
+    // Create change as user
+    PushOneCommit push = pushFactory.create(db, user.getIdent(), userTestRepo);
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+    // Try to change the commit message
+    exception.expect(AuthException.class);
+    exception.expectMessage("modifying commit message not permitted");
+    gApi.changes().id(r.getChangeId()).setMessage("foo");
+  }
+
   private static void assertCherryPickResult(
       ChangeInfo changeInfo, CherryPickInput input, String srcChangeId) throws Exception {
     assertThat(changeInfo.changeId).isEqualTo(srcChangeId);
@@ -1279,6 +1364,10 @@
     return li.all.stream().filter(a -> a._accountId == accountId).findFirst().get();
   }
 
+  private String getCommitMessage(String changeId) throws RestApiException, IOException {
+    return gApi.changes().id(changeId).current().file("/COMMIT_MSG").content().asString();
+  }
+
   private static Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) {
     return Iterables.transform(r, a -> new Account.Id(a._accountId));
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 6941765..d64d67f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -16,20 +16,25 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.GitUtil.getChangeId;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.change.Submit.TestSubmitInput;
 import com.google.gerrit.testutil.ConfigSuite;
+import java.util.ArrayDeque;
 import java.util.Map;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Test;
 
@@ -740,4 +745,69 @@
     expectToHaveSubmoduleState(repoA, "master", "project-b", repoB, "master");
     expectToHaveSubmoduleState(repoA, "dev", "project-b", repoB, "dev");
   }
+
+  @Test
+  public void retrySubmitAfterTornTopicOnLockFailure() throws Exception {
+    assume().that(notesMigration.fuseUpdates()).isTrue();
+
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> sub1 = createProjectWithPush("sub1");
+    TestRepository<?> sub2 = createProjectWithPush("sub2");
+
+    allowMatchingSubmoduleSubscription(
+        "sub1", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(
+        "sub2", "refs/heads/master", "super-project", "refs/heads/master");
+
+    Config config = new Config();
+    prepareSubmoduleConfigEntry(config, "sub1", "master");
+    prepareSubmoduleConfigEntry(config, "sub2", "master");
+    pushSubmoduleConfig(superRepo, "master", config);
+
+    ObjectId superPreviousId = pushChangeTo(superRepo, "master");
+
+    String topic = "same-topic";
+    ObjectId sub1Id = pushChangeTo(sub1, "refs/for/master", "some message", topic);
+    ObjectId sub2Id = pushChangeTo(sub2, "refs/for/master", "some message", topic);
+
+    String changeId1 = getChangeId(sub1, sub1Id).get();
+    String changeId2 = getChangeId(sub2, sub2Id).get();
+    approve(changeId1);
+    approve(changeId2);
+
+    TestSubmitInput input = new TestSubmitInput();
+    input.generateLockFailures =
+        new ArrayDeque<>(
+            ImmutableList.of(
+                false, // Change 1, attempt 1: success
+                true, // Change 2, attempt 1: lock failure
+                false, // Change 1, attempt 2: success
+                false, // Change 2, attempt 2: success
+                false)); // Leftover value to check total number of calls.
+    gApi.changes().id(changeId1).current().submit(input);
+
+    assertThat(info(changeId1).status).isEqualTo(ChangeStatus.MERGED);
+    assertThat(info(changeId2).status).isEqualTo(ChangeStatus.MERGED);
+
+    sub1.git().fetch().call();
+    RevWalk rw1 = sub1.getRevWalk();
+    RevCommit master1 = rw1.parseCommit(getRemoteHead(name("sub1"), "master"));
+    RevCommit change1Ps = parseCurrentRevision(rw1, changeId1);
+    assertThat(rw1.isMergedInto(change1Ps, master1)).isTrue();
+
+    sub2.git().fetch().call();
+    RevWalk rw2 = sub2.getRevWalk();
+    RevCommit master2 = rw2.parseCommit(getRemoteHead(name("sub2"), "master"));
+    RevCommit change2Ps = parseCurrentRevision(rw2, changeId2);
+    assertThat(rw2.isMergedInto(change2Ps, master2)).isTrue();
+
+    assertThat(input.generateLockFailures).containsExactly(false);
+
+    expectToHaveSubmoduleState(superRepo, "master", "sub1", sub1, "master");
+    expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2, "master");
+
+    assertWithMessage("submodule subscription update should have made one commit")
+        .that(superRepo.getRepository().resolve("origin/master^"))
+        .isEqualTo(superPreviousId);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNoteDbIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/MigrateToNoteDbIT.java
similarity index 72%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNoteDbIT.java
rename to gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/MigrateToNoteDbIT.java
index 2279fca..18217eb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNoteDbIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/MigrateToNoteDbIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.pgm;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
 
 import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.server.config.SitePaths;
@@ -28,7 +29,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class RebuildNoteDbIT {
+public class MigrateToNoteDbIT {
   private String sitePath;
   private StoredConfig gerritConfig;
 
@@ -37,6 +38,7 @@
     SitePaths sitePaths = new SitePaths(TempFileUtil.createTempDirectory().toPath());
     sitePath = sitePaths.site_path.toString();
     gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
+    initSite();
   }
 
   @After
@@ -45,10 +47,17 @@
   }
 
   @Test
+  public void rebuildEmptySiteStartingWithNoteDbDisabed() throws Exception {
+    assertNotesMigrationState(NotesMigrationState.REVIEW_DB);
+    runGerrit("MigrateToNoteDb", "-d", sitePath, "--show-stack-trace");
+    assertNotesMigrationState(NotesMigrationState.READ_WRITE_NO_SEQUENCE);
+  }
+
+  @Test
   public void rebuildEmptySiteStartingWithNoteDbEnabled() throws Exception {
-    initSite();
-    setNotesMigrationState(NotesMigrationState.NOTE_DB_UNFUSED);
-    runGerrit("RebuildNoteDb", "-d", sitePath, "--show-stack-trace");
+    setNotesMigrationState(NotesMigrationState.READ_WRITE_NO_SEQUENCE);
+    runGerrit("MigrateToNoteDb", "-d", sitePath, "--show-stack-trace");
+    assertNotesMigrationState(NotesMigrationState.READ_WRITE_NO_SEQUENCE);
   }
 
   private void initSite() throws Exception {
@@ -71,4 +80,10 @@
     ConfigNotesMigration.setConfigValues(gerritConfig, state.migration());
     gerritConfig.save();
   }
+
+  private void assertNotesMigrationState(NotesMigrationState expected) throws Exception {
+    gerritConfig.load();
+    assertThat(NotesMigrationState.forNotesMigration(new ConfigNotesMigration(gerritConfig)))
+        .hasValue(expected);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index e4b5ff5..4334555 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -67,6 +67,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Submit;
+import com.google.gerrit.server.change.Submit.TestSubmitInput;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -81,6 +82,7 @@
 import com.google.inject.Inject;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -482,6 +484,43 @@
   }
 
   @Test
+  public void submitReusingOldTopic() throws Exception {
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+
+    String topic = "test-topic";
+    PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "content", topic);
+    PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "content", topic);
+    String id1 = change1.getChangeId();
+    String id2 = change2.getChangeId();
+    approve(id1);
+    approve(id2);
+    assertSubmittedTogether(id1, ImmutableList.of(id1, id2));
+    assertSubmittedTogether(id2, ImmutableList.of(id1, id2));
+    submit(id2);
+
+    String expectedTopic = name(topic);
+    change1.assertChange(Change.Status.MERGED, expectedTopic, admin);
+    change2.assertChange(Change.Status.MERGED, expectedTopic, admin);
+    assertSubmittedTogether(id1, ImmutableList.of(id1, id2));
+    assertSubmittedTogether(id2, ImmutableList.of(id1, id2));
+
+    PushOneCommit.Result change3 = createChange("Change 3", "c.txt", "content", topic);
+    String id3 = change3.getChangeId();
+    approve(id3);
+    assertSubmittedTogether(id3, ImmutableList.of());
+    submit(id3);
+
+    change3.assertChange(Change.Status.MERGED, expectedTopic, admin);
+    assertSubmittedTogether(id3, ImmutableList.of());
+  }
+
+  private void assertSubmittedTogether(String changeId, Iterable<String> expected)
+      throws Exception {
+    assertThat(gApi.changes().id(changeId).submittedTogether().stream().map(i -> i.changeId))
+        .containsExactlyElementsIn(expected);
+  }
+
+  @Test
   public void submitDraftChange() throws Exception {
     PushOneCommit.Result draft = createDraftChange();
     Change.Id num = draft.getChange().getId();
@@ -875,6 +914,81 @@
     assertThat(rw.isMergedInto(fix, master)).isTrue();
   }
 
+  @Test
+  public void retrySubmitSingleChangeOnLockFailure() throws Exception {
+    assume().that(notesMigration.fuseUpdates()).isTrue();
+
+    PushOneCommit.Result change = createChange();
+    String id = change.getChangeId();
+    approve(id);
+
+    TestSubmitInput input = new TestSubmitInput();
+    input.generateLockFailures =
+        new ArrayDeque<>(
+            ImmutableList.of(
+                true, // Attempt 1: lock failure
+                false, // Attempt 2: success
+                false)); // Leftover value to check total number of calls.
+    submit(id, input);
+    assertMerged(id);
+
+    testRepo.git().fetch().call();
+    RevWalk rw = testRepo.getRevWalk();
+    RevCommit master = rw.parseCommit(getRemoteHead(project, "master"));
+    RevCommit patchSet = parseCurrentRevision(rw, change);
+    assertThat(rw.isMergedInto(patchSet, master)).isTrue();
+
+    assertThat(input.generateLockFailures).containsExactly(false);
+  }
+
+  @Test
+  public void retrySubmitAfterTornTopicOnLockFailure() throws Exception {
+    assume().that(notesMigration.fuseUpdates()).isTrue();
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+
+    String topic = "test-topic";
+
+    TestRepository<?> repoA = createProjectWithPush("project-a", null, getSubmitType());
+    TestRepository<?> repoB = createProjectWithPush("project-b", null, getSubmitType());
+
+    PushOneCommit.Result change1 =
+        createChange(repoA, "master", "Change 1", "a.txt", "content", topic);
+    PushOneCommit.Result change2 =
+        createChange(repoB, "master", "Change 2", "b.txt", "content", topic);
+
+    approve(change1.getChangeId());
+    approve(change2.getChangeId());
+
+    TestSubmitInput input = new TestSubmitInput();
+    input.generateLockFailures =
+        new ArrayDeque<>(
+            ImmutableList.of(
+                false, // Change 1, attempt 1: success
+                true, // Change 2, attempt 1: lock failure
+                false, // Change 1, attempt 2: success
+                false, // Change 2, attempt 2: success
+                false)); // Leftover value to check total number of calls.
+    submit(change2.getChangeId(), input);
+
+    String expectedTopic = name(topic);
+    change1.assertChange(Change.Status.MERGED, expectedTopic, admin);
+    change2.assertChange(Change.Status.MERGED, expectedTopic, admin);
+
+    repoA.git().fetch().call();
+    RevWalk rwA = repoA.getRevWalk();
+    RevCommit masterA = rwA.parseCommit(getRemoteHead(name("project-a"), "master"));
+    RevCommit change1Ps = parseCurrentRevision(rwA, change1);
+    assertThat(rwA.isMergedInto(change1Ps, masterA)).isTrue();
+
+    repoB.git().fetch().call();
+    RevWalk rwB = repoB.getRevWalk();
+    RevCommit masterB = rwB.parseCommit(getRemoteHead(name("project-b"), "master"));
+    RevCommit change2Ps = parseCurrentRevision(rwB, change2);
+    assertThat(rwB.isMergedInto(change2Ps, masterB)).isTrue();
+
+    assertThat(input.generateLockFailures).containsExactly(false);
+  }
+
   private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Exception {
     for (PushOneCommit.Result change : changes) {
       try (BatchUpdate bu =
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
index 48499f8..b4d8557 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -138,10 +138,11 @@
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
     Change.Id id2 = change2.getChange().getId();
-    SubmitInput failAfterRefUpdates = new TestSubmitInput(new SubmitInput(), true);
+    TestSubmitInput failInput = new TestSubmitInput();
+    failInput.failAfterRefUpdates = true;
     submit(
         change2.getChangeId(),
-        failAfterRefUpdates,
+        failInput,
         ResourceConflictException.class,
         "Failing after ref updates");
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
index b94b062..5dfc76d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -256,10 +255,11 @@
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
     Change.Id id2 = change2.getChange().getId();
-    SubmitInput failAfterRefUpdates = new TestSubmitInput(new SubmitInput(), true);
+    TestSubmitInput failInput = new TestSubmitInput();
+    failInput.failAfterRefUpdates = true;
     submit(
         change2.getChangeId(),
-        failAfterRefUpdates,
+        failInput,
         ResourceConflictException.class,
         "Failing after ref updates");
     RevCommit headAfterFailedSubmit = getRemoteHead();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index 5235b14..a385932 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -400,10 +400,11 @@
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
     Change.Id id2 = change2.getChange().getId();
-    SubmitInput failAfterRefUpdates = new TestSubmitInput(new SubmitInput(), true);
+    TestSubmitInput failInput = new TestSubmitInput();
+    failInput.failAfterRefUpdates = true;
     submit(
         change2.getChangeId(),
-        failAfterRefUpdates,
+        failInput,
         ResourceConflictException.class,
         "Failing after ref updates");
     RevCommit headAfterFailedSubmit = getRemoteHead();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index a65ba82..d4397d64 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
@@ -151,10 +150,11 @@
 
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     Change.Id id = change.getChange().getId();
-    SubmitInput failAfterRefUpdates = new TestSubmitInput(new SubmitInput(), true);
+    TestSubmitInput failInput = new TestSubmitInput();
+    failInput.failAfterRefUpdates = true;
     submit(
         change.getChangeId(),
-        failAfterRefUpdates,
+        failInput,
         ResourceConflictException.class,
         "Failing after ref updates");
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 3dabced..d52cd98 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -190,6 +190,9 @@
    */
   ChangeEditApi edit() throws RestApiException;
 
+  /** Create a new patch set with a new commit message. */
+  void setMessage(String message) throws RestApiException;
+
   /** Set hashtags on a change */
   void setHashtags(HashtagsInput input) throws RestApiException;
 
@@ -431,6 +434,11 @@
     }
 
     @Override
+    public void setMessage(String message) {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public EditInfo getEdit() {
       throw new NotImplementedException();
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index 93673bc..51340ae 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -133,7 +133,7 @@
     String src = "gerrit_ui/gerrit_ui.nocache.js";
     try (InputStream in = servletContext.getResourceAsStream("/" + src)) {
       if (in != null) {
-        Hasher md = Hashing.md5().newHasher();
+        Hasher md = Hashing.murmur3_128().newHasher();
         byte[] buf = new byte[1024];
         int n;
         while ((n = in.read(buf)) > 0) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index 914c830..150acc6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -312,7 +312,7 @@
       this.lastModified = checkNotNull(lastModified, "lastModified");
       this.contentType = checkNotNull(contentType, "contentType");
       this.raw = checkNotNull(raw, "raw");
-      this.etag = Hashing.md5().hashBytes(raw).toString();
+      this.etag = Hashing.murmur3_128().hashBytes(raw).toString();
     }
 
     boolean isStale(Path p, ResourceServlet rs) throws IOException {
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 7fd3d81..6dbebf1 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -37,10 +37,10 @@
 
   <link rel="icon" type="image/x-icon" href="{$canonicalPath}/favicon.ico">{\n}
 
-  // SourceCodePro fonts are used in styles/fonts.css
+  // RobotoMono fonts are used in styles/fonts.css
   // @see https://github.com/w3c/preload/issues/32 regarding crossorigin
-  <link rel="preload" href="{$staticResourcePath}/fonts/SourceCodePro-Regular.woff2" as="font" type="font/woff2" crossorigin>{\n}
-  <link rel="preload" href="{$staticResourcePath}/fonts/SourceCodePro-Regular.woff" as="font" type="font/woff" crossorigin>{\n}
+  <link rel="preload" href="{$staticResourcePath}/fonts/RobotoMono-Regular.woff2" as="font" type="font/woff2" crossorigin>{\n}
+  <link rel="preload" href="{$staticResourcePath}/fonts/RobotoMono-Regular.woff" as="font" type="font/woff" crossorigin>{\n}
   <link rel="stylesheet" href="{$staticResourcePath}/styles/fonts.css">{\n}
   <link rel="stylesheet" href="{$staticResourcePath}/styles/main.css">{\n}
   <script src="{$staticResourcePath}/bower_components/webcomponentsjs/webcomponents-lite.js"></script>{\n}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index bbc469f..1b29182 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -190,6 +190,11 @@
     sshd = enable;
   }
 
+  @VisibleForTesting
+  public boolean getEnableSshd() {
+    return sshd;
+  }
+
   public void setEnableHttpd(boolean enable) {
     httpd = enable;
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateToNoteDb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateToNoteDb.java
new file mode 100644
index 0000000..9a1cc49
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateToNoteDb.java
@@ -0,0 +1,153 @@
+// Copyright (C) 2014 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.pgm;
+
+import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.pgm.util.BatchProgramModule;
+import com.google.gerrit.pgm.util.RuntimeShutdown;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.pgm.util.ThreadLimiter;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.index.DummyIndexModule;
+import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
+import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import java.util.ArrayList;
+import java.util.List;
+import org.kohsuke.args4j.Option;
+
+public class MigrateToNoteDb extends SiteProgram {
+  @Option(name = "--threads", usage = "Number of threads to use for rebuilding NoteDb")
+  private int threads = Runtime.getRuntime().availableProcessors();
+
+  @Option(
+    name = "--project",
+    usage =
+        "Only rebuild these projects, do no other migration; incompatible with --change;"
+            + " recommended for debugging only"
+  )
+  private List<String> projects = new ArrayList<>();
+
+  @Option(
+    name = "--change",
+    usage =
+        "Only rebuild these changes, do no other migration; incompatible with --project;"
+            + " recommended for debugging only"
+  )
+  private List<Integer> changes = new ArrayList<>();
+
+  @Option(
+    name = "--force",
+    usage =
+        "Force rebuilding changes where ReviewDb is still the source of truth, even if they"
+            + " were previously migrated"
+  )
+  private boolean force;
+
+  @Option(
+    name = "--trial",
+    usage =
+        "trial mode: migrate changes and turn on reading from NoteDb, but leave ReviewDb as"
+            + " the source of truth"
+  )
+  private boolean trial = true; // TODO(dborowitz): Default to false in 3.0.
+
+  private Injector dbInjector;
+  private Injector sysInjector;
+  private LifecycleManager dbManager;
+  private LifecycleManager sysManager;
+
+  @Inject private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
+
+  @Override
+  public int run() throws Exception {
+    RuntimeShutdown.add(this::stop);
+    try {
+      mustHaveValidSite();
+      dbInjector = createDbInjector(MULTI_USER);
+      threads = ThreadLimiter.limitThreads(dbInjector, threads);
+
+      dbManager = new LifecycleManager();
+      dbManager.add(dbInjector);
+      dbManager.start();
+
+      sysInjector = createSysInjector();
+      sysInjector.injectMembers(this);
+      sysManager = new LifecycleManager();
+      sysManager.add(sysInjector);
+      sysManager.start();
+
+      try (NoteDbMigrator migrator =
+          migratorBuilderProvider
+              .get()
+              .setThreads(threads)
+              .setProgressOut(System.err)
+              .setProjects(projects.stream().map(Project.NameKey::new).collect(toList()))
+              .setChanges(changes.stream().map(Change.Id::new).collect(toList()))
+              .setTrialMode(trial)
+              .setForceRebuild(force)
+              .build()) {
+        if (!projects.isEmpty() || !changes.isEmpty()) {
+          migrator.rebuild();
+        } else {
+          migrator.migrate();
+        }
+      }
+    } finally {
+      stop();
+    }
+    return 0;
+  }
+
+  private Injector createSysInjector() {
+    return dbInjector.createChildInjector(
+        new FactoryModule() {
+          @Override
+          public void configure() {
+            install(dbInjector.getInstance(BatchProgramModule.class));
+            DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
+                .to(ReindexAfterRefUpdate.class);
+            install(new DummyIndexModule());
+            factory(ChangeResource.Factory.class);
+          }
+        });
+  }
+
+  private void stop() {
+    try {
+      LifecycleManager m = sysManager;
+      sysManager = null;
+      if (m != null) {
+        m.stop();
+      }
+    } finally {
+      LifecycleManager m = dbManager;
+      dbManager = null;
+      if (m != null) {
+        m.stop();
+      }
+    }
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
deleted file mode 100644
index 90f70de..0000000
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright (C) 2014 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.pgm;
-
-import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
-import static java.util.stream.Collectors.toList;
-
-import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.pgm.util.BatchProgramModule;
-import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.pgm.util.ThreadLimiter;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.index.DummyIndexModule;
-import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.notedb.rebuild.SiteRebuilder;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import java.util.ArrayList;
-import java.util.List;
-import org.kohsuke.args4j.Option;
-
-public class RebuildNoteDb extends SiteProgram {
-  @Option(name = "--threads", usage = "Number of threads to use for rebuilding NoteDb")
-  private int threads = Runtime.getRuntime().availableProcessors();
-
-  @Option(name = "--project", usage = "Projects to rebuild; recommended for debugging only")
-  private List<String> projects = new ArrayList<>();
-
-  @Option(
-    name = "--change",
-    usage = "Individual change numbers to rebuild; recommended for debugging only"
-  )
-  private List<Integer> changes = new ArrayList<>();
-
-  private Injector dbInjector;
-  private Injector sysInjector;
-
-  @Inject private SiteRebuilder.Factory rebuilderFactory;
-
-  @Inject private NotesMigration notesMigration;
-
-  @Override
-  public int run() throws Exception {
-    mustHaveValidSite();
-    dbInjector = createDbInjector(MULTI_USER);
-    threads = ThreadLimiter.limitThreads(dbInjector, threads);
-
-    LifecycleManager dbManager = new LifecycleManager();
-    dbManager.add(dbInjector);
-    dbManager.start();
-
-    sysInjector = createSysInjector();
-    sysInjector.injectMembers(this);
-    if (!notesMigration.enabled()) {
-      throw die("NoteDb is not enabled.");
-    }
-    LifecycleManager sysManager = new LifecycleManager();
-    sysManager.add(sysInjector);
-    sysManager.start();
-
-    System.out.println("Rebuilding the NoteDb");
-
-    try (SiteRebuilder rebuilder =
-        rebuilderFactory.create(
-            threads,
-            projects.stream().map(Project.NameKey::new).collect(toList()),
-            changes.stream().map(Change.Id::new).collect(toList()))) {
-      return rebuilder.rebuild() ? 0 : 1;
-    }
-  }
-
-  private Injector createSysInjector() {
-    return dbInjector.createChildInjector(
-        new FactoryModule() {
-          @Override
-          public void configure() {
-            install(dbInjector.getInstance(BatchProgramModule.class));
-            DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
-                .to(ReindexAfterRefUpdate.class);
-            install(new DummyIndexModule());
-            factory(ChangeResource.Factory.class);
-          }
-        });
-  }
-}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
index 737e538..256164a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
@@ -20,6 +20,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
 
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
@@ -69,7 +70,6 @@
   private static final Pattern URI_PATTERN = Pattern.compile(FILTER_RE);
 
   public static class Module extends ServletModule {
-
     @Override
     protected void configureServlets() {
       bind(QueueProvider.class).to(CommandExecutorQueueProvider.class).in(SINGLETON);
@@ -77,18 +77,20 @@
     }
   }
 
+  private final CapabilityControl.Factory capabilityFactory;
   private final Provider<CurrentUser> user;
   private final QueueProvider queue;
-
   private final ServletContext context;
   private final long maxWait;
 
   @Inject
   ProjectQoSFilter(
+      CapabilityControl.Factory capabilityFactory,
       Provider<CurrentUser> user,
       QueueProvider queue,
       ServletContext context,
       @GerritServerConfig Config cfg) {
+    this.capabilityFactory = capabilityFactory;
     this.user = user;
     this.queue = queue;
     this.context = context;
@@ -137,7 +139,8 @@
   }
 
   private ScheduledThreadPoolExecutor getExecutor() {
-    return queue.getQueue(user.get().getCapabilities().getQueueType());
+    QueueProvider.QueueType qt = capabilityFactory.create(user.get()).getQueueType();
+    return queue.getQueue(qt);
   }
 
   @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
index fb4e0eb..e0589ed 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
@@ -280,7 +280,7 @@
       System.err.flush();
       return;
     }
-    Hasher h = Hashing.sha1().newHasher();
+    Hasher h = Hashing.murmur3_128().newHasher();
     try (InputStream in = Files.newInputStream(dst);
         OutputStream out = Funnels.asOutputStream(h)) {
       ByteStreams.copy(in, out);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index c051bff..788f7ce 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.account.AccountVisibility;
 import com.google.gerrit.server.account.AccountVisibilityProvider;
 import com.google.gerrit.server.account.CapabilityCollection;
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.FakeRealm;
 import com.google.gerrit.server.account.GroupCacheImpl;
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
@@ -164,7 +163,6 @@
     install(MergeabilityCacheImpl.module());
     install(TagCache.module());
     factory(CapabilityCollection.Factory.class);
-    factory(CapabilityControl.Factory.class);
     factory(ChangeData.Factory.class);
     factory(ProjectState.Factory.class);
 
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
index 5952880..d331347 100755
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
@@ -45,6 +45,10 @@
 #   If set to "0" disables using start-stop-daemon.  This may need to
 #   be set on SuSE systems.
 
+if test -f /lib/lsb/init-functions ; then
+  . /lib/lsb/init-functions
+fi
+
 usage() {
     me=`basename "$0"`
     echo >&2 "Usage: $me {start|stop|restart|check|status|run|supervise|threads} [-d site]"
diff --git a/gerrit-plugin-api/BUILD b/gerrit-plugin-api/BUILD
index d17bda6..cc01802 100644
--- a/gerrit-plugin-api/BUILD
+++ b/gerrit-plugin-api/BUILD
@@ -43,6 +43,7 @@
     "//lib:args4j",
     "//lib:blame-cache",
     "//lib:guava",
+    "//lib:guava-retrying",
     "//lib:gson",
     "//lib:gwtorm",
     "//lib:icu4j",
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java
index 9791572..d4c6354 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.reviewdb.server;
 
-import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -82,8 +81,10 @@
       throw new UnsupportedOperationException(MSG);
     }
 
+    @SuppressWarnings("deprecation")
     @Override
-    public CheckedFuture<Change, OrmException> getAsync(Change.Id key) {
+    public com.google.common.util.concurrent.CheckedFuture<Change, OrmException> getAsync(
+        Change.Id key) {
       throw new UnsupportedOperationException(MSG);
     }
 
@@ -113,8 +114,10 @@
       throw new UnsupportedOperationException(MSG);
     }
 
+    @SuppressWarnings("deprecation")
     @Override
-    public CheckedFuture<PatchSetApproval, OrmException> getAsync(PatchSetApproval.Key key) {
+    public com.google.common.util.concurrent.CheckedFuture<PatchSetApproval, OrmException> getAsync(
+        PatchSetApproval.Key key) {
       throw new UnsupportedOperationException(MSG);
     }
 
@@ -149,8 +152,10 @@
       throw new UnsupportedOperationException(MSG);
     }
 
+    @SuppressWarnings("deprecation")
     @Override
-    public CheckedFuture<ChangeMessage, OrmException> getAsync(ChangeMessage.Key key) {
+    public com.google.common.util.concurrent.CheckedFuture<ChangeMessage, OrmException> getAsync(
+        ChangeMessage.Key key) {
       throw new UnsupportedOperationException(MSG);
     }
 
@@ -190,8 +195,10 @@
       throw new UnsupportedOperationException(MSG);
     }
 
+    @SuppressWarnings("deprecation")
     @Override
-    public CheckedFuture<PatchSet, OrmException> getAsync(PatchSet.Id key) {
+    public com.google.common.util.concurrent.CheckedFuture<PatchSet, OrmException> getAsync(
+        PatchSet.Id key) {
       throw new UnsupportedOperationException(MSG);
     }
 
@@ -221,8 +228,10 @@
       throw new UnsupportedOperationException(MSG);
     }
 
+    @SuppressWarnings("deprecation")
     @Override
-    public CheckedFuture<PatchLineComment, OrmException> getAsync(PatchLineComment.Key key) {
+    public com.google.common.util.concurrent.CheckedFuture<PatchLineComment, OrmException> getAsync(
+        PatchLineComment.Key key) {
       throw new UnsupportedOperationException(MSG);
     }
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
index 3b22889..c8c4fb2 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -190,8 +189,10 @@
       return delegate.toMap(c);
     }
 
+    @SuppressWarnings("deprecation")
     @Override
-    public CheckedFuture<Change, OrmException> getAsync(Change.Id key) {
+    public com.google.common.util.concurrent.CheckedFuture<Change, OrmException> getAsync(
+        Change.Id key) {
       return delegate.getAsync(key);
     }
 
@@ -278,8 +279,10 @@
       return delegate.toMap(c);
     }
 
+    @SuppressWarnings("deprecation")
     @Override
-    public CheckedFuture<PatchSetApproval, OrmException> getAsync(PatchSetApproval.Key key) {
+    public com.google.common.util.concurrent.CheckedFuture<PatchSetApproval, OrmException> getAsync(
+        PatchSetApproval.Key key) {
       return delegate.getAsync(key);
     }
 
@@ -384,8 +387,10 @@
       return delegate.toMap(c);
     }
 
+    @SuppressWarnings("deprecation")
     @Override
-    public CheckedFuture<ChangeMessage, OrmException> getAsync(ChangeMessage.Key key) {
+    public com.google.common.util.concurrent.CheckedFuture<ChangeMessage, OrmException> getAsync(
+        ChangeMessage.Key key) {
       return delegate.getAsync(key);
     }
 
@@ -483,8 +488,10 @@
       return delegate.toMap(c);
     }
 
+    @SuppressWarnings("deprecation")
     @Override
-    public CheckedFuture<PatchSet, OrmException> getAsync(PatchSet.Id key) {
+    public com.google.common.util.concurrent.CheckedFuture<PatchSet, OrmException> getAsync(
+        PatchSet.Id key) {
       return delegate.getAsync(key);
     }
 
@@ -577,8 +584,10 @@
       return delegate.toMap(c);
     }
 
+    @SuppressWarnings("deprecation")
     @Override
-    public CheckedFuture<PatchLineComment, OrmException> getAsync(PatchLineComment.Key key) {
+    public com.google.common.util.concurrent.CheckedFuture<PatchLineComment, OrmException> getAsync(
+        PatchLineComment.Key key) {
       return delegate.getAsync(key);
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
index de8e9a4..c96d61a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
@@ -14,20 +14,13 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.inject.Inject;
 import java.util.Collections;
 
 /** An anonymous user who has not yet authenticated. */
 public class AnonymousUser extends CurrentUser {
-  @Inject
-  AnonymousUser(CapabilityControl.Factory capabilityControlFactory) {
-    super(capabilityControlFactory);
-  }
-
   @Override
   public GroupMembership getEffectiveGroups() {
     return new ListGroupMembership(Collections.singleton(SystemGroupBackend.ANONYMOUS_USERS));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index 7e1be17..7ac8fda 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.inject.servlet.RequestScoped;
@@ -40,16 +39,9 @@
     private PropertyKey() {}
   }
 
-  private final CapabilityControl.Factory capabilityControlFactory;
   private AccessPath accessPath = AccessPath.UNKNOWN;
-
-  private CapabilityControl capabilities;
   private PropertyKey<ExternalId.Key> lastLoginExternalIdPropertyKey = PropertyKey.create();
 
-  protected CurrentUser(CapabilityControl.Factory capabilityControlFactory) {
-    this.capabilityControlFactory = capabilityControlFactory;
-  }
-
   /** How this user is accessing the Gerrit Code Review application. */
   public final AccessPath getAccessPath() {
     return accessPath;
@@ -98,14 +90,6 @@
     return null;
   }
 
-  /** Capabilities available to this user account. */
-  public CapabilityControl getCapabilities() {
-    if (capabilities == null) {
-      capabilities = capabilityControlFactory.create(this);
-    }
-    return capabilities;
-  }
-
   /** Check if user is the IdentifiedUser */
   public boolean isIdentifiedUser() {
     return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index 5121a6f..e39dc69 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
@@ -55,7 +54,6 @@
   /** Create an IdentifiedUser, ignoring any per-request state. */
   @Singleton
   public static class GenericFactory {
-    private final CapabilityControl.Factory capabilityControlFactory;
     private final AuthConfig authConfig;
     private final Realm realm;
     private final String anonymousCowardName;
@@ -66,7 +64,6 @@
 
     @Inject
     public GenericFactory(
-        @Nullable CapabilityControl.Factory capabilityControlFactory,
         AuthConfig authConfig,
         Realm realm,
         @AnonymousCowardName String anonymousCowardName,
@@ -74,7 +71,6 @@
         @DisableReverseDnsLookup Boolean disableReverseDnsLookup,
         AccountCache accountCache,
         GroupBackend groupBackend) {
-      this.capabilityControlFactory = capabilityControlFactory;
       this.authConfig = authConfig;
       this.realm = realm;
       this.anonymousCowardName = anonymousCowardName;
@@ -86,7 +82,6 @@
 
     public IdentifiedUser create(AccountState state) {
       return new IdentifiedUser(
-          capabilityControlFactory,
           authConfig,
           realm,
           anonymousCowardName,
@@ -110,7 +105,6 @@
     public IdentifiedUser runAs(
         SocketAddress remotePeer, Account.Id id, @Nullable CurrentUser caller) {
       return new IdentifiedUser(
-          capabilityControlFactory,
           authConfig,
           realm,
           anonymousCowardName,
@@ -132,7 +126,6 @@
    */
   @Singleton
   public static class RequestFactory {
-    private final CapabilityControl.Factory capabilityControlFactory;
     private final AuthConfig authConfig;
     private final Realm realm;
     private final String anonymousCowardName;
@@ -144,7 +137,6 @@
 
     @Inject
     RequestFactory(
-        CapabilityControl.Factory capabilityControlFactory,
         AuthConfig authConfig,
         Realm realm,
         @AnonymousCowardName String anonymousCowardName,
@@ -153,7 +145,6 @@
         GroupBackend groupBackend,
         @DisableReverseDnsLookup Boolean disableReverseDnsLookup,
         @RemotePeer Provider<SocketAddress> remotePeerProvider) {
-      this.capabilityControlFactory = capabilityControlFactory;
       this.authConfig = authConfig;
       this.realm = realm;
       this.anonymousCowardName = anonymousCowardName;
@@ -166,7 +157,6 @@
 
     public IdentifiedUser create(Account.Id id) {
       return new IdentifiedUser(
-          capabilityControlFactory,
           authConfig,
           realm,
           anonymousCowardName,
@@ -181,7 +171,6 @@
 
     public IdentifiedUser runAs(Account.Id id, CurrentUser caller) {
       return new IdentifiedUser(
-          capabilityControlFactory,
           authConfig,
           realm,
           anonymousCowardName,
@@ -219,7 +208,6 @@
   private Map<PropertyKey<Object>, Object> properties;
 
   private IdentifiedUser(
-      CapabilityControl.Factory capabilityControlFactory,
       AuthConfig authConfig,
       Realm realm,
       String anonymousCowardName,
@@ -231,7 +219,6 @@
       AccountState state,
       @Nullable CurrentUser realUser) {
     this(
-        capabilityControlFactory,
         authConfig,
         realm,
         anonymousCowardName,
@@ -246,7 +233,6 @@
   }
 
   private IdentifiedUser(
-      CapabilityControl.Factory capabilityControlFactory,
       AuthConfig authConfig,
       Realm realm,
       String anonymousCowardName,
@@ -257,7 +243,6 @@
       @Nullable Provider<SocketAddress> remotePeerProvider,
       Account.Id id,
       @Nullable CurrentUser realUser) {
-    super(capabilityControlFactory);
     this.canonicalUrl = canonicalUrl;
     this.accountCache = accountCache;
     this.groupBackend = groupBackend;
@@ -465,7 +450,6 @@
    * @return copy of the identified user
    */
   public IdentifiedUser materializedCopy() {
-    CapabilityControl capabilities = getCapabilities();
     Provider<SocketAddress> remotePeer;
     try {
       remotePeer = Providers.of(remotePeerProvider.get());
@@ -479,13 +463,6 @@
           };
     }
     return new IdentifiedUser(
-        new CapabilityControl.Factory() {
-
-          @Override
-          public CapabilityControl create(CurrentUser user) {
-            return capabilities;
-          }
-        },
         authConfig,
         realm,
         anonymousCowardName,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
index bc99ec1..821a0c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -14,10 +14,7 @@
 
 package com.google.gerrit.server;
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
-import com.google.inject.Inject;
 
 /**
  * User identity for plugin code that needs an identity.
@@ -33,12 +30,6 @@
     InternalUser create();
   }
 
-  @VisibleForTesting
-  @Inject
-  public InternalUser(CapabilityControl.Factory capabilityControlFactory) {
-    super(capabilityControlFactory);
-  }
-
   @Override
   public GroupMembership getEffectiveGroups() {
     return GroupMembership.EMPTY;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
index 263bb50..8a8b67a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -32,9 +31,7 @@
   private final SocketAddress peer;
 
   @Inject
-  protected PeerDaemonUser(
-      CapabilityControl.Factory capabilityControlFactory, @Assisted SocketAddress peer) {
-    super(capabilityControlFactory);
+  protected PeerDaemonUser(@Assisted SocketAddress peer) {
     this.peer = peer;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.java
index 13e04c5..09f5043 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -27,9 +26,7 @@
   private final String pluginName;
 
   @Inject
-  protected PluginUser(
-      CapabilityControl.Factory capabilityControlFactory, @Assisted String pluginName) {
-    super(capabilityControlFactory);
+  protected PluginUser(@Assisted String pluginName) {
     this.pluginName = pluginName;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
index c6abb5b..bb118a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
@@ -18,12 +18,16 @@
 
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.AccountsSection;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -32,6 +36,7 @@
 /** Access control management for one account's access to other accounts. */
 public class AccountControl {
   public static class Factory {
+    private final PermissionBackend permissionBackend;
     private final ProjectCache projectCache;
     private final GroupControl.Factory groupControlFactory;
     private final Provider<CurrentUser> user;
@@ -40,11 +45,13 @@
 
     @Inject
     Factory(
-        final ProjectCache projectCache,
-        final GroupControl.Factory groupControlFactory,
-        final Provider<CurrentUser> user,
-        final IdentifiedUser.GenericFactory userFactory,
-        final AccountVisibility accountVisibility) {
+        PermissionBackend permissionBackend,
+        ProjectCache projectCache,
+        GroupControl.Factory groupControlFactory,
+        Provider<CurrentUser> user,
+        IdentifiedUser.GenericFactory userFactory,
+        AccountVisibility accountVisibility) {
+      this.permissionBackend = permissionBackend;
       this.projectCache = projectCache;
       this.groupControlFactory = groupControlFactory;
       this.user = user;
@@ -54,24 +61,34 @@
 
     public AccountControl get() {
       return new AccountControl(
-          projectCache, groupControlFactory, user.get(), userFactory, accountVisibility);
+          permissionBackend,
+          projectCache,
+          groupControlFactory,
+          user.get(),
+          userFactory,
+          accountVisibility);
     }
   }
 
   private final AccountsSection accountsSection;
   private final GroupControl.Factory groupControlFactory;
+  private final PermissionBackend.WithUser perm;
   private final CurrentUser user;
   private final IdentifiedUser.GenericFactory userFactory;
   private final AccountVisibility accountVisibility;
 
+  private Boolean viewAll;
+
   AccountControl(
-      final ProjectCache projectCache,
-      final GroupControl.Factory groupControlFactory,
-      final CurrentUser user,
-      final IdentifiedUser.GenericFactory userFactory,
-      final AccountVisibility accountVisibility) {
+      PermissionBackend permissionBackend,
+      ProjectCache projectCache,
+      GroupControl.Factory groupControlFactory,
+      CurrentUser user,
+      IdentifiedUser.GenericFactory userFactory,
+      AccountVisibility accountVisibility) {
     this.accountsSection = projectCache.getAllProjects().getConfig().getAccountsSection();
     this.groupControlFactory = groupControlFactory;
+    this.perm = permissionBackend.user(user);
     this.user = user;
     this.userFactory = userFactory;
     this.accountVisibility = accountVisibility;
@@ -137,17 +154,16 @@
   }
 
   private boolean canSee(OtherUser otherUser) {
-    // Special case: I can always see myself.
-    if (user.isIdentifiedUser() && user.getAccountId().equals(otherUser.getId())) {
+    if (accountVisibility == AccountVisibility.ALL) {
       return true;
-    }
-    if (user.getCapabilities().canViewAllAccounts()) {
+    } else if (user.isIdentifiedUser() && user.getAccountId().equals(otherUser.getId())) {
+      // I can always see myself.
+      return true;
+    } else if (viewAll()) {
       return true;
     }
 
     switch (accountVisibility) {
-      case ALL:
-        return true;
       case SAME_GROUP:
         {
           Set<AccountGroup.UUID> usersGroups = groupsOf(otherUser.getUser());
@@ -178,12 +194,25 @@
         }
       case NONE:
         break;
+      case ALL:
       default:
         throw new IllegalStateException("Bad AccountVisibility " + accountVisibility);
     }
     return false;
   }
 
+  private boolean viewAll() {
+    if (viewAll == null) {
+      try {
+        perm.check(GlobalPermission.VIEW_ALL_ACCOUNTS);
+        viewAll = true;
+      } catch (AuthException | PermissionBackendException e) {
+        viewAll = false;
+      }
+    }
+    return viewAll;
+  }
+
   private Set<AccountGroup.UUID> groupsOf(IdentifiedUser user) {
     return user.getEffectiveGroups()
         .getKnownGroups()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index bb40c26..03ffa67 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -30,12 +30,9 @@
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
 import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
+import com.google.inject.Singleton;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -45,41 +42,40 @@
 
 /** Access control management for server-wide capabilities. */
 public class CapabilityControl {
-  public interface Factory {
-    CapabilityControl create(CurrentUser user);
+  private static final CurrentUser.PropertyKey<CapabilityControl> SELF =
+      CurrentUser.PropertyKey.create();
+
+  @Singleton
+  public static class Factory {
+    private final ProjectCache projectCache;
+
+    @Inject
+    Factory(ProjectCache projectCache) {
+      this.projectCache = projectCache;
+    }
+
+    public CapabilityControl create(CurrentUser user) {
+      CapabilityControl ctl = user.get(SELF);
+      if (ctl == null) {
+        ctl = new CapabilityControl(projectCache, user);
+        user.put(SELF, ctl);
+      }
+      return ctl;
+    }
   }
 
   private final CapabilityCollection capabilities;
   private final CurrentUser user;
   private final Map<String, List<PermissionRule>> effective;
-
   private Boolean canAdministrateServer;
-  private Boolean canEmailReviewers;
 
-  @Inject
-  CapabilityControl(ProjectCache projectCache, @Assisted CurrentUser currentUser) {
+  private CapabilityControl(ProjectCache projectCache, CurrentUser currentUser) {
     capabilities = projectCache.getAllProjects().getCapabilityCollection();
     user = currentUser;
     effective = new HashMap<>();
   }
 
-  /**
-   * <b>Do not use.</b> Determine if the user can administer this server.
-   *
-   * <p>This method is visible only for the benefit of the following transitional classes:
-   *
-   * <ul>
-   *   <li>{@link ProjectControl}
-   *   <li>{@link RefControl}
-   *   <li>{@link ChangeControl}
-   *   <li>{@link GroupControl}
-   * </ul>
-   *
-   * Other callers should not use this method, as it is slated to go away.
-   *
-   * @return true if the user can administer this server.
-   */
-  public boolean isAdmin_DoNotUse() {
+  private boolean isAdmin() {
     if (canAdministrateServer == null) {
       if (user.getRealUser() != user) {
         canAdministrateServer = false;
@@ -93,18 +89,9 @@
   }
 
   /** @return true if the user can email reviewers. */
-  public boolean canEmailReviewers() {
-    if (canEmailReviewers == null) {
-      canEmailReviewers =
-          matchAny(capabilities.emailReviewers, ALLOWED_RULE)
-              || !matchAny(capabilities.emailReviewers, not(ALLOWED_RULE));
-    }
-    return canEmailReviewers;
-  }
-
-  /** @return true if the user can view all accounts. */
-  public boolean canViewAllAccounts() {
-    return canPerform(GlobalCapability.VIEW_ALL_ACCOUNTS) || isAdmin_DoNotUse();
+  private boolean canEmailReviewers() {
+    return matchAny(capabilities.emailReviewers, ALLOWED_RULE)
+        || !matchAny(capabilities.emailReviewers, not(ALLOWED_RULE));
   }
 
   /** @return which priority queue the user's tasks should be submitted to. */
@@ -226,7 +213,7 @@
     } else if (perm instanceof PluginPermission) {
       PluginPermission pluginPermission = (PluginPermission) perm;
       return canPerform(pluginPermission.permissionName())
-          || (pluginPermission.fallBackToAdmin() && isAdmin_DoNotUse());
+          || (pluginPermission.fallBackToAdmin() && isAdmin());
     }
     throw new PermissionBackendException(perm + " unsupported");
   }
@@ -234,11 +221,9 @@
   private boolean can(GlobalPermission perm) throws PermissionBackendException {
     switch (perm) {
       case ADMINISTRATE_SERVER:
-        return isAdmin_DoNotUse();
+        return isAdmin();
       case EMAIL_REVIEWERS:
         return canEmailReviewers();
-      case VIEW_ALL_ACCOUNTS:
-        return canViewAllAccounts();
 
       case FLUSH_CACHES:
       case KILL_TASK:
@@ -247,7 +232,7 @@
       case VIEW_QUEUE:
         return canPerform(perm.permissionName())
             || canPerform(GlobalCapability.MAINTAIN_SERVER)
-            || isAdmin_DoNotUse();
+            || isAdmin();
 
       case CREATE_ACCOUNT:
       case CREATE_GROUP:
@@ -255,9 +240,10 @@
       case MAINTAIN_SERVER:
       case MODIFY_ACCOUNT:
       case STREAM_EVENTS:
+      case VIEW_ALL_ACCOUNTS:
       case VIEW_CONNECTIONS:
       case VIEW_PLUGINS:
-        return canPerform(perm.permissionName()) || isAdmin_DoNotUse();
+        return canPerform(perm.permissionName()) || isAdmin();
 
       case ACCESS_DATABASE:
       case RUN_AS:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index f519b6c..fe01b32 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -56,15 +56,18 @@
   private Set<String> query;
 
   private final PermissionBackend permissionBackend;
+  private final CapabilityControl.Factory capabilityFactory;
   private final Provider<CurrentUser> self;
   private final DynamicMap<CapabilityDefinition> pluginCapabilities;
 
   @Inject
   GetCapabilities(
       PermissionBackend permissionBackend,
+      CapabilityControl.Factory capabilityFactory,
       Provider<CurrentUser> self,
       DynamicMap<CapabilityDefinition> pluginCapabilities) {
     this.permissionBackend = permissionBackend;
+    this.capabilityFactory = capabilityFactory;
     this.self = self;
     this.pluginCapabilities = pluginCapabilities;
   }
@@ -81,8 +84,10 @@
     for (GlobalOrPluginPermission p : perm.test(permissionsToTest())) {
       have.put(p.permissionName(), true);
     }
-    addRanges(have, rsrc);
-    addPriority(have, rsrc);
+
+    CapabilityControl cc = capabilityFactory.create(rsrc.getUser());
+    addRanges(have, cc);
+    addPriority(have, cc);
 
     return OutputFormat.JSON
         .newGson()
@@ -112,8 +117,7 @@
     return query == null || query.contains(name.toLowerCase());
   }
 
-  private void addRanges(Map<String, Object> have, AccountResource rsrc) {
-    CapabilityControl cc = rsrc.getUser().getCapabilities();
+  private void addRanges(Map<String, Object> have, CapabilityControl cc) {
     for (String name : GlobalCapability.getRangeNames()) {
       if (want(name) && cc.hasExplicitRange(name)) {
         have.put(name, new Range(cc.getRange(name)));
@@ -121,8 +125,8 @@
     }
   }
 
-  private void addPriority(Map<String, Object> have, AccountResource rsrc) {
-    QueueProvider.QueueType queue = rsrc.getUser().getCapabilities().getQueueType();
+  private void addPriority(Map<String, Object> have, CapabilityControl cc) {
+    QueueProvider.QueueType queue = cc.getQueueType();
     if (queue != QueueProvider.QueueType.INTERACTIVE
         || (query != null && query.contains(PRIORITY))) {
       have.put(PRIORITY, queue);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index 4b4c266..6721e11 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -17,9 +17,13 @@
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -29,30 +33,38 @@
 
   @Singleton
   public static class GenericFactory {
+    private final PermissionBackend permissionBackend;
     private final GroupBackend groupBackend;
 
     @Inject
-    GenericFactory(GroupBackend gb) {
+    GenericFactory(PermissionBackend permissionBackend, GroupBackend gb) {
+      this.permissionBackend = permissionBackend;
       groupBackend = gb;
     }
 
     public GroupControl controlFor(CurrentUser who, AccountGroup.UUID groupId)
         throws NoSuchGroupException {
-      final GroupDescription.Basic group = groupBackend.get(groupId);
+      GroupDescription.Basic group = groupBackend.get(groupId);
       if (group == null) {
         throw new NoSuchGroupException(groupId);
       }
-      return new GroupControl(who, group, groupBackend);
+      return new GroupControl(who, group, permissionBackend, groupBackend);
     }
   }
 
   public static class Factory {
+    private final PermissionBackend permissionBackend;
     private final GroupCache groupCache;
     private final Provider<CurrentUser> user;
     private final GroupBackend groupBackend;
 
     @Inject
-    Factory(GroupCache gc, Provider<CurrentUser> cu, GroupBackend gb) {
+    Factory(
+        PermissionBackend permissionBackend,
+        GroupCache gc,
+        Provider<CurrentUser> cu,
+        GroupBackend gb) {
+      this.permissionBackend = permissionBackend;
       groupCache = gc;
       user = cu;
       groupBackend = gb;
@@ -79,7 +91,7 @@
     }
 
     public GroupControl controlFor(GroupDescription.Basic group) {
-      return new GroupControl(user.get(), group, groupBackend);
+      return new GroupControl(user.get(), group, permissionBackend, groupBackend);
     }
 
     public GroupControl validateFor(AccountGroup.Id groupId) throws NoSuchGroupException {
@@ -102,11 +114,17 @@
   private final CurrentUser user;
   private final GroupDescription.Basic group;
   private Boolean isOwner;
+  private final PermissionBackend.WithUser perm;
   private final GroupBackend groupBackend;
 
-  GroupControl(CurrentUser who, GroupDescription.Basic gd, GroupBackend gb) {
+  GroupControl(
+      CurrentUser who,
+      GroupDescription.Basic gd,
+      PermissionBackend permissionBackend,
+      GroupBackend gb) {
     user = who;
     group = gd;
+    this.perm = permissionBackend.user(user);
     groupBackend = gb;
   }
 
@@ -127,8 +145,8 @@
     return user.isInternalUser()
         || groupBackend.isVisibleToAll(group.getGroupUUID())
         || user.getEffectiveGroups().contains(group.getGroupUUID())
-        || user.getCapabilities().isAdmin_DoNotUse()
-        || isOwner();
+        || isOwner()
+        || canAdministrateServer();
   }
 
   public boolean isOwner() {
@@ -137,13 +155,20 @@
       isOwner = false;
     } else if (isOwner == null) {
       AccountGroup.UUID ownerUUID = accountGroup.getOwnerGroupUUID();
-      isOwner =
-          getUser().getEffectiveGroups().contains(ownerUUID)
-              || getUser().getCapabilities().isAdmin_DoNotUse();
+      isOwner = getUser().getEffectiveGroups().contains(ownerUUID) || canAdministrateServer();
     }
     return isOwner;
   }
 
+  private boolean canAdministrateServer() {
+    try {
+      perm.check(GlobalPermission.ADMINISTRATE_SERVER);
+      return true;
+    } catch (AuthException | PermissionBackendException denied) {
+      return false;
+    }
+  }
+
   public boolean canAddMember() {
     return isOwner();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 4aabf59..9c456f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -96,12 +96,13 @@
      * Returns the SHA1 of the external ID that is used as note ID in the refs/meta/external-ids
      * notes branch.
      */
+    @SuppressWarnings("deprecation") // Use Hashing.sha1 for compatibility.
     public ObjectId sha1() {
       return ObjectId.fromRaw(Hashing.sha1().hashString(get(), UTF_8).asBytes());
     }
 
     /**
-     * Exports this external ID key as string with the format "scheme:id", or "id" id scheme is
+     * Exports this external ID key as string with the format "scheme:id", or "id" if scheme is
      * null.
      *
      * <p>This string representation is used as subsection name in the Git config file that stores
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index f4ea3b0..4f4af54 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -71,6 +71,7 @@
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.PublishDraftPatchSet;
 import com.google.gerrit.server.change.PutAssignee;
+import com.google.gerrit.server.change.PutMessage;
 import com.google.gerrit.server.change.PutTopic;
 import com.google.gerrit.server.change.Rebase;
 import com.google.gerrit.server.change.Restore;
@@ -139,6 +140,7 @@
   private final Unmute unmute;
   private final SetWorkInProgress setWip;
   private final SetReadyForReview setReady;
+  private final PutMessage putMessage;
 
   @Inject
   ChangeApiImpl(
@@ -182,6 +184,7 @@
       Unmute unmute,
       SetWorkInProgress setWip,
       SetReadyForReview setReady,
+      PutMessage putMessage,
       @Assisted ChangeResource change) {
     this.changeApi = changeApi;
     this.revert = revert;
@@ -223,6 +226,7 @@
     this.unmute = unmute;
     this.setWip = setWip;
     this.setReady = setReady;
+    this.putMessage = putMessage;
     this.change = change;
   }
 
@@ -511,6 +515,17 @@
   }
 
   @Override
+  public void setMessage(String in) throws RestApiException {
+    try {
+      PutMessage.Input input = new PutMessage.Input();
+      input.message = in;
+      putMessage.apply(change, input);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot edit commit message", e);
+    }
+  }
+
+  @Override
   public ChangeInfo info() throws RestApiException {
     return get(EnumSet.noneOf(ListChangesOption.class));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
index 1bec3d1..f27c53b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -137,7 +137,7 @@
   @Override
   public String getETag() {
     CurrentUser user = control.getUser();
-    Hasher h = Hashing.md5().newHasher();
+    Hasher h = Hashing.murmur3_128().newHasher();
     if (user.isIdentifiedUser()) {
       h.putString(starredChangesUtil.getObjectId(user.getAccountId(), getId()).name(), UTF_8);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
index 0c2ec68..181505d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -273,7 +273,7 @@
     // an attacker could upload a *.class file and have us send a ZIP
     // that can be invoked through an applet tag in the victim's browser.
     //
-    Hasher h = Hashing.md5().newHasher();
+    Hasher h = Hashing.murmur3_128().newHasher();
     byte[] buf = new byte[8];
 
     NB.encodeInt64(buf, 0, TimeUtil.nowMs());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
index e476e73..8a6a1ab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
@@ -63,7 +63,7 @@
 
   @Override
   public String getETag(RevisionResource rsrc) {
-    Hasher h = Hashing.md5().newHasher();
+    Hasher h = Hashing.murmur3_128().newHasher();
     CurrentUser user = rsrc.getControl().getUser();
     try {
       rsrc.getChangeResource().prepareETag(h, user);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index db31c17..c66469c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -94,6 +94,7 @@
     put(CHANGE_KIND, "unmute").to(Unmute.class);
     post(CHANGE_KIND, "wip").to(SetWorkInProgress.class);
     post(CHANGE_KIND, "ready").to(SetReadyForReview.class);
+    put(CHANGE_KIND, "message").to(PutMessage.class);
 
     post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
     get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 40c6e06..bebccc4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -745,7 +745,7 @@
           comment.key.patchSetId,
           comment.lineNbr,
           Side.fromShort(comment.side),
-          Hashing.sha1().hashString(comment.message, UTF_8),
+          Hashing.murmur3_128().hashString(comment.message, UTF_8),
           comment.range);
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
index ab3478b..53f127e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
@@ -36,8 +36,6 @@
 import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -60,8 +58,7 @@
   private static final int MAX_DEFAULT_BUNDLE_SIZE = 100 * 1024 * 1024;
 
   private final Provider<ReviewDb> dbProvider;
-  private final RetryHelper retryHelper;
-  private final MergeOp.Factory mergeOpFactory;
+  private final Provider<MergeOp> mergeOpProvider;
   private final AllowedFormats allowedFormats;
   private int maxBundleSize;
   private String format;
@@ -74,60 +71,53 @@
   @Inject
   PreviewSubmit(
       Provider<ReviewDb> dbProvider,
-      RetryHelper retryHelper,
-      MergeOp.Factory mergeOpFactory,
+      Provider<MergeOp> mergeOpProvider,
       AllowedFormats allowedFormats,
       @GerritServerConfig Config cfg) {
     this.dbProvider = dbProvider;
-    this.retryHelper = retryHelper;
-    this.mergeOpFactory = mergeOpFactory;
+    this.mergeOpProvider = mergeOpProvider;
     this.allowedFormats = allowedFormats;
     this.maxBundleSize = cfg.getInt("download", "maxBundleSize", MAX_DEFAULT_BUNDLE_SIZE);
   }
 
   @Override
-  public BinaryResult apply(RevisionResource rsrc) throws UpdateException, RestApiException {
-    // Shouldn't ever need to retry, since we are doing a dry-run BatchUpdate, but this is required
-    // to get access to a BatchUpdate.Factory.
-    return retryHelper.execute(
-        (updateFactory) -> {
-          if (Strings.isNullOrEmpty(format)) {
-            throw new BadRequestException("format is not specified");
-          }
-          ArchiveFormat f = allowedFormats.extensions.get("." + format);
-          if (f == null && format.equals("tgz")) {
-            // Always allow tgz, even when the allowedFormats doesn't contain it.
-            // Then we allow at least one format even if the list of allowed
-            // formats is empty.
-            f = ArchiveFormat.TGZ;
-          }
-          if (f == null) {
-            throw new BadRequestException("unknown archive format");
-          }
+  public BinaryResult apply(RevisionResource rsrc)
+      throws OrmException, RestApiException, UpdateException {
+    if (Strings.isNullOrEmpty(format)) {
+      throw new BadRequestException("format is not specified");
+    }
+    ArchiveFormat f = allowedFormats.extensions.get("." + format);
+    if (f == null && format.equals("tgz")) {
+      // Always allow tgz, even when the allowedFormats doesn't contain it.
+      // Then we allow at least one format even if the list of allowed
+      // formats is empty.
+      f = ArchiveFormat.TGZ;
+    }
+    if (f == null) {
+      throw new BadRequestException("unknown archive format");
+    }
 
-          Change change = rsrc.getChange();
-          if (!change.getStatus().isOpen()) {
-            throw new PreconditionFailedException("change is " + ChangeUtil.status(change));
-          }
-          ChangeControl control = rsrc.getControl();
-          if (!control.getUser().isIdentifiedUser()) {
-            throw new MethodNotAllowedException("Anonymous users cannot submit");
-          }
+    Change change = rsrc.getChange();
+    if (!change.getStatus().isOpen()) {
+      throw new PreconditionFailedException("change is " + ChangeUtil.status(change));
+    }
+    ChangeControl control = rsrc.getControl();
+    if (!control.getUser().isIdentifiedUser()) {
+      throw new MethodNotAllowedException("Anonymous users cannot submit");
+    }
 
-          return getBundles(updateFactory, rsrc, f);
-        });
+    return getBundles(rsrc, f);
   }
 
-  private BinaryResult getBundles(
-      BatchUpdate.Factory updateFactory, RevisionResource rsrc, ArchiveFormat f)
-      throws OrmException, RestApiException {
+  private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormat f)
+      throws OrmException, RestApiException, UpdateException {
     ReviewDb db = dbProvider.get();
     ChangeControl control = rsrc.getControl();
     IdentifiedUser caller = control.getUser().asIdentifiedUser();
     Change change = rsrc.getChange();
 
     @SuppressWarnings("resource") // Returned BinaryResult takes ownership and handles closing.
-    MergeOp op = mergeOpFactory.create(updateFactory);
+    MergeOp op = mergeOpProvider.get();
     try {
       op.merge(db, change, caller, false, new SubmitInput(), true);
       BinaryResult bin = new SubmitPreviewResult(op, f, maxBundleSize);
@@ -135,7 +125,7 @@
           .setContentType(f.getMimeType())
           .setAttachmentName("submit-preview-" + change.getChangeId() + "." + format);
       return bin;
-    } catch (OrmException | RestApiException | RuntimeException e) {
+    } catch (OrmException | RestApiException | UpdateException | RuntimeException e) {
       op.close();
       throw e;
     }
@@ -162,8 +152,7 @@
           BundleWriter bw = new BundleWriter(or.getCodeReviewRevWalk().getObjectReader());
           bw.setObjectCountCallback(null);
           bw.setPackConfig(new PackConfig(or.getRepo()));
-          Collection<ReceiveCommand> refs =
-              or.getUpdate(mergeOp.getBatchUpdateFactory()).getRefUpdates().values();
+          Collection<ReceiveCommand> refs = or.getUpdate().getRefUpdates().values();
           for (ReceiveCommand r : refs) {
             bw.include(r.getRefName(), r.getNewId());
             ObjectId oldId = r.getOldId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
new file mode 100644
index 0000000..0935c71
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
@@ -0,0 +1,217 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.edit.UnchangedCommitMessageException;
+import com.google.gerrit.server.git.GitRepositoryManager;
+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.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.CommitMessageUtil;
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+@Singleton
+public class PutMessage
+    extends RetryingRestModifyView<ChangeResource, PutMessage.Input, Response<?>> {
+
+  public static class Input {
+    @DefaultInput public String message;
+
+    public NotifyHandling notify = NotifyHandling.ALL;
+
+    public Map<RecipientType, NotifyInfo> notifyDetails;
+  }
+
+  private final GitRepositoryManager repositoryManager;
+  private final Provider<CurrentUser> currentUserProvider;
+  private final Provider<ReviewDb> db;
+  private final TimeZone tz;
+  private final PatchSetInserter.Factory psInserterFactory;
+  private final PermissionBackend permissionBackend;
+  private final PatchSetUtil psUtil;
+  private final NotifyUtil notifyUtil;
+
+  @Inject
+  PutMessage(
+      RetryHelper retryHelper,
+      GitRepositoryManager repositoryManager,
+      Provider<CurrentUser> currentUserProvider,
+      Provider<ReviewDb> db,
+      PatchSetInserter.Factory psInserterFactory,
+      PermissionBackend permissionBackend,
+      @GerritPersonIdent PersonIdent gerritIdent,
+      PatchSetUtil psUtil,
+      NotifyUtil notifyUtil) {
+    super(retryHelper);
+    this.repositoryManager = repositoryManager;
+    this.currentUserProvider = currentUserProvider;
+    this.db = db;
+    this.psInserterFactory = psInserterFactory;
+    this.tz = gerritIdent.getTimeZone();
+    this.permissionBackend = permissionBackend;
+    this.psUtil = psUtil;
+    this.notifyUtil = notifyUtil;
+  }
+
+  @Override
+  protected Response<String> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource resource, Input input)
+      throws IOException, UnchangedCommitMessageException, RestApiException, UpdateException,
+          PermissionBackendException, OrmException {
+    PatchSet ps = psUtil.current(db.get(), resource.getNotes());
+    if (ps == null) {
+      throw new ResourceConflictException("current revision is missing");
+    } else if (!resource.getControl().isPatchVisible(ps, db.get())) {
+      throw new AuthException("current revision not accessible");
+    }
+
+    if (input == null) {
+      throw new BadRequestException("input cannot be null");
+    }
+    String sanitizedCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(input.message);
+
+    ensureCanEditCommitMessage(resource.getControl().getNotes());
+    ensureChangeIdIsCorrect(
+        resource.getControl().getProjectControl().getProjectState().isRequireChangeID(),
+        resource.getChange().getKey().get(),
+        sanitizedCommitMessage);
+
+    try (Repository repository = repositoryManager.openRepository(resource.getProject());
+        RevWalk revWalk = new RevWalk(repository);
+        ObjectInserter objectInserter = repository.newObjectInserter()) {
+      RevCommit patchSetCommit = revWalk.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+
+      String currentCommitMessage = patchSetCommit.getFullMessage();
+      if (input.equals(currentCommitMessage)) {
+        throw new ResourceConflictException("new and existing commit message are the same");
+      }
+
+      Timestamp ts = TimeUtil.nowTs();
+      try (BatchUpdate bu =
+          updateFactory.create(
+              db.get(), resource.getChange().getProject(), currentUserProvider.get(), ts)) {
+        // Ensure that BatchUpdate will update the same repo
+        bu.setRepository(repository, new RevWalk(objectInserter.newReader()), objectInserter);
+
+        PatchSet.Id psId = ChangeUtil.nextPatchSetId(repository, ps.getId());
+        ObjectId newCommit =
+            createCommit(objectInserter, patchSetCommit, sanitizedCommitMessage, ts);
+        PatchSetInserter inserter =
+            psInserterFactory.create(resource.getControl(), psId, newCommit);
+        inserter.setMessage(
+            String.format("Patch Set %s: Commit message was updated.", psId.getId()));
+        inserter.setNotify(input.notify);
+        inserter.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+        bu.addOp(resource.getChange().getId(), inserter);
+        bu.execute();
+      }
+    }
+    return Response.ok("ok");
+  }
+
+  private ObjectId createCommit(
+      ObjectInserter objectInserter,
+      RevCommit basePatchSetCommit,
+      String commitMessage,
+      Timestamp timestamp)
+      throws IOException {
+    CommitBuilder builder = new CommitBuilder();
+    builder.setTreeId(basePatchSetCommit.getTree());
+    builder.setParentIds(basePatchSetCommit.getParents());
+    builder.setAuthor(basePatchSetCommit.getAuthorIdent());
+    builder.setCommitter(
+        currentUserProvider.get().asIdentifiedUser().newCommitterIdent(timestamp, tz));
+    builder.setMessage(commitMessage);
+    ObjectId newCommitId = objectInserter.insert(builder);
+    objectInserter.flush();
+    return newCommitId;
+  }
+
+  private void ensureCanEditCommitMessage(ChangeNotes changeNotes)
+      throws AuthException, PermissionBackendException {
+    if (!currentUserProvider.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+    try {
+      permissionBackend
+          .user(currentUserProvider.get())
+          .database(db.get())
+          .change(changeNotes)
+          .check(ChangePermission.ADD_PATCH_SET);
+    } catch (AuthException denied) {
+      throw new AuthException("modifying commit message not permitted", denied);
+    }
+  }
+
+  private static void ensureChangeIdIsCorrect(
+      boolean requireChangeId, String currentChangeId, String newCommitMessage)
+      throws ResourceConflictException, BadRequestException {
+    RevCommit revCommit =
+        RevCommit.parse(
+            Constants.encode("tree " + ObjectId.zeroId().name() + "\n\n" + newCommitMessage));
+
+    // Check that the commit message without footers is not empty
+    CommitMessageUtil.checkAndSanitizeCommitMessage(revCommit.getShortMessage());
+
+    List<String> changeIdFooters = revCommit.getFooterLines(FooterConstants.CHANGE_ID);
+    if (requireChangeId && changeIdFooters.isEmpty()) {
+      throw new ResourceConflictException("missing Change-Id footer");
+    }
+    if (!changeIdFooters.isEmpty() && !changeIdFooters.get(0).equals(currentChangeId)) {
+      throw new ResourceConflictException("wrong Change-Id footer");
+    }
+    if (changeIdFooters.size() > 1) {
+      throw new ResourceConflictException("multiple Change-Id footers");
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index a86767a..3fb8a79 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -45,7 +46,6 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.account.AccountsCollection;
-import com.google.gerrit.server.change.Submit.Output;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.ChangeSet;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -58,9 +58,7 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
+import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.inject.Inject;
@@ -73,6 +71,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 import java.util.Set;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
@@ -84,8 +83,8 @@
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class Submit extends RetryingRestModifyView<RevisionResource, SubmitInput, Output>
-    implements UiAction<RevisionResource> {
+public class Submit
+    implements RestModifyView<RevisionResource, SubmitInput>, UiAction<RevisionResource> {
   private static final Logger log = LoggerFactory.getLogger(Submit.class);
 
   private static final String DEFAULT_TOOLTIP = "Submit patch set ${patchSet} into ${branch}";
@@ -119,13 +118,14 @@
    */
   @VisibleForTesting
   public static class TestSubmitInput extends SubmitInput {
-    public final boolean failAfterRefUpdates;
+    public boolean failAfterRefUpdates;
 
-    public TestSubmitInput(SubmitInput base, boolean failAfterRefUpdates) {
-      this.onBehalfOf = base.onBehalfOf;
-      this.notify = base.notify;
-      this.failAfterRefUpdates = failAfterRefUpdates;
-    }
+    /**
+     * For each change being submitted, an element is removed from this queue and, if the value is
+     * true, a bogus ref update is added to the batch, in order to generate a lock failure during
+     * execution.
+     */
+    public Queue<Boolean> generateLockFailures;
   }
 
   private final Provider<ReviewDb> dbProvider;
@@ -134,7 +134,7 @@
   private final ChangeData.Factory changeDataFactory;
   private final ChangeMessagesUtil cmUtil;
   private final ChangeNotes.Factory changeNotesFactory;
-  private final MergeOp.Factory mergeOpFactory;
+  private final Provider<MergeOp> mergeOpProvider;
   private final Provider<MergeSuperSet> mergeSuperSet;
   private final AccountsCollection accounts;
   private final String label;
@@ -152,24 +152,22 @@
       Provider<ReviewDb> dbProvider,
       GitRepositoryManager repoManager,
       PermissionBackend permissionBackend,
-      RetryHelper retryHelper,
       ChangeData.Factory changeDataFactory,
       ChangeMessagesUtil cmUtil,
       ChangeNotes.Factory changeNotesFactory,
-      MergeOp.Factory mergeOpFactory,
+      Provider<MergeOp> mergeOpProvider,
       Provider<MergeSuperSet> mergeSuperSet,
       AccountsCollection accounts,
       @GerritServerConfig Config cfg,
       Provider<InternalChangeQuery> queryProvider,
       PatchSetUtil psUtil) {
-    super(retryHelper);
     this.dbProvider = dbProvider;
     this.repoManager = repoManager;
     this.permissionBackend = permissionBackend;
     this.changeDataFactory = changeDataFactory;
     this.cmUtil = cmUtil;
     this.changeNotesFactory = changeNotesFactory;
-    this.mergeOpFactory = mergeOpFactory;
+    this.mergeOpProvider = mergeOpProvider;
     this.mergeSuperSet = mergeSuperSet;
     this.accounts = accounts;
     this.label =
@@ -202,10 +200,9 @@
   }
 
   @Override
-  protected Output applyImpl(
-      BatchUpdate.Factory updateFactory, RevisionResource rsrc, SubmitInput input)
+  public Output apply(RevisionResource rsrc, SubmitInput input)
       throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
-          PermissionBackendException {
+          PermissionBackendException, UpdateException {
     input.onBehalfOf = Strings.emptyToNull(input.onBehalfOf);
     IdentifiedUser submitter;
     if (input.onBehalfOf != null) {
@@ -215,15 +212,11 @@
       submitter = rsrc.getUser().asIdentifiedUser();
     }
 
-    return new Output(mergeChange(updateFactory, rsrc, submitter, input));
+    return new Output(mergeChange(rsrc, submitter, input));
   }
 
-  public Change mergeChange(
-      BatchUpdate.Factory updateFactory,
-      RevisionResource rsrc,
-      IdentifiedUser submitter,
-      SubmitInput input)
-      throws OrmException, RestApiException, IOException {
+  public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
+      throws OrmException, RestApiException, IOException, UpdateException {
     Change change = rsrc.getChange();
     if (!change.getStatus().isOpen()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
@@ -237,7 +230,7 @@
               "revision %s is not current revision", rsrc.getPatchSet().getRevision().get()));
     }
 
-    try (MergeOp op = mergeOpFactory.create(updateFactory)) {
+    try (MergeOp op = mergeOpProvider.get()) {
       ReviewDb db = dbProvider.get();
       op.merge(db, change, submitter, true, input, false);
       try {
@@ -291,7 +284,7 @@
         if (c.change().isWorkInProgress()) {
           return BLOCKED_WORK_IN_PROGRESS;
         }
-        MergeOp.checkSubmitRule(c);
+        MergeOp.checkSubmitRule(c, false);
       }
 
       Collection<ChangeData> unmergeable = unmergeableChanges(cs);
@@ -328,7 +321,7 @@
               && resource.isCurrent()
               && !resource.getPatchSet().isDraft()
               && resource.permissions().test(ChangePermission.SUBMIT);
-      MergeOp.checkSubmitRule(cd);
+      MergeOp.checkSubmitRule(cd, false);
     } catch (ResourceConflictException e) {
       visible = false;
     } catch (PermissionBackendException e) {
@@ -513,8 +506,7 @@
     }
   }
 
-  public static class CurrentRevision
-      extends RetryingRestModifyView<ChangeResource, SubmitInput, ChangeInfo> {
+  public static class CurrentRevision implements RestModifyView<ChangeResource, SubmitInput> {
     private final Provider<ReviewDb> dbProvider;
     private final Submit submit;
     private final ChangeJson.Factory json;
@@ -523,11 +515,9 @@
     @Inject
     CurrentRevision(
         Provider<ReviewDb> dbProvider,
-        RetryHelper retryHelper,
         Submit submit,
         ChangeJson.Factory json,
         PatchSetUtil psUtil) {
-      super(retryHelper);
       this.dbProvider = dbProvider;
       this.submit = submit;
       this.json = json;
@@ -535,10 +525,9 @@
     }
 
     @Override
-    protected ChangeInfo applyImpl(
-        BatchUpdate.Factory updateFactory, ChangeResource rsrc, SubmitInput input)
+    public ChangeInfo apply(ChangeResource rsrc, SubmitInput input)
         throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
-            PermissionBackendException {
+            PermissionBackendException, UpdateException {
       PatchSet ps = psUtil.current(dbProvider.get(), rsrc.getNotes());
       if (ps == null) {
         throw new ResourceConflictException("current revision is missing");
@@ -546,7 +535,7 @@
         throw new AuthException("current revision not accessible");
       }
 
-      Output out = submit.applyImpl(updateFactory, new RevisionResource(rsrc, ps), input);
+      Output out = submit.apply(new RevisionResource(rsrc, ps), input);
       return json.noOptions().format(out.change);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 366cbd1..6bd7c57 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -85,7 +85,6 @@
 import com.google.gerrit.server.account.AccountVisibility;
 import com.google.gerrit.server.account.AccountVisibilityProvider;
 import com.google.gerrit.server.account.CapabilityCollection;
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.ChangeUserName;
 import com.google.gerrit.server.account.EmailExpander;
 import com.google.gerrit.server.account.GroupCacheImpl;
@@ -115,7 +114,6 @@
 import com.google.gerrit.server.git.EmailMerge;
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.GitModules;
-import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.MergedByPushOp;
 import com.google.gerrit.server.git.NotesBranchUtil;
@@ -246,7 +244,6 @@
     factory(DeleteReviewerSender.Factory.class);
     factory(AddKeySender.Factory.class);
     factory(CapabilityCollection.Factory.class);
-    factory(CapabilityControl.Factory.class);
     factory(ChangeData.Factory.class);
     factory(ChangeJson.AssistedFactory.class);
     factory(CreateChangeSender.Factory.class);
@@ -401,7 +398,6 @@
     factory(MergedByPushOp.Factory.class);
     factory(GitModules.Factory.class);
     factory(VersionedAuthorizedKeys.Factory.class);
-    factory(MergeOp.Factory.class);
 
     bind(AccountManager.class);
     factory(ChangeUserName.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerIdProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerIdProvider.java
index 83b60e2f1..dd84d78 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerIdProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerIdProvider.java
@@ -46,11 +46,11 @@
       return;
     }
 
-    // We're not generally supposed to do work in provider constructors, but
-    // this is a bit of a special case because we really need to have the ID
-    // available by the time the dbInjector is created. This even applies during
-    // RebuildNoteDb, which otherwise would have been a reasonable place to do
-    // the ID generation. Fortunately, it's not much work, and it happens once.
+    // We're not generally supposed to do work in provider constructors, but this is a bit of a
+    // special case because we really need to have the ID available by the time the dbInjector
+    // is created. This even applies during MigrateToNoteDb, which otherwise would have been a
+    // reasonable place to do the ID generation. Fortunately, it's not much work, and it happens
+    // once.
     id = generate();
     Config newCfg = readGerritConfig(sitePaths);
     newCfg.setString(SECTION, null, KEY, id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 8d3cbae..79dc9c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -14,12 +14,10 @@
 
 package com.google.gerrit.server.edit;
 
-import static com.google.common.base.Preconditions.checkState;
-
-import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.reviewdb.client.Change;
@@ -42,6 +40,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -205,13 +204,14 @@
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
    * @throws UnchangedCommitMessageException if the commit message is the same as before
    * @throws PermissionBackendException
+   * @throws BadRequestException if the commit message is malformed
    */
   public void modifyMessage(
       Repository repository, ChangeControl changeControl, String newCommitMessage)
       throws AuthException, IOException, UnchangedCommitMessageException, OrmException,
-          PermissionBackendException {
+          PermissionBackendException, BadRequestException {
     assertCanEdit(changeControl);
-    newCommitMessage = getWellFormedCommitMessage(newCommitMessage);
+    newCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(newCommitMessage);
 
     Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
     PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, changeControl);
@@ -435,13 +435,6 @@
     }
   }
 
-  private static String getWellFormedCommitMessage(String commitMessage) {
-    String wellFormedMessage = Strings.nullToEmpty(commitMessage).trim();
-    checkState(!wellFormedMessage.isEmpty(), "Commit message cannot be null or empty");
-    wellFormedMessage = wellFormedMessage + "\n";
-    return wellFormedMessage;
-  }
-
   private Optional<ChangeEdit> lookupChangeEdit(ChangeControl changeControl)
       throws AuthException, IOException {
     return changeEditUtil.byChange(changeControl);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
index 29570a7..322d158 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
@@ -30,7 +30,6 @@
 import java.util.Date;
 import java.util.List;
 import java.util.TimeZone;
-import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.Constants;
@@ -87,7 +86,7 @@
 
   public BanCommitResult ban(
       ProjectControl projectControl, List<ObjectId> commitsToBan, String reason)
-      throws PermissionDeniedException, IOException, ConcurrentRefUpdateException {
+      throws PermissionDeniedException, LockFailureException, IOException {
     if (!projectControl.isOwner()) {
       throw new PermissionDeniedException("Not project owner: not permitted to ban commits");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
index 03d44ca..d4fa9ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import java.util.Collection;
@@ -91,6 +92,14 @@
     return changeData.values();
   }
 
+  public ImmutableSet<Project.NameKey> projects() throws OrmException {
+    ImmutableSet.Builder<Project.NameKey> ret = ImmutableSet.builder();
+    for (ChangeData cd : changeData.values()) {
+      ret.add(cd.project());
+    }
+    return ret.build();
+  }
+
   public ImmutableSet<Change.Id> nonVisibleIds() {
     return nonVisibleChanges.keySet();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 28cdebf..ce6b5c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -20,6 +20,8 @@
 import static java.util.Comparator.comparing;
 import static java.util.stream.Collectors.toSet;
 
+import com.github.rholder.retry.Attempt;
+import com.github.rholder.retry.RetryListener;
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableListMultimap;
@@ -69,12 +71,13 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.RequestId;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
@@ -108,18 +111,17 @@
   private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
 
   private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS = SubmitRuleOptions.defaults().build();
-
-  public interface Factory {
-    MergeOp create(BatchUpdate.Factory batchUpdateFactory);
-  }
+  private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_ALLOW_CLOSED =
+      SUBMIT_RULE_OPTIONS.toBuilder().allowClosed(true).build();
 
   public static class CommitStatus {
     private final ImmutableMap<Change.Id, ChangeData> changes;
     private final ImmutableSetMultimap<Branch.NameKey, Change.Id> byBranch;
     private final Map<Change.Id, CodeReviewCommit> commits;
     private final ListMultimap<Change.Id, String> problems;
+    private final boolean allowClosed;
 
-    private CommitStatus(ChangeSet cs) throws OrmException {
+    private CommitStatus(ChangeSet cs, boolean allowClosed) throws OrmException {
       checkArgument(
           !cs.furtherHiddenChanges(), "CommitStatus must not be called with hidden changes");
       changes = cs.changesById();
@@ -130,6 +132,7 @@
       byBranch = bb.build();
       commits = new HashMap<>();
       problems = MultimapBuilder.treeKeys(comparing(Change.Id::get)).arrayListValues(1).build();
+      this.allowClosed = allowClosed;
     }
 
     public ImmutableSet<Change.Id> getChangeIds() {
@@ -182,7 +185,7 @@
       // date by this point.
       ChangeData cd = checkNotNull(changes.get(id), "ChangeData for %s", id);
       return checkNotNull(
-          cd.getSubmitRecords(SUBMIT_RULE_OPTIONS),
+          cd.getSubmitRecords(submitRuleOptions(allowClosed)),
           "getSubmitRecord only valid after submit rules are evalutated");
     }
 
@@ -226,13 +229,15 @@
   private final InternalChangeQuery internalChangeQuery;
   private final SubmitStrategyFactory submitStrategyFactory;
   private final SubmoduleOp.Factory subOpFactory;
-  private final MergeOpRepoManager orm;
+  private final Provider<MergeOpRepoManager> ormProvider;
   private final NotifyUtil notifyUtil;
+  private final RetryHelper retryHelper;
 
   private Timestamp ts;
   private RequestId submissionId;
   private IdentifiedUser caller;
 
+  private MergeOpRepoManager orm;
   private CommitStatus commitStatus;
   private ReviewDb db;
   private SubmitInput submitInput;
@@ -244,40 +249,45 @@
   @Inject
   MergeOp(
       ChangeMessagesUtil cmUtil,
+      BatchUpdate.Factory batchUpdateFactory,
       InternalUser.Factory internalUserFactory,
       MergeSuperSet mergeSuperSet,
       MergeValidators.Factory mergeValidatorsFactory,
       InternalChangeQuery internalChangeQuery,
       SubmitStrategyFactory submitStrategyFactory,
       SubmoduleOp.Factory subOpFactory,
-      MergeOpRepoManager orm,
+      Provider<MergeOpRepoManager> ormProvider,
       NotifyUtil notifyUtil,
       TopicMetrics topicMetrics,
-      @Assisted BatchUpdate.Factory batchUpdateFactory) {
+      RetryHelper retryHelper) {
     this.cmUtil = cmUtil;
+    this.batchUpdateFactory = batchUpdateFactory;
     this.internalUserFactory = internalUserFactory;
     this.mergeSuperSet = mergeSuperSet;
     this.mergeValidatorsFactory = mergeValidatorsFactory;
     this.internalChangeQuery = internalChangeQuery;
     this.submitStrategyFactory = submitStrategyFactory;
     this.subOpFactory = subOpFactory;
-    this.orm = orm;
+    this.ormProvider = ormProvider;
     this.notifyUtil = notifyUtil;
-    this.batchUpdateFactory = batchUpdateFactory;
+    this.retryHelper = retryHelper;
     this.topicMetrics = topicMetrics;
   }
 
   @Override
   public void close() {
-    orm.close();
+    if (orm != null) {
+      orm.close();
+    }
   }
 
-  public static void checkSubmitRule(ChangeData cd) throws ResourceConflictException, OrmException {
+  public static void checkSubmitRule(ChangeData cd, boolean allowClosed)
+      throws ResourceConflictException, OrmException {
     PatchSet patchSet = cd.currentPatchSet();
     if (patchSet == null) {
       throw new ResourceConflictException("missing current patch set for change " + cd.getId());
     }
-    List<SubmitRecord> results = getSubmitRecords(cd);
+    List<SubmitRecord> results = getSubmitRecords(cd, allowClosed);
     if (SubmitRecord.findOkRecord(results).isPresent()) {
       // Rules supplied a valid solution.
       return;
@@ -311,8 +321,13 @@
     throw new IllegalStateException();
   }
 
-  private static List<SubmitRecord> getSubmitRecords(ChangeData cd) throws OrmException {
-    return cd.submitRecords(SUBMIT_RULE_OPTIONS);
+  private static SubmitRuleOptions submitRuleOptions(boolean allowClosed) {
+    return allowClosed ? SUBMIT_RULE_OPTIONS_ALLOW_CLOSED : SUBMIT_RULE_OPTIONS;
+  }
+
+  private static List<SubmitRecord> getSubmitRecords(ChangeData cd, boolean allowClosed)
+      throws OrmException {
+    return cd.submitRecords(submitRuleOptions(allowClosed));
   }
 
   private static String describeLabels(ChangeData cd, List<SubmitRecord.Label> labels)
@@ -346,19 +361,23 @@
     return Joiner.on("; ").join(labelResults);
   }
 
-  private void checkSubmitRulesAndState(ChangeSet cs) throws ResourceConflictException {
+  private void checkSubmitRulesAndState(ChangeSet cs, boolean allowMerged)
+      throws ResourceConflictException {
     checkArgument(
         !cs.furtherHiddenChanges(), "checkSubmitRulesAndState called for topic with hidden change");
     for (ChangeData cd : cs.changes()) {
       try {
-        if (cd.change().getStatus() != Change.Status.NEW) {
-          commitStatus.problem(
-              cd.getId(),
-              "Change " + cd.getId() + " is " + cd.change().getStatus().toString().toLowerCase());
+        Change.Status status = cd.change().getStatus();
+        if (status != Change.Status.NEW) {
+          if (!(status == Change.Status.MERGED && allowMerged)) {
+            commitStatus.problem(
+                cd.getId(),
+                "Change " + cd.getId() + " is " + cd.change().getStatus().toString().toLowerCase());
+          }
         } else if (cd.change().isWorkInProgress()) {
           commitStatus.problem(cd.getId(), "Change " + cd.getId() + " is work in progress");
         } else {
-          checkSubmitRule(cd);
+          checkSubmitRule(cd, allowMerged);
         }
       } catch (ResourceConflictException e) {
         commitStatus.problem(cd.getId(), e.getMessage());
@@ -371,13 +390,13 @@
     commitStatus.maybeFailVerbose();
   }
 
-  private void bypassSubmitRules(ChangeSet cs) {
+  private void bypassSubmitRules(ChangeSet cs, boolean allowClosed) {
     checkArgument(
         !cs.furtherHiddenChanges(), "cannot bypass submit rules for topic with hidden change");
     for (ChangeData cd : cs.changes()) {
       List<SubmitRecord> records;
       try {
-        records = new ArrayList<>(getSubmitRecords(cd));
+        records = new ArrayList<>(getSubmitRecords(cd, allowClosed));
       } catch (OrmException e) {
         log.warn("Error checking submit rules for change " + cd.getId(), e);
         records = new ArrayList<>(1);
@@ -385,7 +404,7 @@
       SubmitRecord forced = new SubmitRecord();
       forced.status = SubmitRecord.Status.FORCED;
       records.add(forced);
-      cd.setSubmitRecords(SUBMIT_RULE_OPTIONS, records);
+      cd.setSubmitRecords(submitRuleOptions(allowClosed), records);
     }
   }
 
@@ -411,7 +430,7 @@
       boolean checkSubmitRules,
       SubmitInput submitInput,
       boolean dryrun)
-      throws OrmException, RestApiException {
+      throws OrmException, RestApiException, UpdateException {
     this.submitInput = submitInput;
     this.accountsToNotify = notifyUtil.resolveAccounts(submitInput.notifyDetails);
     this.dryrun = dryrun;
@@ -419,7 +438,7 @@
     this.ts = TimeUtil.nowTs();
     submissionId = RequestId.forChange(change);
     this.db = db;
-    orm.setContext(db, ts, caller, submissionId);
+    openRepoManager();
 
     logDebug("Beginning integration of {}", change);
     try {
@@ -430,21 +449,47 @@
         throw new AuthException(
             "A change to be submitted with " + change.getId() + " is not visible");
       }
-      this.commitStatus = new CommitStatus(cs);
-      MergeSuperSet.reloadChanges(cs);
       logDebug("Calculated to merge {}", cs);
-      if (checkSubmitRules) {
-        logDebug("Checking submit rules and state");
-        checkSubmitRulesAndState(cs);
-      } else {
-        logDebug("Bypassing submit rules");
-        bypassSubmitRules(cs);
+
+      // Count cross-project submissions outside of the retry loop. The chance of a single project
+      // failing increases with the number of projects, so the failure count would be inflated if
+      // this metric were incremented inside of integrateIntoHistory.
+      int projects = cs.projects().size();
+      if (projects > 1) {
+        topicMetrics.topicSubmissions.increment();
       }
-      try {
-        integrateIntoHistory(cs);
-      } catch (IntegrationException e) {
-        logError("Error from integrateIntoHistory", e);
-        throw new ResourceConflictException(e.getMessage(), e);
+
+      RetryTracker retryTracker = new RetryTracker();
+      retryHelper.execute(
+          updateFactory -> {
+            long attempt = retryTracker.lastAttemptNumber + 1;
+            boolean isRetry = attempt > 1;
+            if (isRetry) {
+              logDebug("Retrying, attempt #{}; skipping merged changes", attempt);
+              this.ts = TimeUtil.nowTs();
+              openRepoManager();
+            }
+            this.commitStatus = new CommitStatus(cs, isRetry);
+            MergeSuperSet.reloadChanges(cs);
+            if (checkSubmitRules) {
+              logDebug("Checking submit rules and state");
+              checkSubmitRulesAndState(cs, isRetry);
+            } else {
+              logDebug("Bypassing submit rules");
+              bypassSubmitRules(cs, isRetry);
+            }
+            try {
+              integrateIntoHistory(cs);
+            } catch (IntegrationException e) {
+              logError("Error from integrateIntoHistory", e);
+              throw new ResourceConflictException(e.getMessage(), e);
+            }
+            return null;
+          },
+          retryTracker);
+
+      if (projects > 1) {
+        topicMetrics.topicSubmissionsCompleted.increment();
       }
     } catch (IOException e) {
       // Anything before the merge attempt is an error
@@ -452,6 +497,23 @@
     }
   }
 
+  private void openRepoManager() {
+    if (orm != null) {
+      orm.close();
+    }
+    orm = ormProvider.get();
+    orm.setContext(db, ts, caller, submissionId);
+  }
+
+  private class RetryTracker implements RetryListener {
+    long lastAttemptNumber;
+
+    @Override
+    public <V> void onRetry(Attempt<V> attempt) {
+      lastAttemptNumber = attempt.getAttemptNumber();
+    }
+  }
+
   @Singleton
   private static class TopicMetrics {
     final Counter0 topicSubmissions;
@@ -471,7 +533,8 @@
     }
   }
 
-  private void integrateIntoHistory(ChangeSet cs) throws IntegrationException, RestApiException {
+  private void integrateIntoHistory(ChangeSet cs)
+      throws IntegrationException, RestApiException, UpdateException {
     checkArgument(!cs.furtherHiddenChanges(), "cannot integrate hidden changes into history");
     logDebug("Beginning merge attempt on {}", cs);
     Map<Branch.NameKey, BranchBatch> toSubmit = new HashMap<>();
@@ -484,30 +547,22 @@
     }
     Set<Branch.NameKey> branches = cbb.keySet();
 
-    int projects = 0;
     for (Branch.NameKey branch : branches) {
       OpenRepo or = openRepo(branch.getParentKey());
       if (or != null) {
-        BranchBatch bb = validateChangeList(or, cbb.get(branch));
-        toSubmit.put(branch, bb);
-        if (!bb.commits().isEmpty()) {
-          projects++;
-        }
+        toSubmit.put(branch, validateChangeList(or, cbb.get(branch)));
       }
     }
 
     // Done checks that don't involve running submit strategies.
     commitStatus.maybeFailVerbose();
 
-    if (projects > 1) {
-      topicMetrics.topicSubmissions.increment();
-    }
     try {
       SubmoduleOp submoduleOp = subOpFactory.create(branches, orm);
       List<SubmitStrategy> strategies = getSubmitStrategies(toSubmit, submoduleOp, dryrun);
       this.allProjects = submoduleOp.getProjectsInOrder();
       batchUpdateFactory.execute(
-          orm.batchUpdates(batchUpdateFactory, allProjects),
+          orm.batchUpdates(allProjects),
           new SubmitStrategyListener(submitInput, strategies, commitStatus),
           submissionId,
           dryrun);
@@ -516,6 +571,15 @@
     } catch (IOException | SubmoduleException e) {
       throw new IntegrationException(e);
     } catch (UpdateException e) {
+      if (e.getCause() instanceof LockFailureException) {
+        // Lock failures are a special case: RetryHelper depends on this specific causal chain in
+        // order to trigger a retry. The downside of throwing here is we will not get the nicer
+        // error message constructed below, in the case where this is the final attempt and the
+        // operation is not retried further. This is not a huge downside, and is hopefully so rare
+        // as to be unnoticeable, assuming RetryHelper is retrying sufficiently.
+        throw e;
+      }
+
       // BatchUpdate may have inadvertently wrapped an IntegrationException
       // thrown by some legacy SubmitStrategyOp code that intended the error
       // message to be user-visible. Copy the message from the wrapped
@@ -531,10 +595,6 @@
       }
       throw new IntegrationException(msg, e);
     }
-
-    if (projects > 1) {
-      topicMetrics.topicSubmissionsCompleted.increment();
-    }
   }
 
   public Set<Project.NameKey> getAllProjects() {
@@ -545,10 +605,6 @@
     return orm;
   }
 
-  public BatchUpdate.Factory getBatchUpdateFactory() {
-    return batchUpdateFactory;
-  }
-
   private List<SubmitStrategy> getSubmitStrategies(
       Map<Branch.NameKey, BranchBatch> toSubmit, SubmoduleOp submoduleOp, boolean dryrun)
       throws IntegrationException, NoSuchProjectException, IOException {
@@ -580,20 +636,20 @@
                 ob.mergeTip,
                 commitStatus,
                 submissionId,
-                submitInput.notify,
+                submitInput,
                 accountsToNotify,
                 submoduleOp,
                 dryrun);
         strategies.add(strategy);
-        strategy.addOps(or.getUpdate(batchUpdateFactory), commitsToSubmit);
+        strategy.addOps(or.getUpdate(), commitsToSubmit);
         if (submitting.submitType().equals(SubmitType.FAST_FORWARD_ONLY)
             && submoduleOp.hasSubscription(branch)) {
-          submoduleOp.addOp(or.getUpdate(batchUpdateFactory), branch);
+          submoduleOp.addOp(or.getUpdate(), branch);
         }
       } else {
         // no open change for this branch
         // add submodule triggered op into BatchUpdate
-        submoduleOp.addOp(or.getUpdate(batchUpdateFactory), branch);
+        submoduleOp.addOp(or.getUpdate(), branch);
       }
     }
     return strategies;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
index ae2c481..ad205f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -101,7 +101,7 @@
       return rw;
     }
 
-    public BatchUpdate getUpdate(BatchUpdate.Factory batchUpdateFactory) {
+    public BatchUpdate getUpdate() {
       checkState(db != null, "call setContext before getUpdate");
       if (update == null) {
         update =
@@ -149,6 +149,7 @@
   }
 
   private final Map<Project.NameKey, OpenRepo> openRepos;
+  private final BatchUpdate.Factory batchUpdateFactory;
   private final OnSubmitValidators.Factory onSubmitValidatorsFactory;
   private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
@@ -162,9 +163,11 @@
   MergeOpRepoManager(
       GitRepositoryManager repoManager,
       ProjectCache projectCache,
+      BatchUpdate.Factory batchUpdateFactory,
       OnSubmitValidators.Factory onSubmitValidatorsFactory) {
     this.repoManager = repoManager;
     this.projectCache = projectCache;
+    this.batchUpdateFactory = batchUpdateFactory;
     this.onSubmitValidatorsFactory = onSubmitValidatorsFactory;
 
     openRepos = new HashMap<>();
@@ -199,12 +202,11 @@
     }
   }
 
-  public List<BatchUpdate> batchUpdates(
-      BatchUpdate.Factory batchUpdateFactory, Collection<Project.NameKey> projects)
+  public List<BatchUpdate> batchUpdates(Collection<Project.NameKey> projects)
       throws NoSuchProjectException, IOException {
     List<BatchUpdate> updates = new ArrayList<>(projects.size());
     for (Project.NameKey project : projects) {
-      updates.add(getRepo(project).getUpdate(batchUpdateFactory).setRefLogMessage("merged"));
+      updates.add(getRepo(project).getUpdate().setRefLogMessage("merged"));
     }
     return updates;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
index fe35231..24b3727 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
@@ -14,32 +14,29 @@
 
 package com.google.gerrit.server.git;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
+
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
-import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
-import org.eclipse.jgit.errors.CorruptObjectException;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.notes.Note;
 import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.notes.NoteMapMerger;
 import org.eclipse.jgit.notes.NoteMerger;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
 
 /** A utility class for updating a notes branch with automatic merge of note trees. */
 public class NotesBranchUtil {
@@ -47,9 +44,6 @@
     NotesBranchUtil create(Project.NameKey project, Repository db, ObjectInserter inserter);
   }
 
-  private static final int MAX_LOCK_FAILURE_CALLS = 10;
-  private static final int SLEEP_ON_LOCK_FAILURE_MS = 25;
-
   private final PersonIdent gerritIdent;
   private final GitReferenceUpdated gitRefUpdated;
   private final Project.NameKey project;
@@ -86,16 +80,20 @@
    * Create a new commit in the {@code notesBranch} by updating existing or creating new notes from
    * the {@code notes} map.
    *
+   * <p>Does not retry in the case of lock failure; callers may use {@link
+   * com.google.gerrit.server.update.RetryHelper}.
+   *
    * @param notes map of notes
    * @param notesBranch notes branch to update
    * @param commitAuthor author of the commit in the notes branch
    * @param commitMessage for the commit in the notes branch
-   * @throws IOException
-   * @throws ConcurrentRefUpdateException
+   * @throws LockFailureException if committing the notes failed due to a lock failure on the notes
+   *     branch
+   * @throws IOException if committing the notes failed for any other reason
    */
   public final void commitAllNotes(
       NoteMap notes, String notesBranch, PersonIdent commitAuthor, String commitMessage)
-      throws IOException, ConcurrentRefUpdateException {
+      throws IOException {
     this.overwrite = true;
     commitNotes(notes, notesBranch, commitAuthor, commitMessage);
   }
@@ -105,17 +103,21 @@
    * {@code notes} map. The notes from the {@code notes} map which already exist in the note-tree of
    * the tip of the {@code notesBranch} will not be updated.
    *
+   * <p>Does not retry in the case of lock failure; callers may use {@link
+   * com.google.gerrit.server.update.RetryHelper}.
+   *
    * @param notes map of notes
    * @param notesBranch notes branch to update
    * @param commitAuthor author of the commit in the notes branch
    * @param commitMessage for the commit in the notes branch
    * @return map with those notes from the {@code notes} that were newly created
-   * @throws IOException
-   * @throws ConcurrentRefUpdateException
+   * @throws LockFailureException if committing the notes failed due to a lock failure on the notes
+   *     branch
+   * @throws IOException if committing the notes failed for any other reason
    */
   public final NoteMap commitNewNotes(
       NoteMap notes, String notesBranch, PersonIdent commitAuthor, String commitMessage)
-      throws IOException, ConcurrentRefUpdateException {
+      throws IOException {
     this.overwrite = false;
     commitNotes(notes, notesBranch, commitAuthor, commitMessage);
     NoteMap newlyCreated = NoteMap.newEmptyMap();
@@ -129,7 +131,7 @@
 
   private void commitNotes(
       NoteMap notes, String notesBranch, PersonIdent commitAuthor, String commitMessage)
-      throws IOException, ConcurrentRefUpdateException {
+      throws LockFailureException, IOException {
     try {
       revWalk = new RevWalk(db);
       reader = db.newObjectReader();
@@ -209,61 +211,16 @@
     return revWalk.parseCommit(commitId);
   }
 
-  private void updateRef(String notesBranch)
-      throws IOException, MissingObjectException, IncorrectObjectTypeException,
-          CorruptObjectException, ConcurrentRefUpdateException {
+  private void updateRef(String notesBranch) throws LockFailureException, IOException {
     if (baseCommit != null && oursCommit.getTree().equals(baseCommit.getTree())) {
       // If the trees are identical, there is no change in the notes.
       // Avoid saving this commit as it has no new information.
       return;
     }
-
-    int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
-    RefUpdate refUpdate = createRefUpdate(notesBranch, oursCommit, baseCommit);
-
-    for (; ; ) {
-      Result result = refUpdate.update();
-
-      if (result == Result.LOCK_FAILURE) {
-        if (--remainingLockFailureCalls > 0) {
-          try {
-            Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
-          } catch (InterruptedException e) {
-            // ignore
-          }
-        } else {
-          throw new ConcurrentRefUpdateException(
-              "Failed to lock the ref: " + notesBranch, refUpdate.getRef(), result);
-        }
-
-      } else if (result == Result.REJECTED) {
-        RevCommit theirsCommit = revWalk.parseCommit(refUpdate.getOldObjectId());
-        NoteMap theirs = NoteMap.read(revWalk.getObjectReader(), theirsCommit);
-        NoteMapMerger merger = new NoteMapMerger(db, getNoteMerger(), MergeStrategy.RESOLVE);
-        NoteMap merged = merger.merge(base, ours, theirs);
-        RevCommit mergeCommit =
-            createCommit(merged, gerritIdent, "Merged note commits\n", theirsCommit, oursCommit);
-        refUpdate = createRefUpdate(notesBranch, mergeCommit, theirsCommit);
-        remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
-
-      } else if (result == Result.IO_FAILURE) {
-        throw new IOException("Couldn't update " + notesBranch + ". " + result.name());
-      } else {
-        gitRefUpdated.fire(project, refUpdate, null);
-        break;
-      }
-    }
-  }
-
-  private RefUpdate createRefUpdate(
-      String notesBranch, ObjectId newObjectId, ObjectId expectedOldObjectId) throws IOException {
-    RefUpdate refUpdate = db.updateRef(notesBranch);
-    refUpdate.setNewObjectId(newObjectId);
-    if (expectedOldObjectId == null) {
-      refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
-    } else {
-      refUpdate.setExpectedOldObjectId(expectedOldObjectId);
-    }
-    return refUpdate;
+    BatchRefUpdate bru = db.getRefDatabase().newBatchUpdate();
+    bru.addCommand(
+        new ReceiveCommand(firstNonNull(baseCommit, ObjectId.zeroId()), oursCommit, notesBranch));
+    RefUpdateUtil.executeChecked(bru, revWalk);
+    gitRefUpdated.fire(project, bru, null);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 585c8e2..86daf8e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -346,7 +346,7 @@
   private Map<String, Ref> allRefs;
 
   private final SubmoduleOp.Factory subOpFactory;
-  private final MergeOp.Factory mergeOpFactory;
+  private final Provider<MergeOp> mergeOpProvider;
   private final Provider<MergeOpRepoManager> ormProvider;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final NotesMigration notesMigration;
@@ -400,7 +400,7 @@
       @Assisted ProjectControl projectControl,
       @Assisted Repository repo,
       SubmoduleOp.Factory subOpFactory,
-      MergeOp.Factory mergeOpFactory,
+      Provider<MergeOp> mergeOpProvider,
       Provider<MergeOpRepoManager> ormProvider,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       NotesMigration notesMigration,
@@ -448,7 +448,7 @@
     this.receiveId = RequestId.forProject(project.getNameKey());
 
     this.subOpFactory = subOpFactory;
-    this.mergeOpFactory = mergeOpFactory;
+    this.mergeOpProvider = mergeOpProvider;
     this.ormProvider = ormProvider;
     this.pluginConfigEntries = pluginConfigEntries;
     this.notesMigration = notesMigration;
@@ -657,7 +657,7 @@
       try (MergeOpRepoManager orm = ormProvider.get()) {
         orm.setContext(db, TimeUtil.nowTs(), user, receiveId);
         SubmoduleOp op = subOpFactory.create(branches, orm);
-        op.updateSuperProjects(batchUpdateFactory);
+        op.updateSuperProjects();
       } catch (SubmoduleException e) {
         logError("Can't update the superprojects", e);
       }
@@ -815,7 +815,7 @@
       } catch (ResourceConflictException e) {
         addMessage(e.getMessage());
         reject(magicBranchCmd, "conflict");
-      } catch (RestApiException | OrmException e) {
+      } catch (RestApiException | OrmException | UpdateException e) {
         logError("Error submitting changes to " + project.getName(), e);
         reject(magicBranchCmd, "error during submit");
       }
@@ -2261,7 +2261,7 @@
   }
 
   private void submit(Collection<CreateRequest> create, Collection<ReplaceRequest> replace)
-      throws OrmException, RestApiException {
+      throws OrmException, RestApiException, UpdateException {
     Map<ObjectId, Change> bySha = Maps.newHashMapWithExpectedSize(create.size() + replace.size());
     for (CreateRequest r : create) {
       checkNotNull(r.change, "cannot submit new change %s; op may not have run", r.changeId);
@@ -2275,7 +2275,7 @@
         tipChange, "tip of push does not correspond to a change; found these changes: %s", bySha);
     logDebug(
         "Processing submit with tip change {} ({})", tipChange.getId(), magicBranch.cmd.getNewId());
-    try (MergeOp op = mergeOpFactory.create(batchUpdateFactory)) {
+    try (MergeOp op = mergeOpProvider.get()) {
       op.merge(db, tipChange, user, false, new SubmitInput(), false);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
index 723fb6f..3a82456 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
@@ -29,18 +29,20 @@
   final boolean checkReferencedObjectsAreReachable;
   final boolean allowDrafts;
   private final int systemMaxBatchChanges;
+  private final CapabilityControl.Factory capabilityFactory;
 
   @Inject
-  ReceiveConfig(@GerritServerConfig Config config) {
+  ReceiveConfig(@GerritServerConfig Config config, CapabilityControl.Factory capabilityFactory) {
     checkMagicRefs = config.getBoolean("receive", null, "checkMagicRefs", true);
     checkReferencedObjectsAreReachable =
         config.getBoolean("receive", null, "checkReferencedObjectsAreReachable", true);
     allowDrafts = config.getBoolean("change", null, "allowDrafts", true);
     systemMaxBatchChanges = config.getInt("receive", "maxBatchChanges", 0);
+    this.capabilityFactory = capabilityFactory;
   }
 
   public int getEffectiveMaxBatchChangesLimit(CurrentUser user) {
-    CapabilityControl cap = user.getCapabilities();
+    CapabilityControl cap = capabilityFactory.create(user);
     if (cap.hasExplicitRange(BATCH_CHANGES_LIMIT)) {
       return cap.getRange(BATCH_CHANGES_LIMIT).getMax();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 69909eb..eb9c024 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -355,7 +355,7 @@
     return ret;
   }
 
-  public void updateSuperProjects(BatchUpdate.Factory updateFactory) throws SubmoduleException {
+  public void updateSuperProjects() throws SubmoduleException {
     ImmutableSet<Project.NameKey> projects = getProjectsInOrder();
     if (projects == null) {
       return;
@@ -370,15 +370,12 @@
           // get a new BatchUpdate for the super project
           OpenRepo or = orm.getRepo(project);
           for (Branch.NameKey branch : branchesByProject.get(project)) {
-            addOp(or.getUpdate(updateFactory), branch);
+            addOp(or.getUpdate(), branch);
           }
         }
       }
       batchUpdateFactory.execute(
-          orm.batchUpdates(updateFactory, superProjects),
-          BatchUpdateListener.NONE,
-          orm.getSubmissionId(),
-          false);
+          orm.batchUpdates(superProjects), BatchUpdateListener.NONE, orm.getSubmissionId(), false);
     } catch (RestApiException | UpdateException | IOException | NoSuchProjectException e) {
       throw new SubmoduleException("Cannot update gitlinks", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
index b5659ac..33e7961 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -108,11 +108,25 @@
 
   /** Create a new executor queue. */
   public ScheduledThreadPoolExecutor createQueue(int poolsize, String prefix) {
-    final Executor r = new Executor(poolsize, prefix);
-    r.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
-    r.setExecuteExistingDelayedTasksAfterShutdownPolicy(true);
-    queues.add(r);
-    return r;
+    return createQueue(poolsize, prefix, Thread.NORM_PRIORITY);
+  }
+
+  public ScheduledThreadPoolExecutor createQueue(int poolsize, String prefix, int threadPriority) {
+    Executor executor = new Executor(poolsize, prefix);
+    executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
+    executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(true);
+    queues.add(executor);
+    if (threadPriority != Thread.NORM_PRIORITY) {
+      ThreadFactory parent = executor.getThreadFactory();
+      executor.setThreadFactory(
+          task -> {
+            Thread t = parent.newThread(task);
+            t.setPriority(threadPriority);
+            return t;
+          });
+    }
+
+    return executor;
   }
 
   /** Get all of the tasks currently scheduled in any work queue. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index 95bdf33..9892b6e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -18,12 +18,13 @@
 
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -32,6 +33,7 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.change.RebaseChangeOp;
+import com.google.gerrit.server.change.Submit.TestSubmitInput;
 import com.google.gerrit.server.extensions.events.ChangeMerged;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -96,7 +98,7 @@
           Set<RevCommit> alreadyAccepted,
           Set<CodeReviewCommit> incoming,
           RequestId submissionId,
-          NotifyHandling notifyHandling,
+          SubmitInput submitInput,
           ListMultimap<RecipientType, Account.Id> accountsToNotify,
           SubmoduleOp submoduleOp,
           boolean dryrun);
@@ -129,7 +131,7 @@
     final Set<RevCommit> alreadyAccepted;
     final RequestId submissionId;
     final SubmitType submitType;
-    final NotifyHandling notifyHandling;
+    final SubmitInput submitInput;
     final ListMultimap<RecipientType, Account.Id> accountsToNotify;
     final SubmoduleOp submoduleOp;
 
@@ -169,7 +171,7 @@
         @Assisted Set<CodeReviewCommit> incoming,
         @Assisted RequestId submissionId,
         @Assisted SubmitType submitType,
-        @Assisted NotifyHandling notifyHandling,
+        @Assisted SubmitInput submitInput,
         @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify,
         @Assisted SubmoduleOp submoduleOp,
         @Assisted boolean dryrun) {
@@ -199,7 +201,7 @@
       this.alreadyAccepted = alreadyAccepted;
       this.submissionId = submissionId;
       this.submitType = submitType;
-      this.notifyHandling = notifyHandling;
+      this.submitInput = submitInput;
       this.accountsToNotify = accountsToNotify;
       this.submoduleOp = submoduleOp;
       this.dryrun = dryrun;
@@ -255,12 +257,21 @@
     List<CodeReviewCommit> difference = new ArrayList<>(Sets.difference(toMerge, added));
     Collections.reverse(difference);
     for (CodeReviewCommit c : difference) {
-      bu.addOp(c.change().getId(), new ImplicitIntegrateOp(args, c));
+      Change.Id id = c.change().getId();
+      bu.addOp(id, new ImplicitIntegrateOp(args, c));
+      maybeAddTestHelperOp(bu, id);
     }
 
     // Then ops for explicitly merged changes
     for (SubmitStrategyOp op : ops) {
       bu.addOp(op.getId(), op);
+      maybeAddTestHelperOp(bu, op.getId());
+    }
+  }
+
+  private void maybeAddTestHelperOp(BatchUpdate bu, Change.Id changeId) {
+    if (args.submitInput instanceof TestSubmitInput) {
+      bu.addOp(changeId, new TestHelperOp(changeId, args));
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
index 8f43a49..7678623 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.git.strategy;
 
 import com.google.common.collect.ListMultimap;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -61,7 +61,7 @@
       MergeTip mergeTip,
       CommitStatus commitStatus,
       RequestId submissionId,
-      NotifyHandling notifyHandling,
+      SubmitInput submitInput,
       ListMultimap<RecipientType, Account.Id> accountsToNotify,
       SubmoduleOp submoduleOp,
       boolean dryrun)
@@ -79,7 +79,7 @@
             alreadyAccepted,
             incoming,
             submissionId,
-            notifyHandling,
+            submitInput,
             accountsToNotify,
             submoduleOp,
             dryrun);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
index 6bf8b2e..aaad040 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -77,7 +77,8 @@
   private ObjectId mergeResultRev;
   private PatchSet mergedPatchSet;
   private Change updatedChange;
-  private CodeReviewCommit alreadyMerged;
+  private CodeReviewCommit alreadyMergedCommit;
+  private boolean changeAlreadyMerged;
 
   protected SubmitStrategyOp(SubmitStrategy.Arguments args, CodeReviewCommit toMerge) {
     this.args = args;
@@ -112,11 +113,11 @@
     // Run the submit strategy implementation and record the merge tip state so
     // we can create the ref update.
     CodeReviewCommit tipBefore = args.mergeTip.getCurrentTip();
-    alreadyMerged = getAlreadyMergedCommit(ctx);
-    if (alreadyMerged == null) {
+    alreadyMergedCommit = getAlreadyMergedCommit(ctx);
+    if (alreadyMergedCommit == null) {
       updateRepoImpl(ctx);
     } else {
-      logDebug("Already merged as {}", alreadyMerged.name());
+      logDebug("Already merged as {}", alreadyMergedCommit.name());
     }
     CodeReviewCommit tipAfter = args.mergeTip.getCurrentTip();
 
@@ -219,8 +220,23 @@
     PatchSet.Id oldPsId = checkNotNull(toMerge.getPatchsetId());
     PatchSet.Id newPsId;
 
-    if (alreadyMerged != null) {
-      alreadyMerged.setControl(ctx.getControl());
+    if (ctx.getChange().getStatus() == Change.Status.MERGED) {
+      // Either another thread won a race, or we are retrying a whole topic submission after one
+      // repo failed with lock failure.
+      if (alreadyMergedCommit == null) {
+        logDebug(
+            "Change is already merged according to its status, but we were unable to find it"
+                + " merged into the current tip ({})",
+            args.mergeTip.getCurrentTip().name());
+      } else {
+        logDebug("Change is already merged");
+      }
+      changeAlreadyMerged = true;
+      return false;
+    }
+
+    if (alreadyMergedCommit != null) {
+      alreadyMergedCommit.setControl(ctx.getControl());
       mergedPatchSet = getOrCreateAlreadyMergedPatchSet(ctx);
       newPsId = mergedPatchSet.getId();
     } else {
@@ -263,12 +279,12 @@
     setApproval(ctx, args.caller);
 
     mergeResultRev =
-        alreadyMerged == null
+        alreadyMergedCommit == null
             ? args.mergeTip.getMergeResults().get(commit)
             // Our fixup code is not smart enough to find a merge commit
             // corresponding to the merge result. This results in a different
             // ChangeMergedEvent in the fixup case, but we'll just live with that.
-            : alreadyMerged;
+            : alreadyMergedCommit;
     try {
       setMerged(ctx, message(ctx, commit, s));
     } catch (OrmException err) {
@@ -284,13 +300,13 @@
 
   private PatchSet getOrCreateAlreadyMergedPatchSet(ChangeContext ctx)
       throws IOException, OrmException {
-    PatchSet.Id psId = alreadyMerged.getPatchsetId();
+    PatchSet.Id psId = alreadyMergedCommit.getPatchsetId();
     logDebug("Fixing up already-merged patch set {}", psId);
     PatchSet prevPs = args.psUtil.current(ctx.getDb(), ctx.getNotes());
-    ctx.getRevWalk().parseBody(alreadyMerged);
+    ctx.getRevWalk().parseBody(alreadyMergedCommit);
     ctx.getChange()
         .setCurrentPatchSet(
-            psId, alreadyMerged.getShortMessage(), ctx.getChange().getOriginalSubject());
+            psId, alreadyMergedCommit.getShortMessage(), ctx.getChange().getOriginalSubject());
     PatchSet existing = args.psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
     if (existing != null) {
       logDebug("Patch set row exists, only updating change");
@@ -300,13 +316,13 @@
     // a patch set ref. Fix up the database. Note that this uses the current
     // user as the uploader, which is as good a guess as any.
     List<String> groups =
-        prevPs != null ? prevPs.getGroups() : GroupCollector.getDefaultGroups(alreadyMerged);
+        prevPs != null ? prevPs.getGroups() : GroupCollector.getDefaultGroups(alreadyMergedCommit);
     return args.psUtil.insert(
         ctx.getDb(),
         ctx.getRevWalk(),
         ctx.getUpdate(psId),
         psId,
-        alreadyMerged,
+        alreadyMergedCommit,
         false,
         groups,
         null,
@@ -482,6 +498,18 @@
 
   @Override
   public final void postUpdate(Context ctx) throws Exception {
+    if (changeAlreadyMerged) {
+      // TODO(dborowitz): This is suboptimal behavior in the presence of retries: postUpdate steps
+      // will never get run for changes that submitted successfully on any but the final attempt.
+      // This is primarily a temporary workaround for the fact that the submitter field is not
+      // populated in the changeAlreadyMerged case.
+      //
+      // If we naively execute postUpdate even if the change is already merged when updateChange
+      // being, then we are subject to a race where postUpdate steps are run twice if two submit
+      // processes run at the same time.
+      logDebug("Skipping post-update steps for change {}", getId());
+      return;
+    }
     postUpdateImpl(ctx);
 
     if (command != null) {
@@ -508,7 +536,7 @@
               ctx.getProject(),
               getId(),
               submitter.getAccountId(),
-              args.notifyHandling,
+              args.submitInput.notify,
               args.accountsToNotify)
           .sendAsync();
     } catch (Exception e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/TestHelperOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/TestHelperOp.java
new file mode 100644
index 0000000..8d95045
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/TestHelperOp.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.strategy;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.change.Submit.TestSubmitInput;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.RepoContext;
+import com.google.gerrit.server.util.RequestId;
+import java.io.IOException;
+import java.util.Queue;
+import org.eclipse.jgit.lib.ObjectId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class TestHelperOp implements BatchUpdateOp {
+  private static final Logger log = LoggerFactory.getLogger(TestHelperOp.class);
+
+  private final Change.Id changeId;
+  private final TestSubmitInput input;
+  private final RequestId submissionId;
+
+  TestHelperOp(Change.Id changeId, SubmitStrategy.Arguments args) {
+    this.changeId = changeId;
+    this.input = (TestSubmitInput) args.submitInput;
+    this.submissionId = args.submissionId;
+  }
+
+  @Override
+  public void updateRepo(RepoContext ctx) throws IOException {
+    Queue<Boolean> q = input.generateLockFailures;
+    if (q != null && !q.isEmpty() && q.remove()) {
+      logDebug("Adding bogus ref update to trigger lock failure, via change {}", changeId);
+      ctx.addRefUpdate(
+          ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
+          ObjectId.zeroId(),
+          "refs/test/" + getClass().getSimpleName());
+    }
+  }
+
+  private void logDebug(String msg, Object... args) {
+    if (log.isDebugEnabled()) {
+      log.debug(submissionId + msg, args);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 4edfab2..431ac7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -19,7 +19,6 @@
 
 import com.google.common.base.Function;
 import com.google.common.util.concurrent.Atomics;
-import com.google.common.util.concurrent.CheckedFuture;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -73,7 +72,8 @@
     ChangeIndexer create(ListeningExecutorService executor, ChangeIndexCollection indexes);
   }
 
-  public static CheckedFuture<?, IOException> allAsList(
+  @SuppressWarnings("deprecation")
+  public static com.google.common.util.concurrent.CheckedFuture<?, IOException> allAsList(
       List<? extends ListenableFuture<?>> futures) {
     // allAsList propagates the first seen exception, wrapped in
     // ExecutionException, so we can reuse the same mapper as for a single
@@ -173,7 +173,9 @@
    * @param id change to index.
    * @return future for the indexing task.
    */
-  public CheckedFuture<?, IOException> indexAsync(Project.NameKey project, Change.Id id) {
+  @SuppressWarnings("deprecation")
+  public com.google.common.util.concurrent.CheckedFuture<?, IOException> indexAsync(
+      Project.NameKey project, Change.Id id) {
     return submit(new IndexTask(project, id));
   }
 
@@ -183,7 +185,8 @@
    * @param ids changes to index.
    * @return future for completing indexing of all changes.
    */
-  public CheckedFuture<?, IOException> indexAsync(
+  @SuppressWarnings("deprecation")
+  public com.google.common.util.concurrent.CheckedFuture<?, IOException> indexAsync(
       Project.NameKey project, Collection<Change.Id> ids) {
     List<ListenableFuture<?>> futures = new ArrayList<>(ids.size());
     for (Change.Id id : ids) {
@@ -277,7 +280,8 @@
    * @param id change to delete.
    * @return future for the deleting task.
    */
-  public CheckedFuture<?, IOException> deleteAsync(Change.Id id) {
+  @SuppressWarnings("deprecation")
+  public com.google.common.util.concurrent.CheckedFuture<?, IOException> deleteAsync(Change.Id id) {
     return submit(new DeleteTask(id));
   }
 
@@ -300,7 +304,9 @@
    * @param id ID of the change to index.
    * @return future for reindexing the change; returns true if the change was stale.
    */
-  public CheckedFuture<Boolean, IOException> reindexIfStale(Project.NameKey project, Change.Id id) {
+  @SuppressWarnings("deprecation")
+  public com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStale(
+      Project.NameKey project, Change.Id id) {
     return submit(new ReindexIfStaleTask(project, id), batchExecutor);
   }
 
@@ -324,11 +330,14 @@
     return indexes != null ? indexes.getWriteIndexes() : Collections.singleton(index);
   }
 
-  private <T> CheckedFuture<T, IOException> submit(Callable<T> task) {
+  @SuppressWarnings("deprecation")
+  private <T> com.google.common.util.concurrent.CheckedFuture<T, IOException> submit(
+      Callable<T> task) {
     return submit(task, executor);
   }
 
-  private static <T> CheckedFuture<T, IOException> submit(
+  @SuppressWarnings("deprecation")
+  private static <T> com.google.common.util.concurrent.CheckedFuture<T, IOException> submit(
       Callable<T> task, ListeningExecutorService executor) {
     return Futures.makeChecked(Futures.nonCancellationPropagating(executor.submit(task)), MAPPER);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
index a9f5306..2ab5c55 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
@@ -90,11 +90,7 @@
       Account.Id accountId = Account.Id.fromRef(event.getRefName());
       if (accountId != null) {
         try {
-          if (event.isDelete()) {
-            // TODO(ekempin): Delete account from cache and index.
-          } else {
-            accountCache.evict(accountId);
-          }
+          accountCache.evict(accountId);
         } catch (IOException e) {
           log.error(String.format("Reindex account %s failed.", accountId), e);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 14512c3..15a4b13 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
@@ -37,6 +38,8 @@
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
@@ -94,8 +97,12 @@
     super.setFrom(id);
 
     /** Is the from user in an email squelching group? */
-    final IdentifiedUser user = args.identifiedUserFactory.create(id);
-    emailOnlyAuthors = !user.getCapabilities().canEmailReviewers();
+    try {
+      IdentifiedUser user = args.identifiedUserFactory.create(id);
+      args.permissionBackend.user(user).check(GlobalPermission.EMAIL_REVIEWERS);
+    } catch (AuthException | PermissionBackendException e) {
+      emailOnlyAuthors = true;
+    }
   }
 
   public void setPatchSet(PatchSet ps) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailArguments.java
index 683416f..3bf5db1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailArguments.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -36,6 +35,7 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -52,6 +52,7 @@
 public class EmailArguments {
   final GitRepositoryManager server;
   final ProjectCache projectCache;
+  final PermissionBackend permissionBackend;
   final GroupBackend groupBackend;
   final GroupIncludeCache groupIncludes;
   final AccountCache accountCache;
@@ -61,7 +62,6 @@
   final EmailSender emailSender;
   final PatchSetInfoFactory patchSetInfoFactory;
   final IdentifiedUser.GenericFactory identifiedUserFactory;
-  final CapabilityControl.Factory capabilityControlFactory;
   final ChangeNotes.Factory changeNotesFactory;
   final AnonymousUser anonymousUser;
   final String anonymousCowardName;
@@ -86,6 +86,7 @@
   EmailArguments(
       GitRepositoryManager server,
       ProjectCache projectCache,
+      PermissionBackend permissionBackend,
       GroupBackend groupBackend,
       GroupIncludeCache groupIncludes,
       AccountCache accountCache,
@@ -95,7 +96,6 @@
       EmailSender emailSender,
       PatchSetInfoFactory patchSetInfoFactory,
       GenericFactory identifiedUserFactory,
-      CapabilityControl.Factory capabilityControlFactory,
       ChangeNotes.Factory changeNotesFactory,
       AnonymousUser anonymousUser,
       @AnonymousCowardName String anonymousCowardName,
@@ -116,6 +116,7 @@
       OutgoingEmailValidator validator) {
     this.server = server;
     this.projectCache = projectCache;
+    this.permissionBackend = permissionBackend;
     this.groupBackend = groupBackend;
     this.groupIncludes = groupIncludes;
     this.accountCache = accountCache;
@@ -125,7 +126,6 @@
     this.emailSender = emailSender;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.identifiedUserFactory = identifiedUserFactory;
-    this.capabilityControlFactory = capabilityControlFactory;
     this.changeNotesFactory = changeNotesFactory;
     this.anonymousUser = anonymousUser;
     this.anonymousCowardName = anonymousCowardName;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index b459d25..91c4834 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -141,7 +141,7 @@
 
   private void add(Watchers matching, NotifyConfig nc) throws OrmException, QueryParseException {
     for (GroupReference ref : nc.getGroups()) {
-      CurrentUser user = new SingleGroupUser(args.capabilityControlFactory, ref.getUUID());
+      CurrentUser user = new SingleGroupUser(ref.getUUID());
       if (filterMatch(user, nc.getFilter())) {
         deliverToMembers(matching.list(nc.getHeader()), ref.getUUID());
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
index bd05143..06826ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 
+import com.google.auto.value.AutoValue;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.inject.AbstractModule;
@@ -30,7 +31,7 @@
  * <p>This class controls the state of the migration according to options in {@code gerrit.config}.
  * In general, any changes to these options should only be made by adventurous administrators, who
  * know what they're doing, on non-production data, for the purposes of testing the NoteDb
- * implementation. Changing options quite likely requires re-running {@code RebuildNoteDb}. For
+ * implementation. Changing options quite likely requires re-running {@code MigrateToNoteDb}. For
  * these reasons, the options remain undocumented.
  */
 @Singleton
@@ -73,62 +74,111 @@
     cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), FUSE_UPDATES, migration.fuseUpdates());
   }
 
-  private final boolean writeChanges;
-  private final boolean readChanges;
-  private final boolean readChangeSequence;
-  private final PrimaryStorage changePrimaryStorage;
-  private final boolean disableChangeReviewDb;
-  private final boolean fuseUpdates;
+  public static String toText(NotesMigration migration) {
+    Config cfg = new Config();
+    setConfigValues(cfg, migration);
+    return cfg.toText();
+  }
+
+  @AutoValue
+  abstract static class Snapshot {
+    static Snapshot create(Config cfg) {
+      boolean writeChanges = cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), WRITE, false);
+      boolean readChanges = cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), READ, false);
+
+      // Reading change sequence numbers from NoteDb is not the default even if
+      // reading changes themselves is. Once this is enabled, it's not easy to
+      // undo: ReviewDb might hand out numbers that have already been assigned by
+      // NoteDb. This decision for the default may be reevaluated later.
+      boolean readChangeSequence = cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), SEQUENCE, false);
+
+      PrimaryStorage changePrimaryStorage =
+          cfg.getEnum(SECTION_NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, PrimaryStorage.REVIEW_DB);
+      boolean disableChangeReviewDb =
+          cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), DISABLE_REVIEW_DB, false);
+      boolean fuseUpdates = cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), FUSE_UPDATES, false);
+
+      checkArgument(
+          !(disableChangeReviewDb && changePrimaryStorage != PrimaryStorage.NOTE_DB),
+          "cannot disable ReviewDb for changes if default change primary storage is ReviewDb");
+
+      return new AutoValue_ConfigNotesMigration_Snapshot(
+          writeChanges,
+          readChanges,
+          readChangeSequence,
+          changePrimaryStorage,
+          disableChangeReviewDb,
+          fuseUpdates);
+    }
+
+    abstract boolean writeChanges();
+
+    abstract boolean readChanges();
+
+    abstract boolean readChangeSequence();
+
+    abstract PrimaryStorage changePrimaryStorage();
+
+    abstract boolean disableChangeReviewDb();
+
+    abstract boolean fuseUpdates();
+  }
+
+  private volatile Snapshot snapshot;
 
   @Inject
   public ConfigNotesMigration(@GerritServerConfig Config cfg) {
-    writeChanges = cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), WRITE, false);
-    readChanges = cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), READ, false);
-
-    // Reading change sequence numbers from NoteDb is not the default even if
-    // reading changes themselves is. Once this is enabled, it's not easy to
-    // undo: ReviewDb might hand out numbers that have already been assigned by
-    // NoteDb. This decision for the default may be reevaluated later.
-    readChangeSequence = cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), SEQUENCE, false);
-
-    changePrimaryStorage =
-        cfg.getEnum(SECTION_NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, PrimaryStorage.REVIEW_DB);
-    disableChangeReviewDb =
-        cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), DISABLE_REVIEW_DB, false);
-    fuseUpdates = cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), FUSE_UPDATES, false);
-
-    checkArgument(
-        !(disableChangeReviewDb && changePrimaryStorage != PrimaryStorage.NOTE_DB),
-        "cannot disable ReviewDb for changes if default change primary storage is ReviewDb");
+    this.snapshot = Snapshot.create(cfg);
   }
 
   @Override
   public boolean rawWriteChangesSetting() {
-    return writeChanges;
+    return snapshot.writeChanges();
   }
 
   @Override
   public boolean readChanges() {
-    return readChanges;
+    return snapshot.readChanges();
   }
 
   @Override
   public boolean readChangeSequence() {
-    return readChangeSequence;
+    return snapshot.readChangeSequence();
   }
 
   @Override
   public PrimaryStorage changePrimaryStorage() {
-    return changePrimaryStorage;
+    return snapshot.changePrimaryStorage();
   }
 
   @Override
   public boolean disableChangeReviewDb() {
-    return disableChangeReviewDb;
+    return snapshot.disableChangeReviewDb();
   }
 
   @Override
   public boolean fuseUpdates() {
-    return fuseUpdates;
+    return snapshot.fuseUpdates();
+  }
+
+  /**
+   * Set the in-memory values returned by this instance to match another instance.
+   *
+   * <p>This method is only intended for use by {@link
+   * com.google.gerrit.server.notedb.rebuild.NoteDbMigrator}.
+   *
+   * <p>This <em>only</em> modifies the in-memory state; if this instance was initialized from a
+   * file-based config, the underlying storage is not updated. Callers are responsible for managing
+   * the underlying storage on their own. This method is synchronized to aid in such
+   * implementations.
+   *
+   * @see NotesMigration#setFrom(NotesMigration)
+   */
+  @Override
+  public synchronized ConfigNotesMigration setFrom(NotesMigration other) {
+    Config cfg = new Config();
+    setConfigValues(cfg, other);
+    snapshot = Snapshot.create(cfg);
+    return this;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
index 6ab8e20..c17fafd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
-import com.google.gerrit.server.notedb.rebuild.SiteRebuilder;
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Names;
 import org.eclipse.jgit.lib.Config;
@@ -55,7 +54,6 @@
     factory(NoteDbUpdateManager.Factory.class);
     factory(RobotCommentNotes.Factory.class);
     factory(RobotCommentUpdate.Factory.class);
-    factory(SiteRebuilder.Factory.class);
 
     if (!useTestBindings) {
       install(ChangeNotesCache.module());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 6b3492a..45cf244 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -22,7 +22,6 @@
 import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ListMultimap;
@@ -39,9 +38,9 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.InMemoryInserter;
 import com.google.gerrit.server.git.InsertedObject;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.update.ChainedReceiveCommands;
+import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gwtorm.server.OrmConcurrencyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -55,9 +54,7 @@
 import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -548,54 +545,11 @@
     bru.setAllowNonFastForwards(true);
 
     if (!dryrun) {
-      bru.execute(or.rw, NullProgressMonitor.INSTANCE);
-      checkResults(bru);
+      RefUpdateUtil.executeChecked(bru, or.rw);
     }
     return bru;
   }
 
-  /**
-   * Check results of all commands in the update batch, reducing to a single exception if there was
-   * a failure.
-   *
-   * <p>Throws {@link LockFailureException} if at least one command failed with {@code
-   * LOCK_FAILURE}, and the entire transaction was aborted, i.e. any non-{@code LOCK_FAILURE}
-   * results, if there were any, failed with "transaction aborted".
-   *
-   * <p>In particular, if the underlying ref database does not {@link
-   * org.eclipse.jgit.lib.RefDatabase#performsAtomicTransactions() perform atomic transactions},
-   * then a combination of {@code LOCK_FAILURE} on one ref and {@code OK} or another result on other
-   * refs will <em>not</em> throw {@code LockFailureException}.
-   *
-   * @param bru batch update; should already have been executed.
-   * @throws LockFailureException if the transaction was aborted due to lock failure.
-   * @throws IOException if any result was not {@code OK}.
-   */
-  @VisibleForTesting
-  static void checkResults(BatchRefUpdate bru) throws LockFailureException, IOException {
-    int lockFailure = 0;
-    int aborted = 0;
-    int failure = 0;
-
-    for (ReceiveCommand cmd : bru.getCommands()) {
-      if (cmd.getResult() != ReceiveCommand.Result.OK) {
-        failure++;
-      }
-      if (cmd.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
-        lockFailure++;
-      } else if (cmd.getResult() == ReceiveCommand.Result.REJECTED_OTHER_REASON
-          && JGitText.get().transactionAborted.equals(cmd.getMessage())) {
-        aborted++;
-      }
-    }
-
-    if (lockFailure + aborted == bru.getCommands().size()) {
-      throw new LockFailureException("Update aborted with one or more lock failures: " + bru);
-    } else if (failure > 0) {
-      throw new IOException("Update failed: " + bru);
-    }
-  }
-
   private void addCommands() throws OrmException, IOException {
     if (isEmpty()) {
       return;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
index b072cdb..61bcf17 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.notedb;
 
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+import java.util.Objects;
 
 /**
  * Current low-level settings of the NoteDb migration for changes.
@@ -39,7 +40,7 @@
  * <p>This class controls the state of the migration according to options in {@code gerrit.config}.
  * In general, any changes to these options should only be made by adventurous administrators, who
  * know what they're doing, on non-production data, for the purposes of testing the NoteDb
- * implementation. Changing options quite likely requires re-running {@code RebuildNoteDb}. For
+ * implementation. Changing options quite likely requires re-running {@code MigrateToNoteDb}. For
  * these reasons, the options remain undocumented.
  *
  * <p><strong>Note:</strong> Callers should not assume the values returned by {@code
@@ -108,6 +109,18 @@
   public abstract boolean fuseUpdates();
 
   /**
+   * Set the values returned by this instance to match another instance.
+   *
+   * <p>Optional operation: not all implementations support setting values after initialization.
+   *
+   * @param other other instance to copy values from.
+   * @return this.
+   */
+  public NotesMigration setFrom(NotesMigration other) {
+    throw new UnsupportedOperationException(getClass().getSimpleName() + " is read-only");
+  }
+
+  /**
    * Whether to fail when reading any data from NoteDb.
    *
    * <p>Used in conjunction with {@link #readChanges()} for tests.
@@ -116,7 +129,7 @@
     return false;
   }
 
-  public boolean commitChangeWrites() {
+  public final boolean commitChangeWrites() {
     // It may seem odd that readChanges() without writeChanges() means we should
     // attempt to commit writes. However, this method is used by callers to know
     // whether or not they should short-circuit and skip attempting to read or
@@ -130,11 +143,38 @@
     return rawWriteChangesSetting() || readChanges();
   }
 
-  public boolean failChangeWrites() {
+  public final boolean failChangeWrites() {
     return !rawWriteChangesSetting() && readChanges();
   }
 
-  public boolean enabled() {
+  public final boolean enabled() {
     return rawWriteChangesSetting() || readChanges();
   }
+
+  @Override
+  public final boolean equals(Object o) {
+    if (!(o instanceof NotesMigration)) {
+      return false;
+    }
+    NotesMigration m = (NotesMigration) o;
+    return readChanges() == m.readChanges()
+        && rawWriteChangesSetting() == m.rawWriteChangesSetting()
+        && readChangeSequence() == m.readChangeSequence()
+        && changePrimaryStorage() == m.changePrimaryStorage()
+        && disableChangeReviewDb() == m.disableChangeReviewDb()
+        && fuseUpdates() == m.fuseUpdates()
+        && failOnLoad() == m.failOnLoad();
+  }
+
+  @Override
+  public final int hashCode() {
+    return Objects.hash(
+        readChanges(),
+        rawWriteChangesSetting(),
+        readChangeSequence(),
+        changePrimaryStorage(),
+        disableChangeReviewDb(),
+        fuseUpdates(),
+        failOnLoad());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigrationState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigrationState.java
index 49a30bb..8c589d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigrationState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigrationState.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.notedb;
 
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+import java.util.Optional;
+import java.util.stream.Stream;
 
 /**
  * Possible high-level states of the NoteDb migration for changes.
@@ -45,6 +47,10 @@
 
   NOTE_DB(true, true, true, PrimaryStorage.NOTE_DB, true, true);
 
+  public static Optional<NotesMigrationState> forNotesMigration(NotesMigration migration) {
+    return Stream.of(values()).filter(s -> s.migration().equals(migration)).findFirst();
+  }
+
   private final NotesMigration migration;
 
   NotesMigrationState(
@@ -92,4 +98,8 @@
   public NotesMigration migration() {
     return migration;
   }
+
+  public String toText() {
+    return ConfigNotesMigration.toText(migration);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
index c970cb0..e88554a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -179,8 +179,8 @@
   private Result rebuild(ReviewDb db, Change.Id changeId, boolean checkReadOnly)
       throws IOException, OrmException {
     db = ReviewDbUtil.unwrapDb(db);
-    // Read change just to get project; this instance is then discarded so we
-    // can read a consistent ChangeBundle inside a transaction.
+    // Read change just to get project; this instance is then discarded so we can read a consistent
+    // ChangeBundle inside a transaction.
     Change change = db.changes().get(changeId);
     if (change == null) {
       throw new NoSuchChangeException(changeId);
@@ -254,29 +254,27 @@
                 }
               });
     } catch (ConflictingUpdateException e) {
-      // Rethrow as an OrmException so the caller knows to use staged results.
-      // Strictly speaking they are not completely up to date, but result we
-      // send to the caller is the same as if this rebuild had executed before
-      // the other thread.
+      // Rethrow as an OrmException so the caller knows to use staged results. Strictly speaking
+      // they are not completely up to date, but result we send to the caller is the same as if this
+      // rebuild had executed before the other thread.
       throw new OrmException(e.getMessage());
     } catch (AbortUpdateException e) {
       if (NoteDbChangeState.parse(changeId, newNoteDbState)
           .isUpToDate(
               manager.getChangeRepo().cmds.getRepoRefCache(),
               manager.getAllUsersRepo().cmds.getRepoRefCache())) {
-        // If the state in ReviewDb matches NoteDb at this point, it means
-        // another thread successfully completed this rebuild. It's ok to not
-        // execute the update in this case, since the object referenced in the
-        // Result was flushed to the repo by whatever thread won the race.
+        // If the state in ReviewDb matches NoteDb at this point, it means another thread
+        // successfully completed this rebuild. It's ok to not execute the update in this case,
+        // since the object referenced in the Result was flushed to the repo by whatever thread won
+        // the race.
         return r;
       }
-      // If the state doesn't match, that means another thread attempted this
-      // rebuild, but failed. Fall through and try to update the ref again.
+      // If the state doesn't match, that means another thread attempted this rebuild, but
+      // failed. Fall through and try to update the ref again.
     }
     if (migration.failChangeWrites()) {
-      // Don't even attempt to execute if read-only, it would fail anyway. But
-      // do throw an exception to the caller so they know to use the staged
-      // results instead of reading from the repo.
+      // Don't even attempt to execute if read-only, it would fail anyway. But do throw an exception
+      // to the caller so they know to use the staged results instead of reading from the repo.
       throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY);
     }
     manager.execute();
@@ -302,15 +300,15 @@
       throw new NoPatchSetsException(change.getId());
     }
 
-    // We will rebuild all events, except for draft comments, in buckets based
-    // on author and timestamp.
+    // We will rebuild all events, except for draft comments, in buckets based on author and
+    // timestamp.
     List<Event> events = new ArrayList<>();
     ListMultimap<Account.Id, DraftCommentEvent> draftCommentEvents =
         MultimapBuilder.hashKeys().arrayListValues().build();
 
     events.addAll(getHashtagsEvents(change, manager));
 
-    // Delete ref only after hashtags have been read
+    // Delete ref only after hashtags have been read.
     deleteChangeMetaRef(change, manager.getChangeRepo().cmds);
     deleteDraftRefs(change, manager.getAllUsersRepo());
 
@@ -429,8 +427,8 @@
     setPostSubmitDeps(events);
     new EventSorter(events).sort();
 
-    // Ensure the first event in the list creates the change, setting the author
-    // and any required footers.
+    // Ensure the first event in the list creates the change, setting the author and any required
+    // footers.
     Event first = events.get(0);
     if (first instanceof PatchSetEvent && change.getOwner().equals(first.user)) {
       ((PatchSetEvent) first).createChange = true;
@@ -440,22 +438,19 @@
 
     // Final pass to correct some inconsistencies.
     //
-    // First, fill in any missing patch set IDs using the latest patch set of
-    // the change at the time of the event, because NoteDb can't represent
-    // actions with no associated patch set ID. This workaround is as if a user
-    // added a ChangeMessage on the change by replying from the latest patch
-    // set.
+    // First, fill in any missing patch set IDs using the latest patch set of the change at the time
+    // of the event, because NoteDb can't represent actions with no associated patch set ID. This
+    // workaround is as if a user added a ChangeMessage on the change by replying from the latest
+    // patch set.
     //
-    // Start with the first patch set that actually exists. If there are no
-    // patch sets at all, minPsNum will be null, so just bail and use 1 as the
-    // patch set ID. The corresponding patch set won't exist, but this change is
-    // probably corrupt anyway, as deleting the last draft patch set should have
-    // deleted the whole change.
+    // Start with the first patch set that actually exists. If there are no patch sets at all,
+    // minPsNum will be null, so just bail and use 1 as the patch set ID. The corresponding patch
+    // set won't exist, but this change is probably corrupt anyway, as deleting the last draft patch
+    // set should have deleted the whole change.
     //
-    // Second, ensure timestamps are nondecreasing, by copying the previous
-    // timestamp if this happens. This assumes that the only way this can happen
-    // is due to dependency constraints, and it is ok to give an event the same
-    // timestamp as one of its dependencies.
+    // Second, ensure timestamps are nondecreasing, by copying the previous timestamp if this
+    // happens. This assumes that the only way this can happen is due to dependency constraints, and
+    // it is ok to give an event the same timestamp as one of its dependencies.
     int ps = firstNonNull(minPsNum, 1);
     for (int i = 0; i < events.size(); i++) {
       Event e = events.get(i);
@@ -492,8 +487,8 @@
     if (projectCache != null) {
       labelNameComparator = projectCache.get(change.getProject()).getLabelTypes().nameComparator();
     } else {
-      // No project cache available, bail and use natural ordering; there's no
-      // semantic difference anyway difference.
+      // No project cache available, bail and use natural ordering; there's no semantic difference
+      // anyway difference.
       labelNameComparator = Ordering.natural();
     }
     ChangeUpdate update =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/MigrationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/MigrationException.java
new file mode 100644
index 0000000..209fd7a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/MigrationException.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import java.io.IOException;
+
+/** Exception thrown by {@link NoteDbMigrator} when migration fails. */
+class MigrationException extends IOException {
+  private static final long serialVersionUID = 1L;
+
+  MigrationException(String message) {
+    super(message);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
new file mode 100644
index 0000000..97fecb7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
@@ -0,0 +1,497 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+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 static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
+import static com.google.gerrit.server.notedb.NotesMigrationState.NOTE_DB_UNFUSED;
+import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_NO_SEQUENCE;
+import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY;
+import static com.google.gerrit.server.notedb.NotesMigrationState.WRITE;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Comparator.comparing;
+
+import com.google.common.base.Predicates;
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Streams;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.common.FormatUtil;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.notedb.ChangeBundleReader;
+import com.google.gerrit.server.notedb.ConfigNotesMigration;
+import com.google.gerrit.server.notedb.NoteDbUpdateManager;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.notedb.NotesMigrationState;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.io.NullOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** One stop shop for migrating a site's change storage from ReviewDb to NoteDb. */
+public class NoteDbMigrator implements AutoCloseable {
+  private static final Logger log = LoggerFactory.getLogger(NoteDbMigrator.class);
+
+  public static class Builder {
+    private final SitePaths sitePaths;
+    private final SchemaFactory<ReviewDb> schemaFactory;
+    private final NoteDbUpdateManager.Factory updateManagerFactory;
+    private final ChangeRebuilder rebuilder;
+    private final ChangeBundleReader bundleReader;
+    private final WorkQueue workQueue;
+    private final NotesMigration globalNotesMigration;
+
+    private int threads;
+    private ImmutableList<Project.NameKey> projects = ImmutableList.of();
+    private ImmutableList<Change.Id> changes = ImmutableList.of();
+    private OutputStream progressOut = NullOutputStream.INSTANCE;
+    private boolean trial;
+    private boolean forceRebuild;
+
+    @Inject
+    Builder(
+        SitePaths sitePaths,
+        SchemaFactory<ReviewDb> schemaFactory,
+        NoteDbUpdateManager.Factory updateManagerFactory,
+        ChangeRebuilder rebuilder,
+        ChangeBundleReader bundleReader,
+        WorkQueue workQueue,
+        NotesMigration globalNotesMigration) {
+      this.sitePaths = sitePaths;
+      this.schemaFactory = schemaFactory;
+      this.updateManagerFactory = updateManagerFactory;
+      this.rebuilder = rebuilder;
+      this.bundleReader = bundleReader;
+      this.workQueue = workQueue;
+      this.globalNotesMigration = globalNotesMigration;
+    }
+
+    /**
+     * Set the number of threads used by parallelizable phases of the migration, such as rebuilding
+     * all changes.
+     *
+     * <p>Not all phases are parallelizable, and calling {@link #rebuild()} directly will do
+     * substantial work in the calling thread regardless of the number of threads configured.
+     *
+     * <p>By default, all work is done in the calling thread.
+     *
+     * @param threads thread count; if less than 2, all work happens in the calling thread.
+     * @return this.
+     */
+    public Builder setThreads(int threads) {
+      this.threads = threads;
+      return this;
+    }
+
+    /**
+     * Limit the set of projects that are processed.
+     *
+     * <p>Incompatible with {@link #setChanges(Collection)}.
+     *
+     * <p>By default, all projects will be processed.
+     *
+     * @param projects set of projects; if null or empty, all projects will be processed.
+     * @return this.
+     */
+    public Builder setProjects(@Nullable Collection<Project.NameKey> projects) {
+      this.projects = projects != null ? ImmutableList.copyOf(projects) : ImmutableList.of();
+      return this;
+    }
+
+    /**
+     * Limit the set of changes that are processed.
+     *
+     * <p>Incompatible with {@link #setProjects(Collection)}.
+     *
+     * <p>By default, all changes will be processed.
+     *
+     * @param changes set of changes; if null or empty, all changes will be processed.
+     * @return this.
+     */
+    public Builder setChanges(@Nullable Collection<Change.Id> changes) {
+      this.changes = changes != null ? ImmutableList.copyOf(changes) : ImmutableList.of();
+      return this;
+    }
+
+    /**
+     * Set output stream for progress monitors.
+     *
+     * <p>By default, there is no progress monitor output (although there may be other logs).
+     *
+     * @param progressOut output stream.
+     * @return this.
+     */
+    public Builder setProgressOut(OutputStream progressOut) {
+      this.progressOut = checkNotNull(progressOut);
+      return this;
+    }
+
+    /**
+     * Rebuild in "trial mode": configure Gerrit to write to and read from NoteDb, but leave
+     * ReviewDb as the source of truth for all changes.
+     *
+     * <p>By default, trial mode is off, and NoteDb is the source of truth for all changes following
+     * the migration.
+     *
+     * @param trial whether to rebuild in trial mode.
+     * @return this.
+     */
+    public Builder setTrialMode(boolean trial) {
+      this.trial = trial;
+      return this;
+    }
+
+    /**
+     * Rebuild all changes in NoteDb from ReviewDb, even if Gerrit is currently configured to read
+     * from NoteDb.
+     *
+     * <p>Only supported if ReviewDb is still the source of truth for all changes.
+     *
+     * <p>By default, force rebuilding is off.
+     *
+     * @param forceRebuild whether to force rebuilding.
+     * @return this.
+     */
+    public Builder setForceRebuild(boolean forceRebuild) {
+      this.forceRebuild = forceRebuild;
+      return this;
+    }
+
+    public NoteDbMigrator build() {
+      return new NoteDbMigrator(
+          sitePaths,
+          schemaFactory,
+          updateManagerFactory,
+          rebuilder,
+          bundleReader,
+          globalNotesMigration,
+          threads > 1
+              ? MoreExecutors.listeningDecorator(workQueue.createQueue(threads, "RebuildChange"))
+              : MoreExecutors.newDirectExecutorService(),
+          projects,
+          changes,
+          progressOut,
+          trial,
+          forceRebuild);
+    }
+  }
+
+  private final FileBasedConfig gerritConfig;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final NoteDbUpdateManager.Factory updateManagerFactory;
+  private final ChangeRebuilder rebuilder;
+  private final ChangeBundleReader bundleReader;
+  private final NotesMigration globalNotesMigration;
+
+  private final ListeningExecutorService executor;
+  private final ImmutableList<Project.NameKey> projects;
+  private final ImmutableList<Change.Id> changes;
+  private final OutputStream progressOut;
+  private final boolean trial;
+  private final boolean forceRebuild;
+
+  private NoteDbMigrator(
+      SitePaths sitePaths,
+      SchemaFactory<ReviewDb> schemaFactory,
+      NoteDbUpdateManager.Factory updateManagerFactory,
+      ChangeRebuilder rebuilder,
+      ChangeBundleReader bundleReader,
+      NotesMigration globalNotesMigration,
+      ListeningExecutorService executor,
+      ImmutableList<Project.NameKey> projects,
+      ImmutableList<Change.Id> changes,
+      OutputStream progressOut,
+      boolean trial,
+      boolean forceRebuild) {
+    this.schemaFactory = schemaFactory;
+    this.updateManagerFactory = updateManagerFactory;
+    this.rebuilder = rebuilder;
+    this.bundleReader = bundleReader;
+    this.globalNotesMigration = globalNotesMigration;
+
+    boolean hasChanges = !changes.isEmpty();
+    boolean hasProjects = !projects.isEmpty();
+    checkArgument(!(hasChanges && hasProjects), "cannot set both changes and projects");
+
+    this.gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
+
+    this.executor = executor;
+    this.projects = projects;
+    this.changes = changes;
+    this.progressOut = progressOut;
+    this.trial = trial;
+    this.forceRebuild = forceRebuild;
+  }
+
+  @Override
+  public void close() {
+    executor.shutdownNow();
+  }
+
+  public void migrate() throws OrmException, IOException {
+    checkState(
+        changes.isEmpty() && projects.isEmpty(),
+        "cannot set changes or projects during auto-migration; call rebuild() instead");
+    Optional<NotesMigrationState> maybeState = loadState();
+    if (!maybeState.isPresent()) {
+      throw new MigrationException("Could not determine initial migration state");
+    }
+
+    NotesMigrationState state = maybeState.get();
+    if (trial && state.compareTo(READ_WRITE_NO_SEQUENCE) > 0) {
+      throw new MigrationException(
+          "Migration has already progressed past the endpoint of the \"trial mode\" state;"
+              + " NoteDb is already the primary storage for some changes");
+    }
+    if (forceRebuild && state.compareTo(READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY) > 0) {
+      throw new MigrationException(
+          "Cannot force rebuild changes; NoteDb is already the primary storage for some changes");
+    }
+
+    boolean rebuilt = false;
+    while (state.compareTo(NOTE_DB_UNFUSED) < 0) {
+      if (trial && state.compareTo(READ_WRITE_NO_SEQUENCE) >= 0) {
+        return;
+      }
+      switch (state) {
+        case REVIEW_DB:
+          state = turnOnWrites(state);
+          break;
+        case WRITE:
+          state = rebuildAndEnableReads(state);
+          rebuilt = true;
+          break;
+        case READ_WRITE_NO_SEQUENCE:
+          if (forceRebuild && !rebuilt) {
+            state = rebuildAndEnableReads(state);
+            rebuilt = true;
+          }
+          state = enableSequences();
+          break;
+        case READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY:
+          if (forceRebuild && !rebuilt) {
+            state = rebuildAndEnableReads(state);
+            rebuilt = true;
+          }
+          state = setNoteDbPrimary();
+          break;
+        case READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY:
+          state = disableReviewDb();
+          break;
+        case NOTE_DB_UNFUSED:
+          // Done!
+          break;
+        case NOTE_DB:
+          // TODO(dborowitz): Allow this state once FileRepository supports fused updates.
+          // Until then, fallthrough and throw.
+        default:
+          throw new MigrationException(
+              "Migration out of the following state is not supported:\n" + state.toText());
+      }
+    }
+  }
+
+  private NotesMigrationState turnOnWrites(NotesMigrationState prev) throws IOException {
+    return saveState(prev, WRITE);
+  }
+
+  private NotesMigrationState rebuildAndEnableReads(NotesMigrationState prev)
+      throws OrmException, IOException {
+    rebuild();
+    return saveState(prev, READ_WRITE_NO_SEQUENCE);
+  }
+
+  private NotesMigrationState enableSequences() {
+    throw new UnsupportedOperationException("not yet implemented");
+  }
+
+  private NotesMigrationState setNoteDbPrimary() {
+    throw new UnsupportedOperationException("not yet implemented");
+  }
+
+  private NotesMigrationState disableReviewDb() {
+    throw new UnsupportedOperationException("not yet implemented");
+  }
+
+  private Optional<NotesMigrationState> loadState() throws IOException {
+    try {
+      gerritConfig.load();
+      return NotesMigrationState.forNotesMigration(new ConfigNotesMigration(gerritConfig));
+    } catch (ConfigInvalidException | IllegalArgumentException e) {
+      log.warn("error reading NoteDb migration options from " + gerritConfig.getFile(), e);
+      return Optional.empty();
+    }
+  }
+
+  private NotesMigrationState saveState(
+      NotesMigrationState expectedOldState, NotesMigrationState newState) throws IOException {
+    synchronized (globalNotesMigration) {
+      // This read-modify-write is racy. We're counting on the fact that no other Gerrit operation
+      // modifies gerrit.config, and hoping that admins don't either.
+      Optional<NotesMigrationState> actualOldState = loadState();
+      if (!actualOldState.equals(Optional.of(expectedOldState))) {
+        throw new MigrationException(
+            "Cannot move to new state:\n"
+                + newState.toText()
+                + "\n\n"
+                + "Expected this state in gerrit.config:\n"
+                + expectedOldState.toText()
+                + "\n\n"
+                + (actualOldState.isPresent()
+                    ? "But found this state:\n" + actualOldState.get().toText()
+                    : "But could not parse the current state"));
+      }
+      ConfigNotesMigration.setConfigValues(gerritConfig, newState.migration());
+      gerritConfig.save();
+
+      // Only set in-memory state once it's been persisted to storage.
+      globalNotesMigration.setFrom(newState.migration());
+
+      return newState;
+    }
+  }
+
+  public void rebuild() throws MigrationException, OrmException {
+    boolean ok;
+    Stopwatch sw = Stopwatch.createStarted();
+    log.info("Rebuilding changes in NoteDb");
+
+    List<ListenableFuture<Boolean>> futures = new ArrayList<>();
+    ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject = getChangesByProject();
+    List<Project.NameKey> projectNames =
+        Ordering.usingToString().sortedCopy(changesByProject.keySet());
+    for (Project.NameKey project : projectNames) {
+      ListenableFuture<Boolean> future =
+          executor.submit(
+              () -> {
+                try (ReviewDb db = unwrapDb(schemaFactory.open())) {
+                  return rebuildProject(db, changesByProject, project);
+                } catch (Exception e) {
+                  log.error("Error rebuilding project " + project, e);
+                  return false;
+                }
+              });
+      futures.add(future);
+    }
+
+    try {
+      ok = Iterables.all(Futures.allAsList(futures).get(), Predicates.equalTo(true));
+    } catch (InterruptedException | ExecutionException e) {
+      log.error("Error rebuilding projects", e);
+      ok = false;
+    }
+
+    double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
+    log.info(
+        String.format(
+            "Rebuilt %d changes in %.01fs (%.01f/s)\n",
+            changesByProject.size(), t, changesByProject.size() / t));
+    if (!ok) {
+      throw new MigrationException("Rebuilding some changes failed, see log");
+    }
+  }
+
+  private ImmutableListMultimap<Project.NameKey, Change.Id> getChangesByProject()
+      throws OrmException {
+    // Memoize all changes so we can close the db connection and allow other threads to use the full
+    // connection pool.
+    try (ReviewDb db = unwrapDb(schemaFactory.open())) {
+      SetMultimap<Project.NameKey, Change.Id> out =
+          MultimapBuilder.treeKeys(comparing(Project.NameKey::get))
+              .treeSetValues(comparing(Change.Id::get))
+              .build();
+      if (!projects.isEmpty()) {
+        return byProject(db.changes().all(), c -> projects.contains(c.getProject()), out);
+      }
+      if (!changes.isEmpty()) {
+        return byProject(db.changes().get(changes), c -> true, out);
+      }
+      return byProject(db.changes().all(), c -> true, out);
+    }
+  }
+
+  private static ImmutableListMultimap<Project.NameKey, Change.Id> byProject(
+      Iterable<Change> changes,
+      Predicate<Change> pred,
+      SetMultimap<Project.NameKey, Change.Id> out) {
+    Streams.stream(changes).filter(pred).forEach(c -> out.put(c.getProject(), c.getId()));
+    return ImmutableListMultimap.copyOf(out);
+  }
+
+  private boolean rebuildProject(
+      ReviewDb db,
+      ImmutableListMultimap<Project.NameKey, Change.Id> allChanges,
+      Project.NameKey project)
+      throws IOException, OrmException {
+    checkArgument(allChanges.containsKey(project));
+    boolean ok = true;
+    ProgressMonitor pm =
+        new TextProgressMonitor(
+            new PrintWriter(new BufferedWriter(new OutputStreamWriter(progressOut, UTF_8))));
+    pm.beginTask(FormatUtil.elide(project.get(), 50), allChanges.get(project).size());
+    try (NoteDbUpdateManager manager = updateManagerFactory.create(project)) {
+      for (Change.Id changeId : allChanges.get(project)) {
+        try {
+          rebuilder.buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
+        } catch (NoPatchSetsException e) {
+          log.warn(e.getMessage());
+        } catch (Throwable t) {
+          log.error("Failed to rebuild change " + changeId, t);
+          ok = false;
+        }
+        pm.update(1);
+      }
+      manager.execute();
+    } finally {
+      pm.endTask();
+    }
+    return ok;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/SiteRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/SiteRebuilder.java
deleted file mode 100644
index 2365392..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/SiteRebuilder.java
+++ /dev/null
@@ -1,205 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb.rebuild;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Comparator.comparing;
-
-import com.google.common.base.Predicates;
-import com.google.common.base.Stopwatch;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Ordering;
-import com.google.common.collect.SetMultimap;
-import com.google.common.collect.Streams;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
-import com.google.gerrit.common.FormatUtil;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.notedb.ChangeBundleReader;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager;
-import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Predicate;
-import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/** Rebuilder for all changes in a site. */
-public class SiteRebuilder implements AutoCloseable {
-  private static final Logger log = LoggerFactory.getLogger(SiteRebuilder.class);
-
-  public interface Factory {
-    SiteRebuilder create(
-        int threads,
-        @Nullable Collection<Project.NameKey> projects,
-        @Nullable Collection<Change.Id> changes);
-  }
-
-  private final SchemaFactory<ReviewDb> schemaFactory;
-  private final NoteDbUpdateManager.Factory updateManagerFactory;
-  private final ChangeRebuilder rebuilder;
-  private final ChangeBundleReader bundleReader;
-
-  private final ListeningExecutorService executor;
-  private final ImmutableList<Project.NameKey> projects;
-  private final ImmutableList<Change.Id> changes;
-
-  @Inject
-  SiteRebuilder(
-      SchemaFactory<ReviewDb> schemaFactory,
-      NoteDbUpdateManager.Factory updateManagerFactory,
-      ChangeRebuilder rebuilder,
-      ChangeBundleReader bundleReader,
-      WorkQueue workQueue,
-      @Assisted int threads,
-      @Assisted @Nullable Collection<Project.NameKey> projects,
-      @Assisted @Nullable Collection<Change.Id> changes) {
-    this.schemaFactory = schemaFactory;
-    this.updateManagerFactory = updateManagerFactory;
-    this.rebuilder = rebuilder;
-    this.bundleReader = bundleReader;
-    this.executor =
-        threads > 0
-            ? MoreExecutors.listeningDecorator(workQueue.createQueue(threads, "RebuildChange"))
-            : MoreExecutors.newDirectExecutorService();
-    this.projects = projects != null ? ImmutableList.copyOf(projects) : ImmutableList.of();
-    this.changes = changes != null ? ImmutableList.copyOf(changes) : ImmutableList.of();
-  }
-
-  @Override
-  public void close() {
-    executor.shutdownNow();
-  }
-
-  public boolean rebuild() throws OrmException {
-    boolean ok;
-    Stopwatch sw = Stopwatch.createStarted();
-
-    List<ListenableFuture<Boolean>> futures = new ArrayList<>();
-    ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject = getChangesByProject();
-    List<Project.NameKey> projectNames =
-        Ordering.usingToString().sortedCopy(changesByProject.keySet());
-    for (Project.NameKey project : projectNames) {
-      ListenableFuture<Boolean> future =
-          executor.submit(
-              () -> {
-                try (ReviewDb db = unwrapDb(schemaFactory.open())) {
-                  return rebuildProject(db, changesByProject, project);
-                } catch (Exception e) {
-                  log.error("Error rebuilding project " + project, e);
-                  return false;
-                }
-              });
-      futures.add(future);
-    }
-
-    try {
-      ok = Iterables.all(Futures.allAsList(futures).get(), Predicates.equalTo(true));
-    } catch (InterruptedException | ExecutionException e) {
-      log.error("Error rebuilding projects", e);
-      ok = false;
-    }
-
-    double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
-    System.out.format(
-        "Rebuild %d changes in %.01fs (%.01f/s)\n",
-        changesByProject.size(), t, changesByProject.size() / t);
-    return ok;
-  }
-
-  private ImmutableListMultimap<Project.NameKey, Change.Id> getChangesByProject()
-      throws OrmException {
-    // Memoize all changes so we can close the db connection and allow other threads to use the full
-    // connection pool.
-    try (ReviewDb db = unwrapDb(schemaFactory.open())) {
-      SetMultimap<Project.NameKey, Change.Id> out =
-          MultimapBuilder.treeKeys(comparing(Project.NameKey::get))
-              .treeSetValues(comparing(Change.Id::get))
-              .build();
-      if (!projects.isEmpty()) {
-        checkState(changes.isEmpty());
-        return byProject(db.changes().all(), c -> projects.contains(c.getProject()), out);
-      }
-      if (!changes.isEmpty()) {
-        checkState(projects.isEmpty());
-        return byProject(db.changes().get(changes), c -> true, out);
-      }
-      return byProject(db.changes().all(), c -> true, out);
-    }
-  }
-
-  private static ImmutableListMultimap<Project.NameKey, Change.Id> byProject(
-      Iterable<Change> changes,
-      Predicate<Change> pred,
-      SetMultimap<Project.NameKey, Change.Id> out) {
-    Streams.stream(changes).filter(pred).forEach(c -> out.put(c.getProject(), c.getId()));
-    return ImmutableListMultimap.copyOf(out);
-  }
-
-  private boolean rebuildProject(
-      ReviewDb db,
-      ImmutableListMultimap<Project.NameKey, Change.Id> allChanges,
-      Project.NameKey project)
-      throws IOException, OrmException {
-    checkArgument(allChanges.containsKey(project));
-    boolean ok = true;
-    ProgressMonitor pm =
-        new TextProgressMonitor(
-            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8))));
-    pm.beginTask(FormatUtil.elide(project.get(), 50), allChanges.get(project).size());
-    try (NoteDbUpdateManager manager = updateManagerFactory.create(project)) {
-      for (Change.Id changeId : allChanges.get(project)) {
-        try {
-          rebuilder.buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
-        } catch (NoPatchSetsException e) {
-          log.warn(e.getMessage());
-        } catch (Throwable t) {
-          log.error("Failed to rebuild change " + changeId, t);
-          ok = false;
-        }
-        pm.update(1);
-      }
-      manager.execute();
-    } finally {
-      pm.endTask();
-    }
-    return ok;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
index 41e8fbc..278b2af 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
@@ -17,21 +17,24 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.git.BanCommitResult;
+import com.google.gerrit.server.project.BanCommit.BanResultInfo;
 import com.google.gerrit.server.project.BanCommit.Input;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
+import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
-import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
 import org.eclipse.jgit.lib.ObjectId;
 
 @Singleton
-public class BanCommit implements RestModifyView<ProjectResource, Input> {
+public class BanCommit extends RetryingRestModifyView<ProjectResource, Input, BanResultInfo> {
   public static class Input {
     public List<String> commits;
     public String reason;
@@ -50,13 +53,15 @@
   private final com.google.gerrit.server.git.BanCommit banCommit;
 
   @Inject
-  BanCommit(com.google.gerrit.server.git.BanCommit banCommit) {
+  BanCommit(RetryHelper retryHelper, com.google.gerrit.server.git.BanCommit banCommit) {
+    super(retryHelper);
     this.banCommit = banCommit;
   }
 
   @Override
-  public BanResultInfo apply(ProjectResource rsrc, Input input)
-      throws UnprocessableEntityException, AuthException, ResourceConflictException, IOException {
+  protected BanResultInfo applyImpl(
+      BatchUpdate.Factory updateFactory, ProjectResource rsrc, Input input)
+      throws RestApiException, UpdateException, IOException {
     BanResultInfo r = new BanResultInfo();
     if (input != null && input.commits != null && !input.commits.isEmpty()) {
       List<ObjectId> commitsToBan = new ArrayList<>(input.commits.size());
@@ -75,8 +80,6 @@
         r.ignored = transformCommits(result.getIgnoredObjectIds());
       } catch (PermissionDeniedException e) {
         throw new AuthException(e.getMessage());
-      } catch (ConcurrentRefUpdateException e) {
-        throw new ResourceConflictException(e.getMessage(), e);
       }
     }
     return r;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index f4ca925..d338e73 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -247,9 +247,8 @@
     return (isOwner() // owner (aka creator) of the change can abandon
             || getRefControl().isOwner() // branch owner can abandon
             || getProjectControl().isOwner() // project owner can abandon
-            || getUser().getCapabilities().isAdmin_DoNotUse() // site administers are god
             || getRefControl().canAbandon() // user can abandon a specific ref
-        )
+            || getProjectControl().isAdmin())
         && !isPatchSetLocked(db);
   }
 
@@ -266,13 +265,11 @@
 
     switch (status) {
       case DRAFT:
-        return isOwner()
-            || getRefControl().canDeleteDrafts()
-            || getUser().getCapabilities().isAdmin_DoNotUse();
+        return isOwner() || getRefControl().canDeleteDrafts() || getProjectControl().isAdmin();
       case NEW:
       case ABANDONED:
         return (isOwner() && getRefControl().canDeleteOwnChanges())
-            || getUser().getCapabilities().isAdmin_DoNotUse();
+            || getProjectControl().isAdmin();
       case MERGED:
       default:
         return false;
@@ -410,7 +407,7 @@
       if (getRefControl().canRemoveReviewer() // has removal permissions
           || getRefControl().isOwner() // branch owner
           || getProjectControl().isOwner() // project owner
-          || getUser().getCapabilities().isAdmin_DoNotUse()) {
+          || getProjectControl().isAdmin()) {
         return true;
       }
     }
@@ -424,9 +421,8 @@
       return isOwner() // owner (aka creator) of the change can edit topic
           || getRefControl().isOwner() // branch owner can edit topic
           || getProjectControl().isOwner() // project owner can edit topic
-          || getUser().getCapabilities().isAdmin_DoNotUse() // site administers are god
           || getRefControl().canEditTopicName() // user can edit topic on a specific ref
-      ;
+          || getProjectControl().isAdmin();
     }
     return getRefControl().canForceEditTopicName();
   }
@@ -437,8 +433,7 @@
       return isOwner() // owner (aka creator) of the change can edit desc
           || getRefControl().isOwner() // branch owner can edit desc
           || getProjectControl().isOwner() // project owner can edit desc
-          || getUser().getCapabilities().isAdmin_DoNotUse() // site administers are god
-      ;
+          || getProjectControl().isAdmin();
     }
     return false;
   }
@@ -455,8 +450,8 @@
     return isOwner() // owner (aka creator) of the change can edit hashtags
         || getRefControl().isOwner() // branch owner can edit hashtags
         || getProjectControl().isOwner() // project owner can edit hashtags
-        || getUser().getCapabilities().isAdmin_DoNotUse() // site administers are god
-        || getRefControl().canEditHashtags(); // user can edit hashtag on a specific ref
+        || getRefControl().canEditHashtags() // user can edit hashtag on a specific ref
+        || getProjectControl().isAdmin();
   }
 
   private boolean match(String destBranch, String refPattern) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
index 07be8fe..d263a2c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.permissions.FailedPermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -34,10 +35,12 @@
 @Singleton
 public class DefaultPermissionBackend extends PermissionBackend {
   private final ProjectCache projectCache;
+  private final CapabilityControl.Factory capabilityFactory;
 
   @Inject
-  DefaultPermissionBackend(ProjectCache projectCache) {
+  DefaultPermissionBackend(ProjectCache projectCache, CapabilityControl.Factory capabilityFactory) {
     this.projectCache = projectCache;
+    this.capabilityFactory = capabilityFactory;
   }
 
   @Override
@@ -47,6 +50,7 @@
 
   class WithUserImpl extends WithUser {
     private final CurrentUser user;
+    private CapabilityControl cap;
 
     WithUserImpl(CurrentUser user) {
       this.user = checkNotNull(user, "user");
@@ -86,7 +90,10 @@
     }
 
     private boolean can(GlobalOrPluginPermission perm) throws PermissionBackendException {
-      return user.getCapabilities().doCanForDefaultPermissionBackend(perm);
+      if (cap == null) {
+        cap = capabilityFactory.create(user);
+      }
+      return cap.doCanForDefaultPermissionBackend(perm);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index d328f87..223073f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -47,6 +47,8 @@
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.FailedPermissionBackend;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
 import com.google.gerrit.server.permissions.PermissionBackend.ForProject;
 import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
@@ -132,6 +134,7 @@
   private final Set<AccountGroup.UUID> receiveGroups;
 
   private final String canonicalWebUrl;
+  private final PermissionBackend.WithUser perm;
   private final CurrentUser user;
   private final ProjectState state;
   private final ChangeControl.Factory changeControlFactory;
@@ -157,6 +160,7 @@
       VisibleRefFilter.Factory refFilter,
       Provider<InternalChangeQuery> queryProvider,
       @CanonicalWebUrl @Nullable String canonicalWebUrl,
+      PermissionBackend permissionBackend,
       @Assisted CurrentUser who,
       @Assisted ProjectState ps,
       Metrics metrics) {
@@ -169,6 +173,7 @@
     this.canonicalWebUrl = canonicalWebUrl;
     this.queryProvider = queryProvider;
     this.metrics = metrics;
+    this.perm = permissionBackend.user(who);
     user = who;
     state = ps;
   }
@@ -264,8 +269,16 @@
 
   /** Is this user a project owner? */
   public boolean isOwner() {
-    return (isDeclaredOwner() && !controlForRef("refs/*").isBlocked(Permission.OWNER))
-        || user.getCapabilities().isAdmin_DoNotUse();
+    return (isDeclaredOwner() && !controlForRef("refs/*").isBlocked(Permission.OWNER)) || isAdmin();
+  }
+
+  boolean isAdmin() {
+    try {
+      perm.check(GlobalPermission.ADMINISTRATE_SERVER);
+      return true;
+    } catch (AuthException | PermissionBackendException e) {
+      return false;
+    }
   }
 
   private boolean isDeclaredOwner() {
@@ -278,7 +291,7 @@
 
   /** Does this user have ownership on at least one reference name? */
   public boolean isOwnerAnyRef() {
-    return canPerformOnAnyRef(Permission.OWNER) || user.getCapabilities().isAdmin_DoNotUse();
+    return canPerformOnAnyRef(Permission.OWNER) || isAdmin();
   }
 
   /** @return true if the user can upload to at least one reference */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index cf67474..de22e23 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -201,8 +201,7 @@
       // On the AllProjects project the owner access right cannot be assigned,
       // this why for the AllProjects project we allow administrators to push
       // configuration changes if they have push without being project owner.
-      if (!(projectControl.getProjectState().isAllProjects()
-          && getUser().getCapabilities().isAdmin_DoNotUse())) {
+      if (!(projectControl.getProjectState().isAllProjects() && projectControl.isAdmin())) {
         return false;
       }
     }
@@ -229,8 +228,7 @@
       case UNKNOWN:
       case WEB_BROWSER:
       default:
-        return getUser().getCapabilities().isAdmin_DoNotUse()
-            || (isOwner() && !isForceBlocked(Permission.PUSH));
+        return (isOwner() && !isForceBlocked(Permission.PUSH)) || projectControl.isAdmin();
     }
   }
 
@@ -376,10 +374,10 @@
       case UNKNOWN:
       case WEB_BROWSER:
       default:
-        return getUser().getCapabilities().isAdmin_DoNotUse()
-            || (isOwner() && !isForceBlocked(Permission.PUSH))
+        return (isOwner() && !isForceBlocked(Permission.PUSH))
             || canPushWithForce()
-            || canPerform(Permission.DELETE);
+            || canPerform(Permission.DELETE)
+            || projectControl.isAdmin();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
index 5fb4497..0d0777f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.index.Index;
 import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.index.IndexConfig;
@@ -61,6 +62,7 @@
 
   protected final Provider<CurrentUser> userProvider;
 
+  private final CapabilityControl.Factory capabilityFactory;
   private final Metrics metrics;
   private final SchemaDefinitions<T> schemaDef;
   private final IndexConfig indexConfig;
@@ -76,6 +78,7 @@
 
   protected QueryProcessor(
       Provider<CurrentUser> userProvider,
+      CapabilityControl.Factory capabilityFactory,
       Metrics metrics,
       SchemaDefinitions<T> schemaDef,
       IndexConfig indexConfig,
@@ -83,6 +86,7 @@
       IndexRewriter<T> rewriter,
       String limitField) {
     this.userProvider = userProvider;
+    this.capabilityFactory = capabilityFactory;
     this.metrics = metrics;
     this.schemaDef = schemaDef;
     this.indexConfig = indexConfig;
@@ -231,7 +235,10 @@
 
   private int getPermittedLimit() {
     if (enforceVisibility) {
-      return userProvider.get().getCapabilities().getRange(GlobalCapability.QUERY_LIMIT).getMax();
+      return capabilityFactory
+          .create(userProvider.get())
+          .getRange(GlobalCapability.QUERY_LIMIT)
+          .getMax();
     }
     return Integer.MAX_VALUE;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
index d984e6d..6a9b37c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
@@ -44,6 +45,7 @@
   @Inject
   protected AccountQueryProcessor(
       Provider<CurrentUser> userProvider,
+      CapabilityControl.Factory capabilityFactory,
       Metrics metrics,
       IndexConfig indexConfig,
       AccountIndexCollection indexes,
@@ -51,6 +53,7 @@
       AccountControl.Factory accountControlFactory) {
     super(
         userProvider,
+        capabilityFactory,
         metrics,
         AccountSchemaDefinitions.INSTANCE,
         indexConfig,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 19fcbe3..6c6179b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -41,7 +41,6 @@
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.VersionedAccountDestinations;
@@ -196,7 +195,6 @@
     final AllProjectsName allProjectsName;
     final AllUsersName allUsersName;
     final PermissionBackend permissionBackend;
-    final CapabilityControl.Factory capabilityControlFactory;
     final ChangeControl.GenericFactory changeControlGenericFactory;
     final ChangeData.Factory changeDataFactory;
     final ChangeIndex index;
@@ -236,7 +234,6 @@
         IdentifiedUser.GenericFactory userFactory,
         Provider<CurrentUser> self,
         PermissionBackend permissionBackend,
-        CapabilityControl.Factory capabilityControlFactory,
         ChangeControl.GenericFactory changeControlGenericFactory,
         ChangeNotes.Factory notesFactory,
         ChangeData.Factory changeDataFactory,
@@ -269,7 +266,6 @@
           userFactory,
           self,
           permissionBackend,
-          capabilityControlFactory,
           changeControlGenericFactory,
           notesFactory,
           changeDataFactory,
@@ -304,7 +300,6 @@
         IdentifiedUser.GenericFactory userFactory,
         Provider<CurrentUser> self,
         PermissionBackend permissionBackend,
-        CapabilityControl.Factory capabilityControlFactory,
         ChangeControl.GenericFactory changeControlGenericFactory,
         ChangeNotes.Factory notesFactory,
         ChangeData.Factory changeDataFactory,
@@ -335,7 +330,6 @@
       this.userFactory = userFactory;
       this.self = self;
       this.permissionBackend = permissionBackend;
-      this.capabilityControlFactory = capabilityControlFactory;
       this.notesFactory = notesFactory;
       this.changeControlGenericFactory = changeControlGenericFactory;
       this.changeDataFactory = changeDataFactory;
@@ -372,7 +366,6 @@
           userFactory,
           Providers.of(otherUser),
           permissionBackend,
-          capabilityControlFactory,
           changeControlGenericFactory,
           notesFactory,
           changeDataFactory,
@@ -933,7 +926,7 @@
       for (GroupReference ref : suggestions) {
         ids.add(ref.getUUID());
       }
-      return visibleto(new SingleGroupUser(args.capabilityControlFactory, ids));
+      return visibleto(new SingleGroupUser(ids));
     }
 
     throw error("No user or group matches \"" + who + "\".");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index efe44fa..d8c872d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.QueryOptions;
@@ -65,6 +66,7 @@
   @Inject
   ChangeQueryProcessor(
       Provider<CurrentUser> userProvider,
+      CapabilityControl.Factory capabilityFactory,
       Metrics metrics,
       IndexConfig indexConfig,
       ChangeIndexCollection indexes,
@@ -75,6 +77,7 @@
       DynamicMap<ChangeAttributeFactory> attributeFactories) {
     super(
         userProvider,
+        capabilityFactory,
         metrics,
         ChangeSchemaDefinitions.INSTANCE,
         indexConfig,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
index 2661b8b..a084b35 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -14,25 +14,21 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
-import java.util.Collections;
 import java.util.Set;
 
 public final class SingleGroupUser extends CurrentUser {
   private final GroupMembership groups;
 
-  public SingleGroupUser(
-      CapabilityControl.Factory capabilityControlFactory, AccountGroup.UUID groupId) {
-    this(capabilityControlFactory, Collections.singleton(groupId));
+  public SingleGroupUser(AccountGroup.UUID groupId) {
+    this(ImmutableSet.of(groupId));
   }
 
-  public SingleGroupUser(
-      CapabilityControl.Factory capabilityControlFactory, Set<AccountGroup.UUID> groups) {
-    super(capabilityControlFactory);
+  public SingleGroupUser(Set<AccountGroup.UUID> groups) {
     this.groups = new ListGroupMembership(groups);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
index 1cfab20..6229f18 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
@@ -19,6 +19,7 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.index.IndexPredicate;
@@ -44,6 +45,7 @@
   @Inject
   protected GroupQueryProcessor(
       Provider<CurrentUser> userProvider,
+      CapabilityControl.Factory capabilityFactory,
       Metrics metrics,
       IndexConfig indexConfig,
       GroupIndexCollection indexes,
@@ -51,6 +53,7 @@
       GroupControl.GenericFactory groupControlFactory) {
     super(
         userProvider,
+        capabilityFactory,
         metrics,
         GroupSchemaDefinitions.INSTANCE,
         indexConfig,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
index af55b00..fd0c7fc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
@@ -17,7 +17,6 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.CheckedFuture;
 import com.google.common.util.concurrent.Futures;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -51,7 +50,9 @@
     return new ListResultSet<>(ImmutableList.of());
   }
 
-  private static <T, K extends Key<?>> CheckedFuture<T, OrmException> emptyFuture() {
+  @SuppressWarnings("deprecation")
+  private static <T, K extends Key<?>>
+      com.google.common.util.concurrent.CheckedFuture<T, OrmException> emptyFuture() {
     return Futures.immediateCheckedFuture(null);
   }
 
@@ -164,8 +165,9 @@
       return empty();
     }
 
+    @SuppressWarnings("deprecation")
     @Override
-    public final CheckedFuture<T, OrmException> getAsync(K key) {
+    public final com.google.common.util.concurrent.CheckedFuture<T, OrmException> getAsync(K key) {
       return emptyFuture();
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java
index f8ef5f9..b979636 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java
@@ -21,7 +21,6 @@
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
@@ -81,7 +80,9 @@
     setRequestIds(updates, requestId);
 
     try {
-      List<CheckedFuture<?, IOException>> indexFutures = new ArrayList<>();
+      @SuppressWarnings("deprecation")
+      List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures =
+          new ArrayList<>();
       List<ChangesHandle> handles = new ArrayList<>(updates.size());
       Order order = getOrder(updates, listener);
       try {
@@ -357,12 +358,14 @@
       FusedNoteDbBatchUpdate.this.batchRefUpdate = manager.execute(dryrun);
     }
 
-    List<CheckedFuture<?, IOException>> startIndexFutures() {
+    @SuppressWarnings("deprecation")
+    List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> startIndexFutures() {
       if (dryrun) {
         return ImmutableList.of();
       }
       logDebug("Reindexing {} changes", results.size());
-      List<CheckedFuture<?, IOException>> indexFutures = new ArrayList<>(results.size());
+      List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures =
+          new ArrayList<>(results.size());
       for (Map.Entry<Change.Id, ChangeResult> e : results.entrySet()) {
         Change.Id id = e.getKey();
         switch (e.getValue()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RefUpdateUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RefUpdateUtil.java
new file mode 100644
index 0000000..ab0b78e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RefUpdateUtil.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.update;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.server.git.LockFailureException;
+import java.io.IOException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Static utilities for working with JGit's ref update APIs. */
+public class RefUpdateUtil {
+  /**
+   * Execute a batch ref update, throwing a checked exception if not all updates succeeded.
+   *
+   * @param bru batch update; should already have been executed.
+   * @throws LockFailureException if the transaction was aborted due to lock failure; see {@link
+   *     #checkResults(BatchRefUpdate)} for details.
+   * @throws IOException if any result was not {@code OK}.
+   */
+  public static void executeChecked(BatchRefUpdate bru, RevWalk rw) throws IOException {
+    bru.execute(rw, NullProgressMonitor.INSTANCE);
+    checkResults(bru);
+  }
+
+  /**
+   * Check results of all commands in the update batch, reducing to a single exception if there was
+   * a failure.
+   *
+   * <p>Throws {@link LockFailureException} if at least one command failed with {@code
+   * LOCK_FAILURE}, and the entire transaction was aborted, i.e. any non-{@code LOCK_FAILURE}
+   * results, if there were any, failed with "transaction aborted".
+   *
+   * <p>In particular, if the underlying ref database does not {@link
+   * org.eclipse.jgit.lib.RefDatabase#performsAtomicTransactions() perform atomic transactions},
+   * then a combination of {@code LOCK_FAILURE} on one ref and {@code OK} or another result on other
+   * refs will <em>not</em> throw {@code LockFailureException}.
+   *
+   * @param bru batch update; should already have been executed.
+   * @throws LockFailureException if the transaction was aborted due to lock failure.
+   * @throws IOException if any result was not {@code OK}.
+   */
+  @VisibleForTesting
+  static void checkResults(BatchRefUpdate bru) throws IOException {
+    int lockFailure = 0;
+    int aborted = 0;
+    int failure = 0;
+
+    for (ReceiveCommand cmd : bru.getCommands()) {
+      if (cmd.getResult() != ReceiveCommand.Result.OK) {
+        failure++;
+      }
+      if (cmd.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
+        lockFailure++;
+      } else if (cmd.getResult() == ReceiveCommand.Result.REJECTED_OTHER_REASON
+          && JGitText.get().transactionAborted.equals(cmd.getMessage())) {
+        aborted++;
+      }
+    }
+
+    if (lockFailure + aborted == bru.getCommands().size()) {
+      throw new LockFailureException("Update aborted with one or more lock failures: " + bru);
+    } else if (failure > 0) {
+      throw new IOException("Update failed: " + bru);
+    }
+  }
+
+  private RefUpdateUtil() {}
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
index 3cc2f9e..1561fc4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
@@ -23,7 +23,6 @@
 import com.google.common.base.Stopwatch;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.CheckedFuture;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -321,7 +320,10 @@
   private final ReviewDb db;
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final long skewMs;
-  private final List<CheckedFuture<?, IOException>> indexFutures = new ArrayList<>();
+
+  @SuppressWarnings("deprecation")
+  private final List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures =
+      new ArrayList<>();
 
   @Inject
   ReviewDbBatchUpdate(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java
index ab4b701..b5c7256 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java
@@ -22,7 +22,6 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
-import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
@@ -250,7 +249,8 @@
   private final GitReferenceUpdated gitRefUpdated;
   private final ReviewDb db;
 
-  private List<CheckedFuture<?, IOException>> indexFutures;
+  @SuppressWarnings("deprecation")
+  private List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures;
 
   @Inject
   UnfusedNoteDbBatchUpdate(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/CommitMessageUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/CommitMessageUtil.java
new file mode 100644
index 0000000..fa55597
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/CommitMessageUtil.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+
+/** Utility functions to manipulate commit messages. */
+public class CommitMessageUtil {
+
+  private CommitMessageUtil() {}
+
+  /**
+   * Checks for null or empty commit messages and appends a newline character to the commit message.
+   *
+   * @throws BadRequestException if the commit message is null or empty
+   * @returns the trimmed message with a trailing newline character
+   */
+  public static String checkAndSanitizeCommitMessage(String commitMessage)
+      throws BadRequestException {
+    String wellFormedMessage = Strings.nullToEmpty(commitMessage).trim();
+    if (wellFormedMessage.isEmpty()) {
+      throw new BadRequestException("Commit message cannot be null or empty");
+    }
+    wellFormedMessage = wellFormedMessage + "\n";
+    return wellFormedMessage;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java
index dc7dd3d..8e8db12 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java
@@ -47,7 +47,7 @@
   private final String str;
 
   private RequestId(String resourceId) {
-    Hasher h = Hashing.sha1().newHasher();
+    Hasher h = Hashing.murmur3_128().newHasher();
     h.putLong(Thread.currentThread().getId()).putUnencodedChars(MACHINE_ID);
     str =
         "["
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java
index 4689688..b32bdc6 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.FakeRealm;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.Realm;
@@ -36,7 +35,6 @@
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
-import com.google.inject.util.Providers;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
@@ -93,8 +91,6 @@
                 .toInstance("http://localhost:8080/");
             bind(AccountCache.class).toInstance(accountCache);
             bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
-            bind(CapabilityControl.Factory.class)
-                .toProvider(Providers.<CapabilityControl.Factory>of(null));
             bind(Realm.class).toInstance(mockRealm);
           }
         };
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index 3bbd335..6fda100 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -27,8 +27,8 @@
         new FakeQueryBuilder.Definition<>(FakeQueryBuilder.class),
         new ChangeQueryBuilder.Arguments(
             null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-            null, null, null, null, null, null, null, null, indexes, null, null, null, null, null,
-            null, null, null, null));
+            null, null, null, null, null, null, null, indexes, null, null, null, null, null, null,
+            null, null, null));
   }
 
   @Operator
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index 86cce09..f59063a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.FakeRealm;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.Realm;
@@ -158,8 +157,6 @@
                 bind(NotesMigration.class).toInstance(MIGRATION);
                 bind(GitRepositoryManager.class).toInstance(repoManager);
                 bind(ProjectCache.class).toProvider(Providers.<ProjectCache>of(null));
-                bind(CapabilityControl.Factory.class)
-                    .toProvider(Providers.<CapabilityControl.Factory>of(null));
                 bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(testConfig);
                 bind(String.class)
                     .annotatedWith(AnonymousCowardName.class)
@@ -199,7 +196,7 @@
     changeOwner = userFactory.create(co.getId());
     otherUser = userFactory.create(ou.getId());
     otherUserId = otherUser.getAccountId();
-    internalUser = new InternalUser(null);
+    internalUser = new InternalUser();
   }
 
   private void setTimeForTesting() {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 51bff6c..9b9cfff 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -48,7 +48,6 @@
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.CapabilityCollection;
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -58,6 +57,7 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.index.SingleVersionModule.SingleVersionListener;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -206,8 +206,8 @@
   private ChangeControl.Factory changeControlFactory;
   private ReviewDb db;
 
+  @Inject private PermissionBackend permissionBackend;
   @Inject private CapabilityCollection.Factory capabilityCollectionFactory;
-  @Inject private CapabilityControl.Factory capabilityControlFactory;
   @Inject private SchemaCreator schemaCreator;
   @Inject private SingleVersionListener singleVersionListener;
   @Inject private InMemoryDatabase schemaFactory;
@@ -910,6 +910,7 @@
         null, // refFilter
         queryProvider,
         canonicalWebUrl,
+        permissionBackend,
         new MockUser(name, memberOf),
         newProjectState(local),
         metrics);
@@ -925,7 +926,6 @@
     private final GroupMembership groups;
 
     MockUser(String name, AccountGroup.UUID[] groupId) {
-      super(capabilityControlFactory);
       username = name;
       ArrayList<AccountGroup.UUID> groupIds = Lists.newArrayList(groupId);
       groupIds.add(REGISTERED_USERS);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbUpdateManagerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/update/RefUpdateUtilTest.java
similarity index 91%
rename from gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbUpdateManagerTest.java
rename to gerrit-server/src/test/java/com/google/gerrit/server/update/RefUpdateUtilTest.java
index 12eb39d..286827a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbUpdateManagerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/update/RefUpdateUtilTest.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.notedb;
+package com.google.gerrit.server.update;
 
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.truth.Truth.assertThat;
@@ -32,9 +32,8 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/** Unit tests for {@link NoteDbUpdateManager}. */
 @RunWith(JUnit4.class)
-public class NoteDbUpdateManagerTest {
+public class RefUpdateUtilTest {
   private static final Consumer<ReceiveCommand> OK = c -> c.setResult(ReceiveCommand.Result.OK);
   private static final Consumer<ReceiveCommand> LOCK_FAILURE =
       c -> c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
@@ -76,13 +75,13 @@
 
   @SafeVarargs
   private static void checkResults(Consumer<ReceiveCommand>... resultSetters) throws Exception {
-    NoteDbUpdateManager.checkResults(newBatchRefUpdate(resultSetters));
+    RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
   }
 
   @SafeVarargs
   private static void assertIoException(Consumer<ReceiveCommand>... resultSetters) {
     try {
-      NoteDbUpdateManager.checkResults(newBatchRefUpdate(resultSetters));
+      RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
       assert_().fail("expected IOException");
     } catch (IOException e) {
       assertThat(e).isNotInstanceOf(LockFailureException.class);
@@ -93,7 +92,7 @@
   private static void assertLockFailureException(Consumer<ReceiveCommand>... resultSetters)
       throws Exception {
     try {
-      NoteDbUpdateManager.checkResults(newBatchRefUpdate(resultSetters));
+      RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
       assert_().fail("expected LockFailureException");
     } catch (LockFailureException e) {
       // Expected.
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/SshMode.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/SshMode.java
index 9320331..0bf643cc 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/SshMode.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/SshMode.java
@@ -19,6 +19,12 @@
 import com.google.common.base.Enums;
 import com.google.common.base.Strings;
 
+/**
+ * Whether to enable/disable tests using SSH by inspecting the global environment.
+ *
+ * <p>Acceptance tests should generally not inspect this directly, since SSH may also be disabled on
+ * a per-class or per-method basis. Inject {@code @SshEnabled boolean} instead.
+ */
 public enum SshMode {
   /** Tests annotated with UseSsh will be disabled. */
   NO,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
index 4a91995..c42fffa 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
@@ -111,6 +111,7 @@
     return setFrom(NoteDbMode.get().migration);
   }
 
+  @Override
   public TestNotesMigration setFrom(NotesMigration other) {
     setWriteChanges(other.rawWriteChangesSetting());
     setReadChanges(other.readChanges());
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorProvider.java
index c807acf..5bc59e9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorProvider.java
@@ -15,24 +15,27 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.git.QueueProvider;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 
 class CommandExecutorProvider implements Provider<ScheduledThreadPoolExecutor> {
-
+  private final CapabilityControl.Factory capabilityFactory;
   private final QueueProvider queues;
   private final CurrentUser user;
 
   @Inject
-  CommandExecutorProvider(QueueProvider queues, CurrentUser user) {
+  CommandExecutorProvider(
+      CapabilityControl.Factory capabilityFactory, QueueProvider queues, CurrentUser user) {
+    this.capabilityFactory = capabilityFactory;
     this.queues = queues;
     this.user = user;
   }
 
   @Override
   public ScheduledThreadPoolExecutor get() {
-    return queues.getQueue(user.getCapabilities().getQueueType());
+    return queues.getQueue(capabilityFactory.create(user).getQueueType());
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
index 5a93898..f729b931 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.ThreadFactory;
 import org.eclipse.jgit.lib.Config;
 
 public class CommandExecutorQueueProvider implements QueueProvider {
@@ -42,27 +41,13 @@
       poolSize += batchThreads;
     }
     int interactiveThreads = Math.max(1, poolSize - batchThreads);
-    interactiveExecutor = queues.createQueue(interactiveThreads, "SSH-Interactive-Worker");
+    interactiveExecutor =
+        queues.createQueue(interactiveThreads, "SSH-Interactive-Worker", Thread.MIN_PRIORITY);
     if (batchThreads != 0) {
-      batchExecutor = queues.createQueue(batchThreads, "SSH-Batch-Worker");
-      setThreadFactory(batchExecutor);
+      batchExecutor = queues.createQueue(batchThreads, "SSH-Batch-Worker", Thread.MIN_PRIORITY);
     } else {
       batchExecutor = interactiveExecutor;
     }
-    setThreadFactory(interactiveExecutor);
-  }
-
-  private void setThreadFactory(ScheduledThreadPoolExecutor executor) {
-    final ThreadFactory parent = executor.getThreadFactory();
-    executor.setThreadFactory(
-        new ThreadFactory() {
-          @Override
-          public Thread newThread(Runnable task) {
-            final Thread t = parent.newThread(task);
-            t.setPriority(Thread.MIN_PRIORITY);
-            return t;
-          }
-        });
   }
 
   @Override
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/StreamCommandExecutorProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/StreamCommandExecutorProvider.java
index 6ab3ae7..c3c6306 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/StreamCommandExecutorProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/StreamCommandExecutorProvider.java
@@ -19,7 +19,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.ThreadFactory;
 import org.eclipse.jgit.lib.Config;
 
 class StreamCommandExecutorProvider implements Provider<ScheduledThreadPoolExecutor> {
@@ -35,20 +34,6 @@
 
   @Override
   public ScheduledThreadPoolExecutor get() {
-    final ScheduledThreadPoolExecutor executor;
-
-    executor = queues.createQueue(poolSize, "SSH-Stream-Worker");
-
-    final ThreadFactory parent = executor.getThreadFactory();
-    executor.setThreadFactory(
-        new ThreadFactory() {
-          @Override
-          public Thread newThread(Runnable task) {
-            final Thread t = parent.newThread(task);
-            t.setPriority(Thread.MIN_PRIORITY);
-            return t;
-          }
-        });
-    return executor;
+    return queues.createQueue(poolSize, "SSH-Stream-Worker", Thread.MIN_PRIORITY);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index 78726a0..22d7e9a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -18,7 +18,6 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.Lists;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.project.BanCommit;
 import com.google.gerrit.server.project.BanCommit.BanResultInfo;
 import com.google.gerrit.server.project.ProjectControl;
@@ -26,7 +25,6 @@
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
@@ -77,7 +75,7 @@
       printCommits(r.newlyBanned, "The following commits were banned");
       printCommits(r.alreadyBanned, "The following commits were already banned");
       printCommits(r.ignored, "The following ids do not represent commits and were ignored");
-    } catch (RestApiException | IOException e) {
+    } catch (Exception e) {
       throw die(e);
     }
   }
diff --git a/lib/fonts/BUILD b/lib/fonts/BUILD
index fb5ea84..6b3b4ee 100644
--- a/lib/fonts/BUILD
+++ b/lib/fonts/BUILD
@@ -1,13 +1,13 @@
 load("//tools/bzl:genrule2.bzl", "genrule2")
 
-# Source Code Pro. Version 2.010 Roman / 1.030 Italics
-# https://github.com/adobe-fonts/source-code-pro/releases/tag/2.010R-ro%2F1.030R-it
+# Roboto Mono. Version 2.136
+# https://github.com/google/roboto/releases/tag/v2.136
 filegroup(
-    name = "sourcecodepro",
+    name = "robotomono",
     srcs = [
-        "SourceCodePro-Regular.woff",
-        "SourceCodePro-Regular.woff2",
+        "RobotoMono-Regular.woff",
+        "RobotoMono-Regular.woff2",
     ],
-    data = ["//lib:LICENSE-OFL1.1"],
+    data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
-)
+)
\ No newline at end of file
diff --git a/lib/fonts/RobotoMono-Regular.woff b/lib/fonts/RobotoMono-Regular.woff
new file mode 100755
index 0000000..1ed8af5
--- /dev/null
+++ b/lib/fonts/RobotoMono-Regular.woff
Binary files differ
diff --git a/lib/fonts/RobotoMono-Regular.woff2 b/lib/fonts/RobotoMono-Regular.woff2
new file mode 100755
index 0000000..1142739
--- /dev/null
+++ b/lib/fonts/RobotoMono-Regular.woff2
Binary files differ
diff --git a/lib/guava.bzl b/lib/guava.bzl
index c71379e..768b99e 100644
--- a/lib/guava.bzl
+++ b/lib/guava.bzl
@@ -1,5 +1,5 @@
-GUAVA_VERSION = "21.0"
+GUAVA_VERSION = "22.0"
 
-GUAVA_BIN_SHA1 = "3a3d111be1be1b745edfa7d91678a12d7ed38709"
+GUAVA_BIN_SHA1 = "3564ef3803de51fb0530a8377ec6100b33b0d073"
 
 GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
diff --git a/lib/highlightjs/building.md b/lib/highlightjs/building.md
index 8cb9e8b..842efa9 100644
--- a/lib/highlightjs/building.md
+++ b/lib/highlightjs/building.md
@@ -35,6 +35,7 @@
           java \
           javascript \
           json \
+          kotlin \
           lisp \
           lua \
           markdown \
diff --git a/lib/highlightjs/highlight.min.js b/lib/highlightjs/highlight.min.js
index cfc8c1c..60a005b 100644
--- a/lib/highlightjs/highlight.min.js
+++ b/lib/highlightjs/highlight.min.js
@@ -1,105 +1,117 @@
-/*! highlight.js v9.5.0 | BSD3 License | git.io/hljslicense */
-(function(b){var p="object"===typeof window&&window||"object"===typeof self&&self;"undefined"!==typeof exports?b(exports):p&&(p.hljs=b({}),"function"===typeof define&&define.amd&&define([],function(){return p.hljs}))})(function(b){function p(a){return a.replace(/[&<>]/gm,function(a){return M[a]})}function C(a,c){var e=a&&a.exec(c);return e&&0===e.index}function v(a,c){var e,b={};for(e in a)b[e]=a[e];if(c)for(e in c)b[e]=c[e];return b}function H(a){var c=[];(function g(a,b){for(var k=a.firstChild;k;k=
-k.nextSibling)3===k.nodeType?b+=k.nodeValue.length:1===k.nodeType&&(c.push({event:"start",offset:b,node:k}),b=g(k,b),k.nodeName.toLowerCase().match(/br|hr|img|input/)||c.push({event:"stop",offset:b,node:k}));return b})(a,0);return c}function N(a,c,e){function b(){return a.length&&c.length?a[0].offset!==c[0].offset?a[0].offset<c[0].offset?a:c:"start"===c[0].event?a:c:a.length?a:c}function d(a){n+="<"+a.nodeName.toLowerCase()+I.map.call(a.attributes,function(a){return" "+a.nodeName+'="'+p(a.value)+
-'"'}).join("")+">"}function f(a){n+="</"+a.nodeName.toLowerCase()+">"}function k(a){("start"===a.event?d:f)(a.node)}for(var l=0,n="",m=[];a.length||c.length;){var h=b(),n=n+p(e.substr(l,h[0].offset-l)),l=h[0].offset;if(h===a){m.reverse().forEach(f);do k(h.splice(0,1)[0]),h=b();while(h===a&&h.length&&h[0].offset===l);m.reverse().forEach(d)}else"start"===h[0].event?m.push(h[0].node):m.pop(),k(h.splice(0,1)[0])}return n+p(e.substr(l))}function O(a){function c(a){return a&&a.source||a}function e(e,b){return new RegExp(c(e),
-"m"+(a.case_insensitive?"i":"")+(b?"g":""))}function b(d,f){if(!d.compiled){d.compiled=!0;d.keywords=d.keywords||d.beginKeywords;if(d.keywords){var k={},l=function(c,e){a.case_insensitive&&(e=e.toLowerCase());e.split(" ").forEach(function(a){a=a.split("|");k[a[0]]=[c,a[1]?Number(a[1]):1]})};"string"===typeof d.keywords?l("keyword",d.keywords):D(d.keywords).forEach(function(a){l(a,d.keywords[a])});d.keywords=k}d.lexemesRe=e(d.lexemes||/\w+/,!0);f&&(d.beginKeywords&&(d.begin="\\b("+d.beginKeywords.split(" ").join("|")+
-")\\b"),d.begin||(d.begin=/\B|\b/),d.beginRe=e(d.begin),d.end||d.endsWithParent||(d.end=/\B|\b/),d.end&&(d.endRe=e(d.end)),d.terminator_end=c(d.end)||"",d.endsWithParent&&f.terminator_end&&(d.terminator_end+=(d.end?"|":"")+f.terminator_end));d.illegal&&(d.illegalRe=e(d.illegal));null==d.relevance&&(d.relevance=1);d.contains||(d.contains=[]);var n=[];d.contains.forEach(function(a){a.variants?a.variants.forEach(function(c){n.push(v(a,c))}):n.push("self"===a?d:a)});d.contains=n;d.contains.forEach(function(a){b(a,
-d)});d.starts&&b(d.starts,f);var m=d.contains.map(function(a){return a.beginKeywords?"\\.?("+a.begin+")\\.?":a.begin}).concat([d.terminator_end,d.illegal]).map(c).filter(Boolean);d.terminators=m.length?e(m.join("|"),!0):{exec:function(){return null}}}}b(a)}function A(a,c,e,b){function d(a,c){if(C(a.endRe,c)){for(;a.endsParent&&a.parent;)a=a.parent;return a}if(a.endsWithParent)return d(a.parent,c)}function f(a,c,e,b){return'<span class="'+(b?"":t.classPrefix)+(a+'">')+c+(e?"":"</span>")}function k(){var a=
-r,c;if(null!=h.subLanguage)if((c="string"===typeof h.subLanguage)&&!w[h.subLanguage])c=p(q);else{var e=c?A(h.subLanguage,q,!0,u[h.subLanguage]):F(q,h.subLanguage.length?h.subLanguage:void 0);0<h.relevance&&(B+=e.relevance);c&&(u[h.subLanguage]=e.top);c=f(e.language,e.value,!1,!0)}else{var b;if(h.keywords){e="";b=0;h.lexemesRe.lastIndex=0;for(c=h.lexemesRe.exec(q);c;){e+=p(q.substr(b,c.index-b));b=h;var d=c,d=m.case_insensitive?d[0].toLowerCase():d[0];(b=b.keywords.hasOwnProperty(d)&&b.keywords[d])?
-(B+=b[1],e+=f(b[0],p(c[0]))):e+=p(c[0]);b=h.lexemesRe.lastIndex;c=h.lexemesRe.exec(q)}c=e+p(q.substr(b))}else c=p(q)}r=a+c;q=""}function l(a){r+=a.className?f(a.className,"",!0):"";h=Object.create(a,{parent:{value:h}})}function n(a,c){q+=a;if(null==c)return k(),0;var b;a:{b=h;var f,g;f=0;for(g=b.contains.length;f<g;f++)if(C(b.contains[f].beginRe,c)){b=b.contains[f];break a}b=void 0}if(b)return b.skip?q+=c:(b.excludeBegin&&(q+=c),k(),b.returnBegin||b.excludeBegin||(q=c)),l(b,c),b.returnBegin?0:c.length;
-if(b=d(h,c)){f=h;f.skip?q+=c:(f.returnEnd||f.excludeEnd||(q+=c),k(),f.excludeEnd&&(q=c));do h.className&&(r+="</span>"),h.skip||(B+=h.relevance),h=h.parent;while(h!==b.parent);b.starts&&l(b.starts,"");return f.returnEnd?0:c.length}if(!e&&C(h.illegalRe,c))throw Error('Illegal lexeme "'+c+'" for mode "'+(h.className||"<unnamed>")+'"');q+=c;return c.length||1}var m=x(a);if(!m)throw Error('Unknown language: "'+a+'"');O(m);var h=b||m,u={},r="";for(b=h;b!==m;b=b.parent)b.className&&(r=f(b.className,"",
-!0)+r);var q="",B=0;try{for(var y,v,z=0;;){h.terminators.lastIndex=z;y=h.terminators.exec(c);if(!y)break;v=n(c.substr(z,y.index-z),y[0]);z=y.index+v}n(c.substr(z));for(b=h;b.parent;b=b.parent)b.className&&(r+="</span>");return{relevance:B,value:r,language:a,top:h}}catch(E){if(E.message&&-1!==E.message.indexOf("Illegal"))return{relevance:0,value:p(c)};throw E;}}function F(a,c){c=c||t.languages||D(w);var b={relevance:0,value:p(a)},g=b;c.filter(x).forEach(function(c){var f=A(c,a,!1);f.language=c;f.relevance>
-g.relevance&&(g=f);f.relevance>b.relevance&&(g=b,b=f)});g.language&&(b.second_best=g);return b}function J(a){return t.tabReplace||t.useBR?a.replace(P,function(a,b){if(t.useBR&&"\n"===a)return"<br>";if(t.tabReplace)return b.replace(/\t/g,t.tabReplace)}):a}function K(a){var c,b,g,d,f;a:if(b=a.className+" ",b+=a.parentNode?a.parentNode.className:"",f=Q.exec(b))f=x(f[1])?f[1]:"no-highlight";else{b=b.split(/\s+/);f=0;for(d=b.length;f<d;f++)if(c=b[f],L.test(c)||x(c)){f=c;break a}f=void 0}L.test(f)||(t.useBR?
-(c=document.createElementNS("http://www.w3.org/1999/xhtml","div"),c.innerHTML=a.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n")):c=a,d=c.textContent,b=f?A(f,d,!0):F(d),c=H(c),c.length&&(g=document.createElementNS("http://www.w3.org/1999/xhtml","div"),g.innerHTML=b.value,b.value=N(c,H(g),d)),b.value=J(b.value),a.innerHTML=b.value,d=a.className,f=f?G[f]:b.language,c=[d.trim()],d.match(/\bhljs\b/)||c.push("hljs"),-1===d.indexOf(f)&&c.push(f),f=c.join(" ").trim(),a.className=f,a.result={language:b.language,
-re:b.relevance},b.second_best&&(a.second_best={language:b.second_best.language,re:b.second_best.relevance}))}function u(){if(!u.called){u.called=!0;var a=document.querySelectorAll("pre code");I.forEach.call(a,K)}}function x(a){a=(a||"").toLowerCase();return w[a]||w[G[a]]}var I=[],D=Object.keys,w={},G={},L=/^(no-?highlight|plain|text)$/i,Q=/\blang(?:uage)?-([\w-]+)\b/i,P=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,t={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},M={"&":"&amp;","<":"&lt;",">":"&gt;"};
-b.highlight=A;b.highlightAuto=F;b.fixMarkup=J;b.highlightBlock=K;b.configure=function(a){t=v(t,a)};b.initHighlighting=u;b.initHighlightingOnLoad=function(){addEventListener("DOMContentLoaded",u,!1);addEventListener("load",u,!1)};b.registerLanguage=function(a,c){var e=w[a]=c(b);e.aliases&&e.aliases.forEach(function(c){G[c]=a})};b.listLanguages=function(){return D(w)};b.getLanguage=x;b.inherit=v;b.IDENT_RE="[a-zA-Z]\\w*";b.UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*";b.NUMBER_RE="\\b\\d+(\\.\\d+)?";b.C_NUMBER_RE=
-"(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";b.BINARY_NUMBER_RE="\\b(0b[01]+)";b.RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";b.BACKSLASH_ESCAPE={begin:"\\\\[\\s\\S]",relevance:0};b.APOS_STRING_MODE={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.QUOTE_STRING_MODE={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};
-b.PHRASAL_WORDS_MODE={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/};b.COMMENT=function(a,c,e){a=b.inherit({className:"comment",begin:a,end:c,contains:[]},e||{});a.contains.push(b.PHRASAL_WORDS_MODE);a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|XXX):",relevance:0});return a};b.C_LINE_COMMENT_MODE=b.COMMENT("//","$");b.C_BLOCK_COMMENT_MODE=b.COMMENT("/\\*","\\*/");b.HASH_COMMENT_MODE=b.COMMENT("#",
-"$");b.NUMBER_MODE={className:"number",begin:b.NUMBER_RE,relevance:0};b.C_NUMBER_MODE={className:"number",begin:b.C_NUMBER_RE,relevance:0};b.BINARY_NUMBER_MODE={className:"number",begin:b.BINARY_NUMBER_RE,relevance:0};b.CSS_NUMBER_MODE={className:"number",begin:b.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0};b.REGEXP_MODE={className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[b.BACKSLASH_ESCAPE,{begin:/\[/,
-end:/\]/,relevance:0,contains:[b.BACKSLASH_ESCAPE]}]};b.TITLE_MODE={className:"title",begin:b.IDENT_RE,relevance:0};b.UNDERSCORE_TITLE_MODE={className:"title",begin:b.UNDERSCORE_IDENT_RE,relevance:0};b.METHOD_GUARD={begin:"\\.\\s*"+b.UNDERSCORE_IDENT_RE,relevance:0};b.registerLanguage("bash",function(a){var c={className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{(.*?)}/}]},b={className:"string",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,c,{className:"variable",begin:/\$\(/,
-end:/\)/,contains:[a.BACKSLASH_ESCAPE]}]};return{aliases:["sh","zsh"],lexemes:/-?[a-z\.]+/,keywords:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",
-_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[{className:"meta",begin:/^#![^\n]+sh\s*$/,relevance:10},{className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0},a.HASH_COMMENT_MODE,b,{className:"string",begin:/'/,end:/'/},c]}});b.registerLanguage("clojure",function(a){var c={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",relevance:0},b=a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),g=a.COMMENT(";","$",{relevance:0}),
-d={className:"literal",begin:/\b(true|false|nil)\b/},f={begin:"[\\[\\{]",end:"[\\]\\}]"},k={className:"comment",begin:"\\^[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},l=a.COMMENT("\\^\\{","\\}"),n={className:"symbol",begin:"[:]{1,2}[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},m={begin:"\\(",end:"\\)"},h={endsWithParent:!0,relevance:0},p={keywords:{"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},
-lexemes:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",className:"name",begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",starts:h},r=[m,b,k,l,g,n,f,c,d,{begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",relevance:0}];m.contains=[a.COMMENT("comment",""),p,h];h.contains=r;f.contains=r;return{aliases:["clj"],illegal:/\S/,contains:[m,b,k,l,g,n,f,c,d]}});b.registerLanguage("cpp",function(a){var c={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},b={className:"string",variants:[{begin:'(u8?|U)?L?"',
-end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'(u8?|U)?R"',end:'"',contains:[a.BACKSLASH_ESCAPE]},{begin:"'\\\\?.",end:"'",illegal:"."}]},g={className:"number",variants:[{begin:"\\b(0b[01'_]+)"},{begin:"\\b([\\d'_]+(\\.[\\d'_]*)?|\\.[\\d'_]+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9'_]+|(\\b[\\d'_]+(\\.[\\d'_]*)?|\\.[\\d'_]+)([eE][-+]?[\\d'_]+)?)"}],relevance:0},d={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},
-contains:[{begin:/\\\n/,relevance:0},a.inherit(b,{className:"meta-string"}),{className:"meta-string",begin:"<",end:">",illegal:"\\n"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},f=a.IDENT_RE+"\\s*\\(",k={keyword:"int float while private char catch export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const struct for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using class asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return",
+/*
+ highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */
+var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(b,g,l){b!=Array.prototype&&b!=Object.prototype&&(b[g]=l.value)};$jscomp.getGlobal=function(b){return"undefined"!=typeof window&&window===b?b:"undefined"!=typeof global&&null!=global?global:b};$jscomp.global=$jscomp.getGlobal(this);$jscomp.SYMBOL_PREFIX="jscomp_symbol_";
+$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){};$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)};$jscomp.symbolCounter_=0;$jscomp.Symbol=function(b){return $jscomp.SYMBOL_PREFIX+(b||"")+$jscomp.symbolCounter_++};
+$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var b=$jscomp.global.Symbol.iterator;b||(b=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator"));"function"!=typeof Array.prototype[b]&&$jscomp.defineProperty(Array.prototype,b,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}});$jscomp.initSymbolIterator=function(){}};$jscomp.arrayIterator=function(b){var g=0;return $jscomp.iteratorPrototype(function(){return g<b.length?{done:!1,value:b[g++]}:{done:!0}})};
+$jscomp.iteratorPrototype=function(b){$jscomp.initSymbolIterator();b={next:b};b[$jscomp.global.Symbol.iterator]=function(){return this};return b};$jscomp.iteratorFromArray=function(b,g){$jscomp.initSymbolIterator();b instanceof String&&(b+="");var l=0,k={next:function(){if(l<b.length){var m=l++;return{value:g(m,b[m]),done:!1}}k.next=function(){return{done:!0,value:void 0}};return k.next()}};k[Symbol.iterator]=function(){return k};return k};
+$jscomp.polyfill=function(b,g,l,k){if(g){l=$jscomp.global;b=b.split(".");for(k=0;k<b.length-1;k++){var m=b[k];m in l||(l[m]={});l=l[m]}b=b[b.length-1];k=l[b];g=g(k);g!=k&&null!=g&&$jscomp.defineProperty(l,b,{configurable:!0,writable:!0,value:g})}};$jscomp.polyfill("Array.prototype.keys",function(b){return b?b:function(){return $jscomp.iteratorFromArray(this,function(b){return b})}},"es6-impl","es3");
+(function(b){var g="object"===typeof window&&window||"object"===typeof self&&self;"undefined"!==typeof exports?b(exports):g&&(g.hljs=b({}),"function"===typeof define&&define.amd&&define([],function(){return g.hljs}))})(function(b){function g(a){return a.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function l(a,f){return(a=a&&a.exec(f))&&0===a.index}function k(a){var f,b={},e=Array.prototype.slice.call(arguments,1);for(f in a)b[f]=a[f];e.forEach(function(a){for(f in a)b[f]=a[f]});
+return b}function m(a){var f=[];(function e(a,b){for(a=a.firstChild;a;a=a.nextSibling)3===a.nodeType?b+=a.nodeValue.length:1===a.nodeType&&(f.push({event:"start",offset:b,node:a}),b=e(a,b),a.nodeName.toLowerCase().match(/br|hr|img|input/)||f.push({event:"stop",offset:b,node:a}));return b})(a,0);return f}function L(a,f,b){function d(){return a.length&&f.length?a[0].offset!==f[0].offset?a[0].offset<f[0].offset?a:f:"start"===f[0].event?a:f:a.length?a:f}function c(a){z+="<"+a.nodeName.toLowerCase()+H.map.call(a.attributes,
+function(a){return" "+a.nodeName+'="'+g(a.value).replace('"',"&quot;")+'"'}).join("")+">"}function q(a){z+="</"+a.nodeName.toLowerCase()+">"}function w(a){("start"===a.event?c:q)(a.node)}for(var r=0,z="",n=[];a.length||f.length;){var h=d(),z=z+g(b.substring(r,h[0].offset)),r=h[0].offset;if(h===a){n.reverse().forEach(q);do w(h.splice(0,1)[0]),h=d();while(h===a&&h.length&&h[0].offset===r);n.reverse().forEach(c)}else"start"===h[0].event?n.push(h[0].node):n.pop(),w(h.splice(0,1)[0])}return z+g(b.substr(r))}
+function M(a){a.variants&&!a.cached_variants&&(a.cached_variants=a.variants.map(function(f){return k(a,{variants:null},f)}));return a.cached_variants||a.endsWithParent&&[k(a)]||[a]}function N(a){function f(a){return a&&a.source||a}function b(b,d){return new RegExp(f(b),"m"+(a.case_insensitive?"i":"")+(d?"g":""))}function e(c,d){if(!c.compiled){c.compiled=!0;c.keywords=c.keywords||c.beginKeywords;if(c.keywords){var q={},g=function(b,f){a.case_insensitive&&(f=f.toLowerCase());f.split(" ").forEach(function(a){a=
+a.split("|");q[a[0]]=[b,a[1]?Number(a[1]):1]})};"string"===typeof c.keywords?g("keyword",c.keywords):x(c.keywords).forEach(function(a){g(a,c.keywords[a])});c.keywords=q}c.lexemesRe=b(c.lexemes||/\w+/,!0);d&&(c.beginKeywords&&(c.begin="\\b("+c.beginKeywords.split(" ").join("|")+")\\b"),c.begin||(c.begin=/\B|\b/),c.beginRe=b(c.begin),c.end||c.endsWithParent||(c.end=/\B|\b/),c.end&&(c.endRe=b(c.end)),c.terminator_end=f(c.end)||"",c.endsWithParent&&d.terminator_end&&(c.terminator_end+=(c.end?"|":"")+
+d.terminator_end));c.illegal&&(c.illegalRe=b(c.illegal));null==c.relevance&&(c.relevance=1);c.contains||(c.contains=[]);c.contains=Array.prototype.concat.apply([],c.contains.map(function(a){return M("self"===a?c:a)}));c.contains.forEach(function(a){e(a,c)});c.starts&&e(c.starts,d);d=c.contains.map(function(a){return a.beginKeywords?"\\.?("+a.begin+")\\.?":a.begin}).concat([c.terminator_end,c.illegal]).map(f).filter(Boolean);c.terminators=d.length?b(d.join("|"),!0):{exec:function(){return null}}}}
+e(a)}function C(a,f,b,e){function c(a,b){if(l(a.endRe,b)){for(;a.endsParent&&a.parent;)a=a.parent;return a}if(a.endsWithParent)return c(a.parent,b)}function d(a,b,f,d){return'<span class="'+(d?"":t.classPrefix)+(a+'">')+b+(f?"":"</span>")}function w(){var a=v,b;if(null!=h.subLanguage)if((b="string"===typeof h.subLanguage)&&!y[h.subLanguage])b=g(p);else{var f=b?C(h.subLanguage,p,!0,m[h.subLanguage]):F(p,h.subLanguage.length?h.subLanguage:void 0);0<h.relevance&&(u+=f.relevance);b&&(m[h.subLanguage]=
+f.top);b=d(f.language,f.value,!1,!0)}else if(h.keywords){f="";var c=0;h.lexemesRe.lastIndex=0;for(b=h.lexemesRe.exec(p);b;){f+=g(p.substring(c,b.index));c=h;var e=b,e=n.case_insensitive?e[0].toLowerCase():e[0];(c=c.keywords.hasOwnProperty(e)&&c.keywords[e])?(u+=c[1],f+=d(c[0],g(b[0]))):f+=g(b[0]);c=h.lexemesRe.lastIndex;b=h.lexemesRe.exec(p)}b=f+g(p.substr(c))}else b=g(p);v=a+b;p=""}function r(a){v+=a.className?d(a.className,"",!0):"";h=Object.create(a,{parent:{value:h}})}function k(a,f){p+=a;if(null==
+f)return w(),0;a:{a=h;var d;var e=0;for(d=a.contains.length;e<d;e++)if(l(a.contains[e].beginRe,f)){a=a.contains[e];break a}a=void 0}if(a)return a.skip?p+=f:(a.excludeBegin&&(p+=f),w(),a.returnBegin||a.excludeBegin||(p=f)),r(a,f),a.returnBegin?0:f.length;if(a=c(h,f)){e=h;e.skip?p+=f:(e.returnEnd||e.excludeEnd||(p+=f),w(),e.excludeEnd&&(p=f));do h.className&&(v+="</span>"),h.skip||(u+=h.relevance),h=h.parent;while(h!==a.parent);a.starts&&r(a.starts,"");return e.returnEnd?0:f.length}if(!b&&l(h.illegalRe,
+f))throw Error('Illegal lexeme "'+f+'" for mode "'+(h.className||"<unnamed>")+'"');p+=f;return f.length||1}var n=A(a);if(!n)throw Error('Unknown language: "'+a+'"');N(n);var h=e||n,m={},v="";for(e=h;e!==n;e=e.parent)e.className&&(v=d(e.className,"",!0)+v);var p="",u=0;try{for(var B,x,D=0;;){h.terminators.lastIndex=D;B=h.terminators.exec(f);if(!B)break;x=k(f.substring(D,B.index),B[0]);D=B.index+x}k(f.substr(D));for(e=h;e.parent;e=e.parent)e.className&&(v+="</span>");return{relevance:u,value:v,language:a,
+top:h}}catch(E){if(E.message&&-1!==E.message.indexOf("Illegal"))return{relevance:0,value:g(f)};throw E;}}function F(a,f){f=f||t.languages||x(y);var b={relevance:0,value:g(a)},e=b;f.filter(A).forEach(function(f){var c=C(f,a,!1);c.language=f;c.relevance>e.relevance&&(e=c);c.relevance>b.relevance&&(e=b,b=c)});e.language&&(b.second_best=e);return b}function I(a){return t.tabReplace||t.useBR?a.replace(O,function(a,b){return t.useBR&&"\n"===a?"<br>":t.tabReplace?b.replace(/\t/g,t.tabReplace):""}):a}function J(a){var b,
+d;a:{var e=a.className+" ";e+=a.parentNode?a.parentNode.className:"";if(d=P.exec(e))d=A(d[1])?d[1]:"no-highlight";else{e=e.split(/\s+/);d=0;for(b=e.length;d<b;d++){var c=e[d];if(K.test(c)||A(c)){d=c;break a}}d=void 0}}if(!K.test(d)){t.useBR?(c=document.createElementNS("http://www.w3.org/1999/xhtml","div"),c.innerHTML=a.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n")):c=a;b=c.textContent;e=d?C(d,b,!0):F(b);c=m(c);if(c.length){var q=document.createElementNS("http://www.w3.org/1999/xhtml","div");
+q.innerHTML=e.value;e.value=L(c,m(q),b)}e.value=I(e.value);a.innerHTML=e.value;b=a.className;d=d?G[d]:e.language;c=[b.trim()];b.match(/\bhljs\b/)||c.push("hljs");-1===b.indexOf(d)&&c.push(d);d=c.join(" ").trim();a.className=d;a.result={language:e.language,re:e.relevance};e.second_best&&(a.second_best={language:e.second_best.language,re:e.second_best.relevance})}}function u(){if(!u.called){u.called=!0;var a=document.querySelectorAll("pre code");H.forEach.call(a,J)}}function A(a){a=(a||"").toLowerCase();
+return y[a]||y[G[a]]}var H=[],x=Object.keys,y={},G={},K=/^(no-?highlight|plain|text)$/i,P=/\blang(?:uage)?-([\w-]+)\b/i,O=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,t={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};b.highlight=C;b.highlightAuto=F;b.fixMarkup=I;b.highlightBlock=J;b.configure=function(a){t=k(t,a)};b.initHighlighting=u;b.initHighlightingOnLoad=function(){addEventListener("DOMContentLoaded",u,!1);addEventListener("load",u,!1)};b.registerLanguage=function(a,f){f=y[a]=f(b);f.aliases&&
+f.aliases.forEach(function(b){G[b]=a})};b.listLanguages=function(){return x(y)};b.getLanguage=A;b.inherit=k;b.IDENT_RE="[a-zA-Z]\\w*";b.UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*";b.NUMBER_RE="\\b\\d+(\\.\\d+)?";b.C_NUMBER_RE="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";b.BINARY_NUMBER_RE="\\b(0b[01]+)";b.RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";b.BACKSLASH_ESCAPE=
+{begin:"\\\\[\\s\\S]",relevance:0};b.APOS_STRING_MODE={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.QUOTE_STRING_MODE={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.PHRASAL_WORDS_MODE={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/};b.COMMENT=function(a,f,d){a=b.inherit({className:"comment",begin:a,end:f,contains:[]},d||{});
+a.contains.push(b.PHRASAL_WORDS_MODE);a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|XXX):",relevance:0});return a};b.C_LINE_COMMENT_MODE=b.COMMENT("//","$");b.C_BLOCK_COMMENT_MODE=b.COMMENT("/\\*","\\*/");b.HASH_COMMENT_MODE=b.COMMENT("#","$");b.NUMBER_MODE={className:"number",begin:b.NUMBER_RE,relevance:0};b.C_NUMBER_MODE={className:"number",begin:b.C_NUMBER_RE,relevance:0};b.BINARY_NUMBER_MODE={className:"number",begin:b.BINARY_NUMBER_RE,relevance:0};b.CSS_NUMBER_MODE={className:"number",
+begin:b.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0};b.REGEXP_MODE={className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[b.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0,contains:[b.BACKSLASH_ESCAPE]}]};b.TITLE_MODE={className:"title",begin:b.IDENT_RE,relevance:0};b.UNDERSCORE_TITLE_MODE={className:"title",begin:b.UNDERSCORE_IDENT_RE,relevance:0};b.METHOD_GUARD={begin:"\\.\\s*"+b.UNDERSCORE_IDENT_RE,relevance:0};
+b.registerLanguage("bash",function(a){var b={className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{(.*?)}/}]},d={className:"string",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,b,{className:"variable",begin:/\$\(/,end:/\)/,contains:[a.BACKSLASH_ESCAPE]}]};return{aliases:["sh","zsh"],lexemes:/\b-?[a-z\._]+\b/,keywords:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",
+_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[{className:"meta",begin:/^#![^\n]+sh\s*$/,relevance:10},{className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0},a.HASH_COMMENT_MODE,d,{className:"string",begin:/'/,end:/'/},b]}});b.registerLanguage("clojure",function(a){var b={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",relevance:0},d=a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),e=a.COMMENT(";","$",{relevance:0}),
+c={className:"literal",begin:/\b(true|false|nil)\b/},q={begin:"[\\[\\{]",end:"[\\]\\}]"},g={className:"comment",begin:"\\^[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},r=a.COMMENT("\\^\\{","\\}"),k={className:"symbol",begin:"[:]{1,2}[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},n={begin:"\\(",end:"\\)"},h={endsWithParent:!0,relevance:0},l={keywords:{"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},
+lexemes:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",className:"name",begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",starts:h},m=[n,d,g,r,e,k,q,b,c,{begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",relevance:0}];n.contains=[a.COMMENT("comment",""),l,h];h.contains=m;q.contains=m;r.contains=[q];return{aliases:["clj"],illegal:/\S/,contains:[n,d,g,r,e,k,q,b,c]}});b.registerLanguage("cpp",function(a){var b={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},d={className:"string",
+variants:[{begin:'(u8?|U)?L?"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'(u8?|U)?R"',end:'"',contains:[a.BACKSLASH_ESCAPE]},{begin:"'\\\\?.",end:"'",illegal:"."}]},e={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},
+contains:[{begin:/\\\n/,relevance:0},a.inherit(d,{className:"meta-string"}),{className:"meta-string",begin:/<[^\n>]*>/,end:/$/,illegal:"\\n"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},q=a.IDENT_RE+"\\s*\\(",g={keyword:"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and or not",
 built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr",
-literal:"true false nullptr NULL"},l=[c,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,g,b];return{aliases:"c cc h c++ h++ hpp".split(" "),keywords:k,illegal:"</",contains:l.concat([d,{begin:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",end:">",keywords:k,contains:["self",c]},{begin:a.IDENT_RE+"::",keywords:k},{variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",
-end:/;/}],keywords:k,contains:l.concat([{begin:/\(/,end:/\)/,keywords:k,contains:l.concat(["self"]),relevance:0}]),relevance:0},{className:"function",begin:"("+a.IDENT_RE+"[\\*&\\s]+)+"+f,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:k,illegal:/[^\w\s\*&]/,contains:[{begin:f,returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:k,relevance:0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b,g,c]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,
-d]}]),exports:{preprocessor:d,strings:b,keywords:k}}});b.registerLanguage("cs",function(a){var c={keyword:"abstract as base bool break byte case catch char checked const continue decimal dynamic default delegate do double else enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long when object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual volatile void while async nameof ascending descending from get group into join let orderby partial select set value var where yield",
-literal:"null false true"},b={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},g=a.inherit(b,{illegal:/\n/}),d={className:"subst",begin:"{",end:"}",keywords:c},f=a.inherit(d,{illegal:/\n/}),k={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},a.BACKSLASH_ESCAPE,f]},l={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},d]},n=a.inherit(l,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},f]});d.contains=
-[l,k,b,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE];f.contains=[n,k,g,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,a.inherit(a.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];b={variants:[l,k,b,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]};g=a.IDENT_RE+"(<"+a.IDENT_RE+">)?(\\[\\])?";return{aliases:["csharp"],keywords:c,illegal:/::/,contains:[a.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:"</?",
-end:">"}]}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},b,a.C_NUMBER_MODE,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:]/,contains:[a.TITLE_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},
-{beginKeywords:"new return throw await",relevance:0},{className:"function",begin:"("+g+"\\s+)+"+a.IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:c,contains:[{begin:a.IDENT_RE+"\\s*\\(",returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:c,relevance:0,contains:[b,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}]}});b.registerLanguage("css",function(a){return{case_insensitive:!0,
-illegal:/[=\/|'\$]/,contains:[a.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/},{className:"selector-class",begin:/\.[A-Za-z0-9_-]+/},{className:"selector-attr",begin:/\[/,end:/\]/,illegal:"$"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"@(font-face|page)",lexemes:"[a-z-]+",keywords:"font-face page"},{begin:"@",end:"[{;]",illegal:/:/,contains:[{className:"keyword",begin:/\w+/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,contains:[a.APOS_STRING_MODE,
-a.QUOTE_STRING_MODE,a.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0},{begin:"{",end:"}",illegal:/\S/,contains:[a.C_BLOCK_COMMENT_MODE,{begin:/[A-Z\_\.\-]+\s*:/,returnBegin:!0,end:";",endsWithParent:!0,contains:[{className:"attribute",begin:/\S/,end:":",excludeEnd:!0,starts:{endsWithParent:!0,excludeEnd:!0,contains:[{begin:/[\w-]+\(/,returnBegin:!0,contains:[{className:"built_in",begin:/[\w-]+/},{begin:/\(/,end:/\)/,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]}]},
-a.CSS_NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",begin:"#[0-9A-Fa-f]+"},{className:"meta",begin:"!important"}]}}]}]}]}});b.registerLanguage("d",function(a){var b=a.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{lexemes:a.UNDERSCORE_IDENT_RE,keywords:{keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__",
+literal:"true false nullptr NULL"},k=[b,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,e,d];return{aliases:"c cc h c++ h++ hpp".split(" "),keywords:g,illegal:"</",contains:k.concat([c,{begin:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",end:">",keywords:g,contains:["self",b]},{begin:a.IDENT_RE+"::",keywords:g},{variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",
+end:/;/}],keywords:g,contains:k.concat([{begin:/\(/,end:/\)/,keywords:g,contains:k.concat(["self"]),relevance:0}]),relevance:0},{className:"function",begin:"("+a.IDENT_RE+"[\\*&\\s]+)+"+q,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:g,illegal:/[^\w\s\*&]/,contains:[{begin:q,returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:g,relevance:0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,d,e,b]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,
+c]},{className:"class",beginKeywords:"class struct",end:/[{;:]/,contains:[{begin:/</,end:/>/,contains:["self"]},a.TITLE_MODE]}]),exports:{preprocessor:c,strings:d,keywords:g}}});b.registerLanguage("cs",function(a){var b={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long nameof object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let on orderby partial remove select set value var where yield",
+literal:"null false true"},d={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},e=a.inherit(d,{illegal:/\n/}),c={className:"subst",begin:"{",end:"}",keywords:b},g=a.inherit(c,{illegal:/\n/}),k={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},a.BACKSLASH_ESCAPE,g]},l={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},c]},m=a.inherit(l,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},g]});c.contains=
+[l,k,d,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE];g.contains=[m,k,e,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,a.inherit(a.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];d={variants:[l,k,d,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]};e=a.IDENT_RE+"(<"+a.IDENT_RE+"(\\s*,\\s*"+a.IDENT_RE+")*>)?(\\[\\])?";return{aliases:["csharp"],keywords:b,illegal:/::/,contains:[a.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},
+{begin:"\x3c!--|--\x3e"},{begin:"</?",end:">"}]}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},d,a.C_NUMBER_MODE,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:]/,contains:[a.TITLE_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),
+a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"meta",begin:"^\\s*\\[",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{className:"meta-string",begin:/"/,end:/"/}]},{beginKeywords:"new return throw await else",relevance:0},{className:"function",begin:"("+e+"\\s+)+"+a.IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:b,contains:[{begin:a.IDENT_RE+"\\s*\\(",returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,
+keywords:b,relevance:0,contains:[d,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}]}});b.registerLanguage("css",function(a){return{case_insensitive:!0,illegal:/[=\/|'\$]/,contains:[a.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/},{className:"selector-class",begin:/\.[A-Za-z0-9_-]+/},{className:"selector-attr",begin:/\[/,end:/\]/,illegal:"$"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"@(font-face|page)",
+lexemes:"[a-z-]+",keywords:"font-face page"},{begin:"@",end:"[{;]",illegal:/:/,contains:[{className:"keyword",begin:/\w+/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0},{begin:"{",end:"}",illegal:/\S/,contains:[a.C_BLOCK_COMMENT_MODE,{begin:/[A-Z\_\.\-]+\s*:/,returnBegin:!0,end:";",endsWithParent:!0,contains:[{className:"attribute",begin:/\S/,end:":",
+excludeEnd:!0,starts:{endsWithParent:!0,excludeEnd:!0,contains:[{begin:/[\w-]+\(/,returnBegin:!0,contains:[{className:"built_in",begin:/[\w-]+/},{begin:/\(/,end:/\)/,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]}]},a.CSS_NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",begin:"#[0-9A-Fa-f]+"},{className:"meta",begin:"!important"}]}}]}]}]}});b.registerLanguage("d",function(a){var b=a.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{lexemes:a.UNDERSCORE_IDENT_RE,
+keywords:{keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__",
 built_in:"bool cdouble cent cfloat char creal dchar delegate double dstring float function idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar wstring",literal:"false null true"},contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b,{className:"string",begin:'x"[\\da-fA-F\\s\\n\\r]*"[cwd]?',relevance:10},{className:"string",begin:'"',contains:[{begin:"\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};",relevance:0}],
 end:'"[cwd]?'},{className:"string",begin:'[rq]"',end:'"[cwd]?',relevance:5},{className:"string",begin:"`",end:"`[cwd]?"},{className:"string",begin:'q"\\{',end:'\\}"'},{className:"number",begin:"\\b(((0[xX](([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)\\.([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)|\\.?([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))[pP][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))|((0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(\\.\\d*|([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)))|\\d+\\.(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)|\\.(0|[1-9][\\d_]*)([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))?))([fF]|L|i|[fF]i|Li)?|((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))(i|[fF]i|Li))",
 relevance:0},{className:"number",begin:"\\b((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))(L|u|U|Lu|LU|uL|UL)?",relevance:0},{className:"string",begin:"'(\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};|.)",end:"'",illegal:"."},{className:"meta",begin:"^#!",end:"$",relevance:5},{className:"meta",begin:"#(line)",end:"$",relevance:5},{className:"keyword",begin:"@[a-zA-Z_][a-zA-Z_\\d]*"}]}});b.registerLanguage("markdown",
 function(a){return{aliases:["md","mkdown","mkd"],contains:[{className:"section",variants:[{begin:"^#{1,6}",end:"$"},{begin:"^.+?\\n[=-]{2,}$"}]},{begin:"<",end:">",subLanguage:"xml",relevance:0},{className:"bullet",begin:"^([*+-]|(\\d+\\.))\\s+"},{className:"strong",begin:"[*_]{2}.+?[*_]{2}"},{className:"emphasis",variants:[{begin:"\\*.+?\\*"},{begin:"_.+?_",relevance:0}]},{className:"quote",begin:"^>\\s+",end:"$"},{className:"code",variants:[{begin:"^```w*s*$",end:"^```s*$"},{begin:"`.+?`"},{begin:"^( {4}|\t)",
 end:"$",relevance:0}]},{begin:"^[-\\*]{3,}",end:"$"},{begin:"\\[.+?\\][\\(\\[].*?[\\)\\]]",returnBegin:!0,contains:[{className:"string",begin:"\\[",end:"\\]",excludeBegin:!0,returnEnd:!0,relevance:0},{className:"link",begin:"\\]\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"symbol",begin:"\\]\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0}],relevance:10},{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{className:"link",
-begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}});b.registerLanguage("dart",function(a){var b={className:"subst",begin:"\\$\\{",end:"}",keywords:"true false null this is new super"},e={className:"string",variants:[{begin:"r'''",end:"'''"},{begin:'r"""',end:'"""'},{begin:"r'",end:"'",illegal:"\\n"},{begin:'r"',end:'"',illegal:"\\n"},{begin:"'''",end:"'''",contains:[a.BACKSLASH_ESCAPE,b]},{begin:'"""',end:'"""',contains:[a.BACKSLASH_ESCAPE,b]},{begin:"'",end:"'",illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,
-b]},{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]}]};b.contains=[a.C_NUMBER_MODE,e];return{keywords:{keyword:"assert async await break case catch class const continue default do else enum extends false final finally for if in is new null rethrow return super switch sync this throw true try var void while with yield abstract as dynamic export external factory get implements import library operator part set static typedef",built_in:"print Comparable DateTime Duration Function Iterable Iterator List Map Match Null Object Pattern RegExp Set Stopwatch String StringBuffer StringSink Symbol Type Uri bool double int num document window querySelector querySelectorAll Element ElementList"},
-contains:[e,a.COMMENT("/\\*\\*","\\*/",{subLanguage:"markdown"}),a.COMMENT("///","$",{subLanguage:"markdown"}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"},{begin:"=>"}]}});b.registerLanguage("go",function(a){var b={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",
+begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}});b.registerLanguage("dart",function(a){var b={className:"subst",begin:"\\$\\{",end:"}",keywords:"true false null this is new super"},d={className:"string",variants:[{begin:"r'''",end:"'''"},{begin:'r"""',end:'"""'},{begin:"r'",end:"'",illegal:"\\n"},{begin:'r"',end:'"',illegal:"\\n"},{begin:"'''",end:"'''",contains:[a.BACKSLASH_ESCAPE,b]},{begin:'"""',end:'"""',contains:[a.BACKSLASH_ESCAPE,b]},{begin:"'",end:"'",illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,
+b]},{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]}]};b.contains=[a.C_NUMBER_MODE,d];return{keywords:{keyword:"assert async await break case catch class const continue default do else enum extends false final finally for if in is new null rethrow return super switch sync this throw true try var void while with yield abstract as dynamic export external factory get implements import library operator part set static typedef",built_in:"print Comparable DateTime Duration Function Iterable Iterator List Map Match Null Object Pattern RegExp Set Stopwatch String StringBuffer StringSink Symbol Type Uri bool double int num document window querySelector querySelectorAll Element ElementList"},
+contains:[d,a.COMMENT("/\\*\\*","\\*/",{subLanguage:"markdown"}),a.COMMENT("///","$",{subLanguage:"markdown"}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"},{begin:"=>"}]}});b.registerLanguage("go",function(a){var b={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",
 literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{aliases:["golang"],keywords:b,illegal:"</",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[a.QUOTE_STRING_MODE,{begin:"'",end:"[^\\\\]'"},{begin:"`",end:"`"}]},{className:"number",variants:[{begin:a.C_NUMBER_RE+"[dflsi]",relevance:1},a.C_NUMBER_MODE]},{begin:/:=/},{className:"function",beginKeywords:"func",end:/\s*\{/,excludeEnd:!0,
-contains:[a.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,keywords:b,illegal:/["']/}]}]}});b.registerLanguage("haskell",function(a){var b={variants:[a.COMMENT("--","$"),a.COMMENT("{-","-}",{contains:["self"]})]},e={className:"meta",begin:"{-#",end:"#-}"},g={className:"meta",begin:"^#",end:"$"},d={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},f={begin:"\\(",end:"\\)",illegal:'"',contains:[e,g,{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},a.inherit(a.TITLE_MODE,{begin:"[_a-z][\\w']*"}),
-b]};return{aliases:["hs"],keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",contains:[{beginKeywords:"module",end:"where",keywords:"module where",contains:[f,b],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",keywords:"import qualified as hiding",contains:[f,b],illegal:"\\W\\.|;"},{className:"class",begin:"^(\\s*)?(class|instance)\\b",
-end:"where",keywords:"class family instance where",contains:[d,f,b]},{className:"class",begin:"\\b(data|(new)?type)\\b",end:"$",keywords:"data family type newtype deriving",contains:[e,d,f,{begin:"{",end:"}",contains:f.contains},b]},{beginKeywords:"default",end:"$",contains:[d,f,b]},{beginKeywords:"infix infixl infixr",end:"$",contains:[a.C_NUMBER_MODE,b]},{begin:"\\bforeign\\b",end:"$",keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",contains:[d,a.QUOTE_STRING_MODE,
-b]},{className:"meta",begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"},e,g,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,d,a.inherit(a.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),b,{begin:"->|<-"}]}});b.registerLanguage("java",function(a){var b=a.UNDERSCORE_IDENT_RE+"(<"+a.UNDERSCORE_IDENT_RE+"(\\s*,\\s*"+a.UNDERSCORE_IDENT_RE+")*>)?";return{aliases:["jsp"],keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports",
-illegal:/<\/|#/,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"("+
-b+"\\s+)+"+a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports",contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,
-relevance:0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports",relevance:0,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,
-a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0},{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("javascript",function(a){return{aliases:["js","jsx"],keywords:{keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",
+contains:[a.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,keywords:b,illegal:/["']/}]}]}});b.registerLanguage("haskell",function(a){var b={variants:[a.COMMENT("--","$"),a.COMMENT("{-","-}",{contains:["self"]})]},d={className:"meta",begin:"{-#",end:"#-}"},e={className:"meta",begin:"^#",end:"$"},c={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},g={begin:"\\(",end:"\\)",illegal:'"',contains:[d,e,{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},a.inherit(a.TITLE_MODE,{begin:"[_a-z][\\w']*"}),
+b]};return{aliases:["hs"],keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",contains:[{beginKeywords:"module",end:"where",keywords:"module where",contains:[g,b],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",keywords:"import qualified as hiding",contains:[g,b],illegal:"\\W\\.|;"},{className:"class",begin:"^(\\s*)?(class|instance)\\b",
+end:"where",keywords:"class family instance where",contains:[c,g,b]},{className:"class",begin:"\\b(data|(new)?type)\\b",end:"$",keywords:"data family type newtype deriving",contains:[d,c,g,{begin:"{",end:"}",contains:g.contains},b]},{beginKeywords:"default",end:"$",contains:[c,g,b]},{beginKeywords:"infix infixl infixr",end:"$",contains:[a.C_NUMBER_MODE,b]},{begin:"\\bforeign\\b",end:"$",keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",contains:[c,a.QUOTE_STRING_MODE,
+b]},{className:"meta",begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"},d,e,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,c,a.inherit(a.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),b,{begin:"->|<-"}]}});b.registerLanguage("java",function(a){return{aliases:["jsp"],keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",
+illegal:/<\/|#/,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"([\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*(<[\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*(\\s*,\\s*[\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*)*>)?\\s+)+"+
+a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,
+contains:[a.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",relevance:0,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,
+a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0},{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("javascript",function(a){var b={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",
 literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},
-contains:[{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},{className:"meta",begin:/^#!/,end:/$/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,{className:"subst",begin:"\\$\\{",end:"\\}"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",
-contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{begin:/</,end:/(\/\w+|\w+\/)>/,subLanguage:"xml",contains:[{begin:/<\w+\s*\/>/,skip:!0},{begin:/<\w+/,end:/(\/\w+|\w+\/)>/,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}],
-illegal:/\[|%/},{begin:/\$[(.]/},a.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0}],illegal:/#(?!!)/}});b.registerLanguage("json",function(a){var b={literal:"true false null"},e=[a.QUOTE_STRING_MODE,a.C_NUMBER_MODE],g={end:",",endsWithParent:!0,excludeEnd:!0,contains:e,keywords:b},d={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,
-end:/"/,contains:[a.BACKSLASH_ESCAPE],illegal:"\\n"},a.inherit(g,{begin:/:/})],illegal:"\\S"};a={begin:"\\[",end:"\\]",contains:[a.inherit(g)],illegal:"\\S"};e.splice(e.length,0,d,a);return{contains:e,keywords:b,illegal:"\\S"}});b.registerLanguage("lisp",function(a){var b={className:"literal",begin:"\\b(t{1}|nil)\\b"},e={className:"number",variants:[{begin:"(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",relevance:0},{begin:"#(b|B)[0-1]+(/[0-1]+)?"},{begin:"#(o|O)[0-7]+(/[0-7]+)?"},
-{begin:"#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?"},{begin:"#(c|C)\\((\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)? +(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",end:"\\)"}]},g=a.inherit(a.QUOTE_STRING_MODE,{illegal:null});a=a.COMMENT(";","$",{relevance:0});var d={begin:"\\*",end:"\\*"},f={className:"symbol",begin:"[:&][a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},k={begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*",
-relevance:0},l={contains:[e,g,d,f,{begin:"\\(",end:"\\)",contains:["self",b,g,e,k]},k],variants:[{begin:"['`]\\(",end:"\\)"},{begin:"\\(quote ",end:"\\)",keywords:{name:"quote"}},{begin:"'\\|[^]*?\\|"}]},n={variants:[{begin:"'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"#'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*(::[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*)*"}]},m={begin:"\\(\\s*",end:"\\)"},
-h={endsWithParent:!0,relevance:0};m.contains=[{className:"name",variants:[{begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"\\|[^]*?\\|"}]},h];h.contains=[l,n,m,b,e,g,a,d,f,{begin:"\\|[^]*?\\|"},k];return{illegal:/\S/,contains:[e,{className:"meta",begin:"^#!",end:"$"},b,g,a,l,n,m,k]}});b.registerLanguage("lua",function(a){var b={begin:"\\[=*\\[",end:"\\]=*\\]",contains:["self"]},e=[a.COMMENT("--(?!\\[=*\\[)","$"),a.COMMENT("--\\[=*\\[","\\]=*\\]",{contains:[b],
-relevance:10})];return{lexemes:a.UNDERSCORE_IDENT_RE,keywords:{keyword:"and break do else elseif end false for if in local nil not or repeat return then true until while",built_in:"_G _VERSION assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall coroutine debug io math os package string table"},contains:e.concat([{className:"function",beginKeywords:"function",
-end:"\\)",contains:[a.inherit(a.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:e}].concat(e)},a.C_NUMBER_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"\\[=*\\[",end:"\\]=*\\]",contains:[b],relevance:5}])}});b.registerLanguage("xml",function(a){var b={endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",begin:"[A-Za-z0-9\\._:-]+",relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",
-endsParent:!0,variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/},{begin:/[^\s"'=<>`]+/}]}]}]};return{aliases:"html xhtml rss atom xjb xsd xsl plist".split(" "),case_insensitive:!0,contains:[{className:"meta",begin:"<!DOCTYPE",end:">",relevance:10,contains:[{begin:"\\[",end:"\\]"}]},a.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},{begin:/<\?(php)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0}]},{className:"tag",begin:"<style(?=\\s|>|$)",
-end:">",keywords:{name:"style"},contains:[b],starts:{end:"</style>",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:"<script(?=\\s|>|$)",end:">",keywords:{name:"script"},contains:[b],starts:{end:"\x3c/script>",returnEnd:!0,subLanguage:["actionscript","javascript","handlebars","xml"]}},{className:"meta",variants:[{begin:/<\?xml/,end:/\?>/,relevance:10},{begin:/<\?\w+/,end:/\?>/}]},{className:"tag",begin:"</?",end:"/?>",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},b]}]}});
-b.registerLanguage("objectivec",function(a){var b=/[a-zA-Z@][a-zA-Z0-9_]*/;return{aliases:["mm","objc","obj-c"],keywords:{keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",
+d={className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},e={className:"subst",begin:"\\$\\{",end:"\\}",keywords:b,contains:[]},c={className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,e]};e.contains=[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,c,d,a.REGEXP_MODE];e=e.contains.concat([a.C_BLOCK_COMMENT_MODE,a.C_LINE_COMMENT_MODE]);return{aliases:["js","jsx"],keywords:b,contains:[{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},
+{className:"meta",begin:/^#!/,end:/$/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,c,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,d,{begin:/[{,]\s*/,relevance:0,contains:[{begin:"[A-Za-z$_][0-9A-Za-z$_]*\\s*:",returnBegin:!0,relevance:0,contains:[{className:"attr",begin:"[A-Za-z$_][0-9A-Za-z$_]*",relevance:0}]}]},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|[A-Za-z$_][0-9A-Za-z$_]*)\\s*=>",
+returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:"[A-Za-z$_][0-9A-Za-z$_]*"},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:e}]}]},{begin:/</,end:/(\/\w+|\w+\/)>/,subLanguage:"xml",contains:[{begin:/<\w+\s*\/>/,skip:!0},{begin:/<\w+/,end:/(\/\w+|\w+\/)>/,skip:!0,contains:[{begin:/<\w+\s*\/>/,skip:!0},"self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),
+{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:e}],illegal:/\[|%/},{begin:/\$[(.]/},a.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0}],illegal:/#(?!!)/}});b.registerLanguage("json",function(a){var b={literal:"true false null"},d=[a.QUOTE_STRING_MODE,a.C_NUMBER_MODE],e={end:",",endsWithParent:!0,excludeEnd:!0,
+contains:d,keywords:b},c={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE],illegal:"\\n"},a.inherit(e,{begin:/:/})],illegal:"\\S"};a={begin:"\\[",end:"\\]",contains:[a.inherit(e)],illegal:"\\S"};d.splice(d.length,0,c,a);return{contains:d,keywords:b,illegal:"\\S"}});b.registerLanguage("kotlin",function(a){var b={keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit initinterface annotation data sealed internal infix operator out by constructor super trait volatile transient native default",
+built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",literal:"true false null"},d={className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"@"},e={className:"subst",begin:"\\${",end:"}",contains:[a.APOS_STRING_MODE,a.C_NUMBER_MODE]},c={className:"variable",begin:"\\$"+a.UNDERSCORE_IDENT_RE},e={className:"string",variants:[{begin:'"""',end:'"""',contains:[c,e]},{begin:"'",end:"'",illegal:/\n/,contains:[a.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,contains:[a.BACKSLASH_ESCAPE,c,
+e]}]},c={className:"meta",begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+a.UNDERSCORE_IDENT_RE+")?"},g={className:"meta",begin:"@"+a.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,end:/\)/,contains:[a.inherit(e,{className:"meta-string"})]}]};return{keywords:b,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"keyword",begin:/\b(break|continue|return|this)\b/,
+starts:{contains:[{className:"symbol",begin:/@\w+/}]}},d,c,g,{className:"function",beginKeywords:"fun",end:"[(]|$",returnBegin:!0,excludeEnd:!0,keywords:b,illegal:/fun\s+(<.*>)?[^\s\(]+(\s+[^\s\(]+)\s*=/,relevance:5,contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"type",begin:/</,end:/>/,keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,
+endsWithParent:!0,contains:[{className:"type",begin:a.UNDERSCORE_IDENT_RE},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],relevance:0},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,c,g,e,a.C_NUMBER_MODE]},a.C_BLOCK_COMMENT_MODE]},{className:"class",beginKeywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,illegal:"extends implements",contains:[{beginKeywords:"public protected internal private constructor"},a.UNDERSCORE_TITLE_MODE,{className:"type",begin:/</,end:/>/,excludeBegin:!0,excludeEnd:!0,
+relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,]|$/,excludeBegin:!0,returnEnd:!0},c,g]},e,{className:"meta",begin:"^#!/usr/bin/env",end:"$",illegal:"\n"},a.C_NUMBER_MODE]}});b.registerLanguage("lisp",function(a){var b={className:"literal",begin:"\\b(t{1}|nil)\\b"},d={className:"number",variants:[{begin:"(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",relevance:0},{begin:"#(b|B)[0-1]+(/[0-1]+)?"},{begin:"#(o|O)[0-7]+(/[0-7]+)?"},{begin:"#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?"},
+{begin:"#(c|C)\\((\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)? +(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",end:"\\)"}]},e=a.inherit(a.QUOTE_STRING_MODE,{illegal:null});a=a.COMMENT(";","$",{relevance:0});var c={begin:"\\*",end:"\\*"},g={className:"symbol",begin:"[:&][a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},k={begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*",relevance:0},
+l={contains:[d,e,c,g,{begin:"\\(",end:"\\)",contains:["self",b,e,d,k]},k],variants:[{begin:"['`]\\(",end:"\\)"},{begin:"\\(quote ",end:"\\)",keywords:{name:"quote"}},{begin:"'\\|[^]*?\\|"}]},m={variants:[{begin:"'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"#'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*(::[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*)*"}]},n={begin:"\\(\\s*",end:"\\)"},h={endsWithParent:!0,
+relevance:0};n.contains=[{className:"name",variants:[{begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"\\|[^]*?\\|"}]},h];h.contains=[l,m,n,b,d,e,a,c,g,{begin:"\\|[^]*?\\|"},k];return{illegal:/\S/,contains:[d,{className:"meta",begin:"^#!",end:"$"},b,e,a,l,m,n,k]}});b.registerLanguage("lua",function(a){var b={begin:"\\[=*\\[",end:"\\]=*\\]",contains:["self"]},d=[a.COMMENT("--(?!\\[=*\\[)","$"),a.COMMENT("--\\[=*\\[","\\]=*\\]",{contains:[b],relevance:10})];
+return{lexemes:a.UNDERSCORE_IDENT_RE,keywords:{literal:"true false nil",keyword:"and break do else elseif end for goto if in local not or repeat return then until while",built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstringmodule next pairs pcall print rawequal rawget rawset require select setfenvsetmetatable tonumber tostring type unpack xpcall arg selfcoroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"},
+contains:d.concat([{className:"function",beginKeywords:"function",end:"\\)",contains:[a.inherit(a.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:d}].concat(d)},a.C_NUMBER_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"\\[=*\\[",end:"\\]=*\\]",contains:[b],relevance:5}])}});b.registerLanguage("xml",function(a){var b={endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",begin:"[A-Za-z0-9\\._:-]+",
+relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/},{begin:/[^\s"'=<>`]+/}]}]}]};return{aliases:"html xhtml rss atom xjb xsd xsl plist".split(" "),case_insensitive:!0,contains:[{className:"meta",begin:"<!DOCTYPE",end:">",relevance:10,contains:[{begin:"\\[",end:"\\]"}]},a.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},{begin:/<\?(php)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",
+end:"\\*/",skip:!0}]},{className:"tag",begin:"<style(?=\\s|>|$)",end:">",keywords:{name:"style"},contains:[b],starts:{end:"</style>",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:"<script(?=\\s|>|$)",end:">",keywords:{name:"script"},contains:[b],starts:{end:"\x3c/script>",returnEnd:!0,subLanguage:["actionscript","javascript","handlebars","xml"]}},{className:"meta",variants:[{begin:/<\?xml/,end:/\?>/,relevance:10},{begin:/<\?\w+/,end:/\?>/}]},{className:"tag",begin:"</?",end:"/?>",
+contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},b]}]}});b.registerLanguage("objectivec",function(a){var b=/[a-zA-Z@][a-zA-Z0-9_]*/;return{aliases:["mm","objc","obj-c"],keywords:{keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",
 literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},lexemes:b,illegal:"</",contains:[{className:"built_in",begin:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.C_NUMBER_MODE,a.QUOTE_STRING_MODE,{className:"string",variants:[{begin:'@"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:"'",end:"[^\\\\]'",illegal:"[^\\\\][^']"}]},
 {className:"meta",begin:"#",end:"$",contains:[{className:"meta-string",variants:[{begin:'"',end:'"'},{begin:"<",end:">"}]}]},{className:"class",begin:"(@interface|@class|@protocol|@implementation)\\b",end:"({|$)",excludeEnd:!0,keywords:"@interface @class @protocol @implementation",lexemes:b,contains:[a.UNDERSCORE_TITLE_MODE]},{begin:"\\."+a.UNDERSCORE_IDENT_RE,relevance:0}]}});b.registerLanguage("ocaml",function(a){return{aliases:["ml"],keywords:{keyword:"and as assert asr begin class constraint do done downto else end exception external for fun function functor if in include inherit! inherit initializer land lazy let lor lsl lsr lxor match method!|10 method mod module mutable new object of open! open or private rec sig struct then to try type val! val virtual when while with parser value",
 built_in:"array bool bytes char exn|5 float int int32 int64 list lazy_t|5 nativeint|5 string unit in_channel out_channel ref",literal:"true false"},illegal:/\/\/|>>/,lexemes:"[a-z_]\\w*!?",contains:[{className:"literal",begin:"\\[(\\|\\|)?\\]|\\(\\)",relevance:0},a.COMMENT("\\(\\*","\\*\\)",{contains:["self"]}),{className:"symbol",begin:"'[A-Za-z_](?!')[\\w']*"},{className:"type",begin:"`[A-Z][\\w']*"},{className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},{begin:"[a-z_]\\w*'[\\w']*",relevance:0},
 a.inherit(a.APOS_STRING_MODE,{className:"string",relevance:0}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),{className:"number",begin:"\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)",relevance:0},{begin:/[-=]>/}]}});b.registerLanguage("perl",function(a){var b={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when"},
-e={begin:"->{",end:"}"},g={variants:[{begin:/\$\d/},{begin:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{begin:/[\$%@][^\s\w{]/,relevance:0}]},d=[a.BACKSLASH_ESCAPE,b,g];a=[g,a.HASH_COMMENT_MODE,a.COMMENT("^\\=\\w","\\=cut",{endsWithParent:!0}),e,{className:"string",contains:d,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*\\<",
+d={begin:"->{",end:"}"},e={variants:[{begin:/\$\d/},{begin:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{begin:/[\$%@][^\s\w{]/,relevance:0}]},c=[a.BACKSLASH_ESCAPE,b,e];a=[e,a.HASH_COMMENT_MODE,a.COMMENT("^\\=\\w","\\=cut",{endsWithParent:!0}),d,{className:"string",contains:c,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*\\<",
 end:"\\>",relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE]},{begin:"{\\w+}",contains:[],relevance:0},{begin:"-?\\w+\\s*\\=\\>",contains:[],relevance:0}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\/\\/|"+a.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",keywords:"split return print reverse grep",
 relevance:0,contains:[a.HASH_COMMENT_MODE,{className:"regexp",begin:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",relevance:10},{className:"regexp",begin:"(m|qr)?/",end:"/[a-z]*",contains:[a.BACKSLASH_ESCAPE],relevance:0}]},{className:"function",beginKeywords:"sub",end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[a.TITLE_MODE]},{begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}]}];b.contains=
-a;e.contains=a;return{aliases:["pl","pm"],lexemes:/[\w\.]+/,keywords:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",
-contains:a}});b.registerLanguage("php",function(a){var b={begin:"\\$+[a-zA-Z_\u007f-\u00ff][a-zA-Z0-9_\u007f-\u00ff]*"},e={className:"meta",begin:/<\?(php)?|\?>/},g={className:"string",contains:[a.BACKSLASH_ESCAPE,e],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null})]},d={variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]};return{aliases:["php3","php4","php5","php6"],case_insensitive:!0,keywords:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",
-contains:[a.HASH_COMMENT_MODE,a.COMMENT("//","$",{contains:[e]}),a.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),a.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,keywords:"__halt_compiler",lexemes:a.UNDERSCORE_IDENT_RE}),{className:"string",begin:/<<<['"]?\w+['"]?$/,end:/^\w+;?$/,contains:[a.BACKSLASH_ESCAPE,{className:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,end:/\}/}]}]},e,{className:"keyword",begin:/\$this\b/},b,{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},
-{className:"function",beginKeywords:"function",end:/[;{]/,excludeEnd:!0,illegal:"\\$|\\[|%",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",contains:["self",b,a.C_BLOCK_COMMENT_MODE,g,d]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",end:";",illegal:/[\.']/,contains:[a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",end:";",contains:[a.UNDERSCORE_TITLE_MODE]},
-{begin:"=>"},g,d]}});b.registerLanguage("protobuf",function(a){return{keywords:{keyword:"package import option optional required repeated group",built_in:"double float int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 bool string bytes",literal:"true false"},contains:[a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.C_LINE_COMMENT_MODE,{className:"class",beginKeywords:"message enum service",end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,excludeEnd:!0}})]},
-{className:"function",beginKeywords:"rpc",end:/;/,excludeEnd:!0,keywords:"rpc returns"},{begin:/^\s*[A-Z_]+/,end:/\s*=/,excludeEnd:!0}]}});b.registerLanguage("python",function(a){var b={className:"meta",begin:/^(>>>|\.\.\.) /},e={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[b],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[b],relevance:10},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,
-end:/'/},{begin:/(b|br)"/,end:/"/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},g={className:"number",relevance:0,variants:[{begin:a.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:a.C_NUMBER_RE+"[lLjJ]?"}]};return{aliases:["py","gyp"],keywords:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},
-illegal:/(<\/|->|\?)/,contains:[b,g,e,a.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def",relevance:10},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,contains:["self",b,g,e]},{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}});b.registerLanguage("ruby",function(a){var b={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",
-literal:"true false nil"},e={className:"doctag",begin:"@[A-Za-z]+"},g={begin:"#<",end:">"},e=[a.COMMENT("#","$",{contains:[e]}),a.COMMENT("^\\=begin","^\\=end",{contains:[e],relevance:10}),a.COMMENT("^__END__","\\n$")],d={className:"subst",begin:"#\\{",end:"}",keywords:b},f={className:"string",contains:[a.BACKSLASH_ESCAPE,d],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<",
-end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]},k={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:b};a=[f,g,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+a.IDENT_RE+"::)?"+a.IDENT_RE}]}].concat(e)},
-{className:"function",beginKeywords:"def",end:"$|;",contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}),k].concat(e)},{begin:a.IDENT_RE+"::"},{className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[f,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}],relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",
-relevance:0},{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:b},{begin:"("+a.RE_STARTERS_RE+")\\s*",contains:[g,{className:"regexp",contains:[a.BACKSLASH_ESCAPE,d],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(e),relevance:0}].concat(e);d.contains=a;k.contains=a;return{aliases:["rb","gemspec","podspec","thor","irb"],keywords:b,
-illegal:/\/\*/,contains:e.concat([{begin:/^\s*=>/,starts:{end:"$",contains:a}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:a}}]).concat(a)}});b.registerLanguage("rust",function(a){var b=a.inherit(a.C_BLOCK_COMMENT_MODE);b.contains.push("self");return{aliases:["rs"],keywords:{keyword:"alignof as be box break const continue crate do else enum extern false fn for if impl in let loop match mod mut offsetof once priv proc pub pure ref return self Self sizeof static struct super trait true type typeof unsafe unsized use virtual while where yield move default int i8 i16 i32 i64 isize uint u8 u32 u64 usize float f32 f64 str char bool",
-literal:"true false Some None Ok Err",built_in:"Copy Send Sized Sync Drop Fn FnMut FnOnce drop Box ToOwned Clone PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator Option Result SliceConcatExt String ToString Vec assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules!"},
-lexemes:a.IDENT_RE+"!?",illegal:"</",contains:[a.C_LINE_COMMENT_MODE,b,a.inherit(a.QUOTE_STRING_MODE,{begin:/b?"/,illegal:null}),{className:"string",variants:[{begin:/r(#*)".*?"\1(?!#)/},{begin:/b?'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/}]},{className:"symbol",begin:/'[a-zA-Z_][a-zA-Z0-9_]*/},{className:"number",variants:[{begin:"\\b0b([01_]+)([uif](8|16|32|64|size))?"},{begin:"\\b0o([0-7_]+)([uif](8|16|32|64|size))?"},{begin:"\\b0x([A-Fa-f0-9_]+)([uif](8|16|32|64|size))?"},{begin:"\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)([uif](8|16|32|64|size))?"}],
-relevance:0},{className:"function",beginKeywords:"fn",end:"(\\(|<)",excludeEnd:!0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"meta",begin:"#\\!?\\[",end:"\\]",contains:[{className:"meta-string",begin:/"/,end:/"/}]},{className:"class",beginKeywords:"type",end:";",contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"\\S"},{className:"class",beginKeywords:"trait enum struct",end:"{",contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"[\\w\\d]"},{begin:a.IDENT_RE+
-"::",keywords:{built_in:"Copy Send Sized Sync Drop Fn FnMut FnOnce drop Box ToOwned Clone PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator Option Result SliceConcatExt String ToString Vec assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules!"}},
-{begin:"->"}]}});b.registerLanguage("scala",function(a){var b={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},e={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},g={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0};return{keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},
-contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[b],relevance:10}]},{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},e,{className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[g]},{className:"class",beginKeywords:"class object trait type",
-end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[e]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[e]},g]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("sql",function(a){var b=a.COMMENT("--","$");return{case_insensitive:!0,illegal:/[<>{}*#]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke",
+a;d.contains=a;return{aliases:["pl","pm"],lexemes:/[\w\.]+/,keywords:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",
+contains:a}});b.registerLanguage("protobuf",function(a){return{keywords:{keyword:"package import option optional required repeated group",built_in:"double float int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 bool string bytes",literal:"true false"},contains:[a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.C_LINE_COMMENT_MODE,{className:"class",beginKeywords:"message enum service",end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,excludeEnd:!0}})]},{className:"function",
+beginKeywords:"rpc",end:/;/,excludeEnd:!0,keywords:"rpc returns"},{begin:/^\s*[A-Z_]+/,end:/\s*=/,excludeEnd:!0}]}});b.registerLanguage("python",function(a){var b={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},d={className:"meta",begin:/^(>>>|\.\.\.) /},e={className:"subst",begin:/\{/,end:/\}/,keywords:b,
+illegal:/#/},c={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[d],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[d],relevance:10},{begin:/(fr|rf|f)'''/,end:/'''/,contains:[d,e]},{begin:/(fr|rf|f)"""/,end:/"""/,contains:[d,e]},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},{begin:/(fr|rf|f)'/,end:/'/,contains:[e]},{begin:/(fr|rf|f)"/,end:/"/,contains:[e]},
+a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},g={className:"number",relevance:0,variants:[{begin:a.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:a.C_NUMBER_RE+"[lLjJ]?"}]},k={className:"params",begin:/\(/,end:/\)/,contains:["self",d,g,c]};e.contains=[c,g,d];return{aliases:["py","gyp"],keywords:b,illegal:/(<\/|->|\?)|=>/,contains:[d,g,c,a.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[a.UNDERSCORE_TITLE_MODE,
+k,{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}});b.registerLanguage("ruby",function(a){var b={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},d={className:"doctag",begin:"@[A-Za-z]+"},e={begin:"#<",end:">"},
+d=[a.COMMENT("#","$",{contains:[d]}),a.COMMENT("^\\=begin","^\\=end",{contains:[d],relevance:10}),a.COMMENT("^__END__","\\n$")],c={className:"subst",begin:"#\\{",end:"}",keywords:b},g={className:"string",contains:[a.BACKSLASH_ESCAPE,c],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<",end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",
+end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{begin:/<<(-?)\w+$/,end:/^\s*\w+$/}]},k={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:b};a=[g,e,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+a.IDENT_RE+"::)?"+a.IDENT_RE}]}].concat(d)},{className:"function",beginKeywords:"def",end:"$|;",
+contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}),k].concat(d)},{begin:a.IDENT_RE+"::"},{className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[g,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}],relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},
+{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:b},{begin:"("+a.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[e,{className:"regexp",contains:[a.BACKSLASH_ESCAPE,c],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(d),relevance:0}].concat(d);c.contains=a;k.contains=a;return{aliases:["rb","gemspec","podspec","thor","irb"],
+keywords:b,illegal:/\/\*/,contains:d.concat([{begin:/^\s*=>/,starts:{end:"$",contains:a}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:a}}]).concat(a)}});b.registerLanguage("rust",function(a){return{aliases:["rs"],keywords:{keyword:"alignof as be box break const continue crate do else enum extern false fn for if impl in let loop match mod mut offsetof once priv proc pub pure ref return self Self sizeof static struct super trait true type typeof unsafe unsized use virtual while where yield move default",
+literal:"true false Some None Ok Err",built_in:"drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!"},
+lexemes:a.IDENT_RE+"!?",illegal:"</",contains:[a.C_LINE_COMMENT_MODE,a.COMMENT("/\\*","\\*/",{contains:["self"]}),a.inherit(a.QUOTE_STRING_MODE,{begin:/b?"/,illegal:null}),{className:"string",variants:[{begin:/r(#*)"(.|\n)*?"\1(?!#)/},{begin:/b?'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/}]},{className:"symbol",begin:/'[a-zA-Z_][a-zA-Z0-9_]*/},{className:"number",variants:[{begin:"\\b0b([01_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},{begin:"\\b0o([0-7_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},{begin:"\\b0x([A-Fa-f0-9_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},
+{begin:"\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)([ui](8|16|32|64|128|size)|f(32|64))?"}],relevance:0},{className:"function",beginKeywords:"fn",end:"(\\(|<)",excludeEnd:!0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"meta",begin:"#\\!?\\[",end:"\\]",contains:[{className:"meta-string",begin:/"/,end:/"/}]},{className:"class",beginKeywords:"type",end:";",contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"\\S"},{className:"class",beginKeywords:"trait enum struct union",end:"{",
+contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"[\\w\\d]"},{begin:a.IDENT_RE+"::",keywords:{built_in:"drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!"}},
+{begin:"->"}]}});b.registerLanguage("scala",function(a){var b={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},d={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},e={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0};return{keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},
+contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[b],relevance:10}]},{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},d,{className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[e]},{className:"class",beginKeywords:"class object trait type",
+end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[d]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[d]},e]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("sql",function(a){var b=a.COMMENT("--","$");return{case_insensitive:!0,illegal:/[<>{}*#]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment",
 end:/;/,endsWithParent:!0,lexemes:/[\w\.]+/,keywords:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",
 literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void"},contains:[{className:"string",begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE,{begin:"''"}]},{className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE,{begin:'""'}]},{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE]},a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE,b]},
-a.C_BLOCK_COMMENT_MODE,b]}});b.registerLanguage("swift",function(a){var b={keyword:"__COLUMN__ __FILE__ __FUNCTION__ __LINE__ as as! as? associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",
+a.C_BLOCK_COMMENT_MODE,b]}});b.registerLanguage("swift",function(a){var b={keyword:"__COLUMN__ __FILE__ __FUNCTION__ __LINE__ as as! as? associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",
 literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},
-e=a.COMMENT("/\\*","\\*/",{contains:["self"]}),g={className:"subst",begin:/\\\(/,end:"\\)",keywords:b,contains:[]},d={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0},f=a.inherit(a.QUOTE_STRING_MODE,{contains:[g,a.BACKSLASH_ESCAPE]});g.contains=[d];return{keywords:b,contains:[f,a.C_LINE_COMMENT_MODE,e,{className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},d,{className:"function",beginKeywords:"func",end:"{",excludeEnd:!0,
-contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin:/</,end:/>/},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,contains:["self",d,f,a.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:b,end:"\\{",excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/})]},{className:"meta",begin:"(@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain)"},
-{beginKeywords:"import",end:/$/,contains:[a.C_LINE_COMMENT_MODE,e]}]}});b.registerLanguage("typescript",function(a){var b={keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void"};
+d=a.COMMENT("/\\*","\\*/",{contains:["self"]}),e={className:"subst",begin:/\\\(/,end:"\\)",keywords:b,contains:[]},c={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0},g=a.inherit(a.QUOTE_STRING_MODE,{contains:[e,a.BACKSLASH_ESCAPE]});e.contains=[c];return{keywords:b,contains:[g,a.C_LINE_COMMENT_MODE,d,{className:"type",begin:"\\b[A-Z][\\w\u00c0-\u02b8']*",relevance:0},c,{className:"function",beginKeywords:"func",end:"{",
+excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin:/</,end:/>/},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,contains:["self",c,g,a.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:b,end:"\\{",excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{className:"meta",begin:"(@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain)"},
+{beginKeywords:"import",end:/$/,contains:[a.C_LINE_COMMENT_MODE,d]}]}});b.registerLanguage("typescript",function(a){var b={keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract as from extends async await",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void Promise"};
 return{aliases:["ts"],keywords:b,contains:[{className:"meta",begin:/^\s*['"]use strict['"]/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,{className:"subst",begin:"\\$\\{",end:"\\}"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",
-contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE],relevance:0},{className:"function",begin:"function",end:/[\{;]/,excludeEnd:!0,keywords:b,contains:["self",a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],illegal:/["'\(]/}],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0},{begin:/module\./,keywords:{built_in:"module"},
-relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0},{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+a.IDENT_RE,relevance:0}]}});b.registerLanguage("yaml",function(a){var b={className:"attr",variants:[{begin:"^[ \\-]*[a-zA-Z_][\\w\\-]*:"},{begin:'^[ \\-]*"[a-zA-Z_][\\w\\-]*":'},{begin:"^[ \\-]*'[a-zA-Z_][\\w\\-]*':"}]},e={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/}],contains:[a.BACKSLASH_ESCAPE,
-{className:"template-variable",variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]};return{case_insensitive:!0,aliases:["yml","YAML","yaml"],contains:[b,{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>] *$",returnEnd:!0,contains:e.contains,end:b.variants[0].begin},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!!"+a.UNDERSCORE_IDENT_RE},{className:"meta",begin:"&"+a.UNDERSCORE_IDENT_RE+"$"},
-{className:"meta",begin:"\\*"+a.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"^ *-",relevance:0},e,a.HASH_COMMENT_MODE,a.C_NUMBER_MODE],keywords:{literal:"{ } true false yes no Yes No True False null"}}});return b});
+contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|"+a.IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:a.IDENT_RE},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:["self",a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}]}]}],relevance:0},{className:"function",begin:"function",end:/[\{;]/,excludeEnd:!0,keywords:b,contains:["self",a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),
+{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],illegal:/["'\(]/}],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0,contains:["self",{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],illegal:/["'\(]/}]},{begin:/module\./,keywords:{built_in:"module"},relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0},
+{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+a.IDENT_RE,relevance:0},{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("yaml",function(a){var b={className:"attr",variants:[{begin:"^[ \\-]*[a-zA-Z_][\\w\\-]*:"},{begin:'^[ \\-]*"[a-zA-Z_][\\w\\-]*":'},{begin:"^[ \\-]*'[a-zA-Z_][\\w\\-]*':"}]},d={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[a.BACKSLASH_ESCAPE,{className:"template-variable",
+variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]};return{case_insensitive:!0,aliases:["yml","YAML","yaml"],contains:[b,{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>] *$",returnEnd:!0,contains:d.contains,end:b.variants[0].begin},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!!"+a.UNDERSCORE_IDENT_RE},{className:"meta",begin:"&"+a.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+
+a.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"^ *-",relevance:0},a.HASH_COMMENT_MODE,{beginKeywords:"true false yes no null",keywords:{literal:"true false yes no null"}},a.C_NUMBER_MODE,d]}});return b});
diff --git a/plugins/replication b/plugins/replication
index 2a4d0bf..1085b45 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 2a4d0bfe10c63c79ca0d47be21756377703e46c0
+Subproject commit 1085b453039b0fe230c3584e0024a7965cc1e323
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 1a64a6f..f0930e9 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 1a64a6f6407df31c06350a5c6e9266e9ba0cf72a
+Subproject commit f0930e9dfc56644105cb21e7246096097eeaa385
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 1f11cde..fd75f5f 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -28,7 +28,7 @@
 genrule2(
     name = "fonts",
     srcs = [
-        "//lib/fonts:sourcecodepro",
+        "//lib/fonts:robotomono",
     ],
     outs = ["fonts.zip"],
     cmd = " && ".join([
diff --git a/polygerrit-ui/app/.eslintrc.json b/polygerrit-ui/app/.eslintrc.json
index 14c419c..2f77b42 100644
--- a/polygerrit-ui/app/.eslintrc.json
+++ b/polygerrit-ui/app/.eslintrc.json
@@ -1,6 +1,5 @@
 {
   "extends": ["eslint:recommended", "google"],
-  "installedESLint": true,
   "env": {
     "browser": true,
     "es6": true
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 42d3d83..f334354 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -81,7 +81,7 @@
 genrule2(
     name = "polygerrit_ui",
     srcs = [
-        "//lib/fonts:sourcecodepro",
+        "//lib/fonts:robotomono",
         "//lib/js:highlightjs_files",
         ":top_sources",
         ":css_sources",
@@ -94,7 +94,7 @@
     cmd = " && ".join([
         "mkdir -p $$TMP/polygerrit_ui/{styles,fonts,bower_components/{highlightjs,webcomponentsjs},elements}",
         "for f in $(locations :app_sources); do ext=$${f##*.}; cp -p $$f $$TMP/polygerrit_ui/elements/gr-app.$$ext; done",
-        "cp $(locations //lib/fonts:sourcecodepro) $$TMP/polygerrit_ui/fonts/",
+        "cp $(locations //lib/fonts:robotomono) $$TMP/polygerrit_ui/fonts/",
         "for f in $(locations :top_sources); do cp $$f $$TMP/polygerrit_ui/; done",
         "for f in $(locations :css_sources); do cp $$f $$TMP/polygerrit_ui/styles; done",
         "for f in $(locations //lib/js:highlightjs_files); do cp $$f $$TMP/polygerrit_ui/bower_components/highlightjs/ ; done",
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project.html b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project.html
new file mode 100644
index 0000000..9fc7523
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project.html
@@ -0,0 +1,106 @@
+<!--
+Copyright (C) 2017 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.
+-->
+
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
+
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../shared/gr-select/gr-select.html">
+<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-admin-create-project">
+  <template>
+    <style include="shared-styles">
+      :host {
+        display: inline-block;
+      }
+      main {
+        margin: 2em 1em;
+      }
+      gr-autocomplete {
+        display: flex
+        border-radius: 2px 0 0 2px;
+        outline: none;
+        overflow: hidden;
+      }
+    </style>
+    <style include="gr-form-styles"></style>
+      <main class="gr-form-styles">
+        <h1 id="Title">
+          Create Project
+        </h1>
+        <a id="redirect" href$="[[_redirect(_projectConfig.name)]]"
+           hidden$="[[!_projectCreated]]" hidden></a>
+        <br>
+        <div id="form">
+          <fieldset>
+            <section>
+              <span class="title">Project name</span>
+              <iron-autogrow-textarea
+                  id="projectNameInput"
+                  autocomplete="on"
+                  bind-value="{{_projectConfig.name}}">
+              </iron-autogrow-textarea>
+            </section>
+            <section>
+              <span class="title">Rights inherit from</span>
+              <gr-autocomplete
+                  id="rightsInheritFromInput"
+                  text="{{_projectConfig.parent}}"
+                  query="[[_query]]"
+                  placeholder="Optional, defaults to 'All-Projects'">
+              </gr-autocomplete>
+            </section>
+            <section>
+              <span class="title">Create initial empty commit</span>
+              <span class="value">
+                <select
+                    id="initalCommit"
+                    is="gr-select"
+                    bind-value="{{_projectConfig.create_empty_commit}}">
+                  <option value="false">False</option>
+                  <option value="true">True</option>
+                </select>
+              </span>
+            </section>
+            <section>
+              <span class="title">Only serve as parent for other projects</span>
+              <span class="value">
+                <select
+                    id="parentProject"
+                    is="gr-select"
+                    bind-value="{{_projectConfig.permissions_only}}">
+                  <option value="false">False</option>
+                  <option value="true">True</option>
+                </select>
+              </span>
+            </section>
+          </fieldset>
+          <gr-button
+              id="submitBtn"
+              on-tap="_handleCreateProject"
+              disabled$="[[!_projectConfig.name]]">Save changes</gr-button>
+        </div>
+      </main>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-admin-create-project.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project.js b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project.js
new file mode 100644
index 0000000..2376e0c
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project.js
@@ -0,0 +1,95 @@
+// Copyright (C) 2017 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.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-admin-create-project',
+
+    properties: {
+      params: Object,
+
+      _projectConfig: Object,
+      _projectCreated: {
+        type: Object,
+        value: false,
+      },
+
+      _query: {
+        type: Function,
+        value() {
+          return this._getProjectSuggestions.bind(this);
+        },
+      },
+    },
+
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+      Gerrit.URLEncodingBehavior,
+    ],
+
+    attached() {
+      this._createProject();
+    },
+
+    _createProject() {
+      this._projectConfig = [];
+    },
+
+    _formatProjectConfigForSave(p) {
+      const configInputObj = {};
+      for (const key in p) {
+        if (p.hasOwnProperty(key)) {
+          if (typeof p[key] === 'object') {
+            configInputObj[key] = p[key].configured_value;
+          } else {
+            configInputObj[key] = p[key];
+          }
+        }
+      }
+      return configInputObj;
+    },
+
+    _redirect(projectName) {
+      return this.getBaseUrl() + '/admin/projects/' +
+          this.encodeURL(projectName, true);
+    },
+
+    _handleCreateProject() {
+      const config = this._formatProjectConfigForSave(this._projectConfig);
+      return this.$.restAPI.createProject(config)
+          .then(projectRegistered => {
+            if (projectRegistered.status === 201) {
+              this._projectCreated = true;
+              this.$.redirect.click();
+            }
+          });
+    },
+
+    _getProjectSuggestions(input) {
+      return this.$.restAPI.getSuggestedProjects(input)
+          .then(response => {
+            const projects = [];
+            for (const key in response) {
+              if (!response.hasOwnProperty(key)) { continue; }
+              projects.push({
+                name: key,
+                value: response[key],
+              });
+            }
+            return projects;
+          });
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project_test.html b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project_test.html
new file mode 100644
index 0000000..2425c6a
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project_test.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-admin-create-project</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-admin-create-project.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-admin-create-project></gr-admin-create-project>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-admin-create-project tests', () => {
+    let element;
+    let sandbox;
+    const PROJECT = 'test-project';
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      stub('gr-rest-api-interface', {
+        getLoggedIn() { return Promise.resolve(true); },
+      });
+      element = fixture('basic');
+      element._projectConfig = {
+        name: 'test-project',
+        create_empty_commit: true,
+        parent: 'All-Project',
+        permissions_only: false,
+      };
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('project created', done => {
+      const configInputObj = {
+        name: 'test-project',
+        create_empty_commit: true,
+        parent: 'All-Project',
+        permissions_only: false,
+      };
+
+      const saveStub = sandbox.stub(element.$.restAPI,
+          'createProject', () => {
+            return Promise.resolve({});
+          });
+
+      const button = element.$.submitBtn;
+
+      flushAsynchronousOperations();
+      element.$.projectNameInput.bindValue = configInputObj.name;
+      element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
+      element.$.initalCommit.bindValue =
+          configInputObj.create_empty_commit;
+      element.$.parentProject.bindValue =
+          configInputObj.permissions_only;
+
+      assert.isFalse(button.hasAttribute('disabled'));
+
+      assert.deepEqual(element._projectConfig, configInputObj);
+
+      element._handleCreateProject().then(() => {
+        assert.isTrue(button.hasAttribute('disabled'));
+        assert.isTrue(saveStub.lastCall.calledWithExactly(PROJECT,
+            configInputObj));
+        done();
+      });
+      MockInteractions.tap(button);
+      done();
+    });
+
+    test('test for button being called', done => {
+      const button = element.$.submitBtn;
+
+      flushAsynchronousOperations();
+      sandbox.stub(element, '_handleCreateProject');
+      element._handleCreateProject();
+      MockInteractions.tap(button);
+      assert.isTrue(element._handleCreateProject.called);
+      done();
+    });
+
+    test('test for _handleCreateProject being called', done => {
+      const button = element.$.submitBtn;
+
+      flushAsynchronousOperations();
+      MockInteractions.tap(button);
+      done();
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.js b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.js
index 9672803..3422f17 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.js
@@ -29,6 +29,10 @@
       value: 'FAST_FORWARD_ONLY',
       label: 'Fast forward only',
     },
+    rebaseAlways: {
+      value: 'REBASE_ALWAYS',
+      label: 'Rebase Always',
+    },
     rebaseIfNecessary: {
       value: 'REBASE_IF_NECESSARY',
       label: 'Rebase if necessary',
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 4b0017c..cbf75cb 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -19,6 +19,8 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
+
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index e901eab..c5660ac 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -842,9 +842,8 @@
                     'uploaded to this change.',
                 action: 'Reload',
                 callback: () => {
-                    // Load the current change without any patch range.
-                  location.href = `${this.getBaseUrl()}/c/${
-                      this.change._number}`;
+                  // Load the current change without any patch range.
+                  Gerrit.Nav.navigateToChange(this.change);
                 },
               });
               cleanupFn();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 83b81ca..ed18d6a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -14,11 +14,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../plugins/gr-external-style/gr-external-style.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
@@ -205,7 +204,7 @@
           </template>
           <template is="dom-if" if="[[!change.topic]]">
             <gr-editable-label
-                value="{{change.topic}}"
+                value="[[change.topic]]"
                 placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]"
                 read-only="[[_topicReadOnly]]"
                 on-changed="_handleTopicChanged"></gr-editable-label>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 62fe5ed..b1784e7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -25,6 +25,12 @@
   Polymer({
     is: 'gr-change-metadata',
 
+    /**
+     * Fired when the change topic is changed.
+     *
+     * @event topic-changed
+     */
+
     properties: {
       change: Object,
       commitInfo: Object,
@@ -51,9 +57,7 @@
     },
 
     behaviors: [
-      Gerrit.BaseUrlBehavior,
       Gerrit.RESTClientBehavior,
-      Gerrit.URLEncodingBehavior,
     ],
 
     observers: [
@@ -149,8 +153,16 @@
     },
 
     _handleTopicChanged(e, topic) {
+      const lastTopic = this.change.topic;
       if (!topic.length) { topic = null; }
-      this.$.restAPI.setChangeTopic(this.change._number, topic);
+      this.$.restAPI.setChangeTopic(this.change._number, topic)
+          .then(newTopic => {
+            this.set(['change', 'topic'], newTopic);
+            if (newTopic !== lastTopic) {
+              this.dispatchEvent(
+                  new CustomEvent('topic-changed', {bubbles: true}));
+            }
+          });
     },
 
     _computeTopicReadOnly(mutable, change) {
@@ -252,32 +264,25 @@
     },
 
     _computeProjectURL(project) {
-      return this.getBaseUrl() + '/q/project:' +
-        this.encodeURL(project, false);
+      return Gerrit.Nav.getUrlForProject(project);
     },
 
     _computeBranchURL(project, branch) {
-      let status;
-      if (this.change.status == this.ChangeStatus.NEW) {
-        status = 'open';
-      } else {
-        status = this.change.status.toLowerCase();
-      }
-      return this.getBaseUrl() + '/q/project:' +
-        this.encodeURL(project, false) +
-          ' branch:' + this.encodeURL(branch, false) +
-              ' status:' + this.encodeURL(status, false);
+      return Gerrit.Nav.getUrlForBranch(branch, project,
+          this.change.status == this.ChangeStatus.NEW ? 'open' :
+              this.change.status.toLowerCase());
     },
 
     _computeTopicURL(topic) {
-      return this.getBaseUrl() + '/q/topic:' +
-          this.encodeURL('"' + topic + '"', false) +
-            '+(status:open OR status:merged)';
+      return Gerrit.Nav.getUrlForTopic(topic);
     },
 
     _handleTopicRemoved() {
-      this.set(['change', 'topic'], '');
-      this.$.restAPI.setChangeTopic(this.change._number, null);
+      this.$.restAPI.setChangeTopic(this.change._number, null).then(() => {
+        this.set(['change', 'topic'], '');
+        this.dispatchEvent(
+            new CustomEvent('topic-changed', {bubbles: true}));
+      });
     },
 
     _computeIsWip(change) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 0d7b037..a2d7afe 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -237,10 +237,10 @@
           },
           removable_reviewers: [],
         };
+        flushAsynchronousOperations();
       });
 
       test('_computeCanDeleteVote hides delete button', () => {
-        flushAsynchronousOperations();
         const button = element.$$('gr-account-chip').$$('gr-button');
         assert.isTrue(button.hasAttribute('hidden'));
         element.mutable = true;
@@ -255,7 +255,6 @@
           },
         ];
         element.mutable = true;
-        flushAsynchronousOperations();
         const button = element.$$('gr-account-chip').$$('gr-button');
         assert.isFalse(button.hasAttribute('hidden'));
       });
@@ -285,26 +284,36 @@
         MockInteractions.tap(button);
       });
 
-      test('changing topic calls setChangeTopic', () => {
-        const topicStub = sandbox.stub(element.$.restAPI, 'setChangeTopic',
-            () => {});
-        element._handleTopicChanged({}, 'the new topic');
-        assert.isTrue(topicStub.calledWith('the number', 'the new topic'));
+      test('changing topic', () => {
+        const newTopic = 'the new topic';
+        sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
+            Promise.resolve(newTopic));
+        element._handleTopicChanged({}, newTopic);
+        const topicChangedSpy = sandbox.spy();
+        element.addEventListener('topic-changed', topicChangedSpy);
+        assert.isTrue(element.$.restAPI.setChangeTopic.calledWith(
+            'the number', newTopic));
+        return element.$.restAPI.setChangeTopic.lastCall.returnValue
+            .then(() => {
+              assert.equal(element.change.topic, newTopic);
+              assert.isTrue(topicChangedSpy.called);
+            });
       });
 
-      test('topic href has quotes', () => {
-        const hrefArr = element._computeTopicURL('test')
-            .split('%2522'); // Double-escaped quote.
-        assert.equal(hrefArr[1], 'test');
-      });
-
-      test('clicking x on topic chip removes topic', () => {
-        const topicStub = sandbox.stub(element.$.restAPI, 'setChangeTopic');
-        flushAsynchronousOperations();
+      test('topic removal', () => {
+        sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
+            Promise.resolve());
         const remove = element.$$('gr-linked-chip').$.remove;
+        const topicChangedSpy = sandbox.spy();
+        element.addEventListener('topic-changed', topicChangedSpy);
         MockInteractions.tap(remove);
-        assert.equal(element.change.topic, '');
-        assert.isTrue(topicStub.called);
+        assert.isTrue(element.$.restAPI.setChangeTopic.calledWith(
+            'the number', null));
+        return element.$.restAPI.setChangeTopic.lastCall.returnValue
+            .then(() => {
+              assert.equal(element.change.topic, '');
+              assert.isTrue(topicChangedSpy.called);
+            });
       });
 
       suite('assignee field', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 7474684..97c1dee 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -14,11 +14,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../diff/gr-diff-preferences/gr-diff-preferences.html">
 <link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
 <link rel="import" href="../../shared/gr-select/gr-select.html">
@@ -316,7 +316,7 @@
               change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
           <a
               aria-label$="[[_computeChangePermalinkAriaLabel(_change._number)]]"
-              href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><!--
+              href$="[[_computeChangeUrl(_change)]]">[[_change._number]]</a><!--
        --><template is="dom-if" if="[[_changeStatus]]"><!--
          --> (<!--
          --><span
@@ -471,7 +471,7 @@
                 commit-info="[[_commitInfo]]"></gr-commit-info>
             <span class="latestPatchContainer">
               /
-              <a href$="[[getBaseUrl()]]/c/[[_change._number]]">Go to latest patch set</a>
+              <a href$="[[_computeChangeUrl(_change)]]">Go to latest patch set</a>
             </span>
             <span class="downloadContainer desktop">
               /
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 56e9d4c..b1501b6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -183,12 +183,14 @@
     },
 
     behaviors: [
-      Gerrit.BaseUrlBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.RESTClientBehavior,
     ],
 
+    listeners: {
+      'topic-changed': '_handleTopicChanged',
+    },
     observers: [
       '_labelsChanged(_change.labels.*)',
       '_paramsAndChangeChanged(params, _change)',
@@ -629,8 +631,8 @@
       page.show(this.changePath(this._changeNum) + '/' + patchExpr);
     },
 
-    _computeChangePermalink(changeNum) {
-      return this.getBaseUrl() + '/' + changeNum;
+    _computeChangeUrl(change) {
+      return Gerrit.Nav.getUrlForChange(change);
     },
 
     _computeChangeStatus(change, patchNum) {
@@ -1285,8 +1287,7 @@
                   action: 'Reload',
                   callback: function() {
                     // Load the current change without any patch range.
-                    location.href = this.getBaseUrl() + '/c/' +
-                        this._change._number;
+                    Gerrit.Nav.navigateToChange(this._change);
                   }.bind(this),
                 });
               }
@@ -1307,6 +1308,10 @@
       }
     },
 
+    _handleTopicChanged() {
+      this.$.relatedChanges.reload();
+    },
+
     _computeHeaderClass(change) {
       return change.work_in_progress ? 'header wip' : 'header';
     },
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 1cc2743..75adcdc 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -1347,5 +1347,11 @@
       assert.isTrue(scrollStub.called);
       assert.equal(scrollStub.lastCall.args[0], 'TEST');
     });
+
+    test('topic update reloads related changes', () => {
+      sandbox.stub(element.$.relatedChanges, 'reload');
+      element.dispatchEvent(new CustomEvent('topic-changed'));
+      assert.isTrue(element.$.relatedChanges.reload.calledOnce);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 559217a..c196051 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -154,6 +154,7 @@
 
     _computeIsAutomated(message) {
       return !!(message.reviewer ||
+          message.type === 'REVIEWER_UPDATE' ||
           (message.tag && message.tag.startsWith('autogenerated')));
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 96f5dfb..07f13dc 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -125,6 +125,21 @@
       assert.isTrue(element.hidden);
     });
 
+    test('batch reviewer message treated as autogenerated', () => {
+      element.message = {
+        type: 'REVIEWER_UPDATE',
+        updated: '2016-01-12 20:24:49.448000000',
+        reviewer: {},
+      };
+
+      assert.isTrue(element.isAutomated);
+      assert.isFalse(element.hidden);
+
+      element.hideAutomated = true;
+
+      assert.isTrue(element.hidden);
+    });
+
     test('tag that is not autogenerated prefix does not hide', () => {
       element.message = {
         tag: 'something',
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
new file mode 100644
index 0000000..0c632e1
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -0,0 +1,152 @@
+<!--
+Copyright (C) 2017 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.
+-->
+<script>
+  (function(window) {
+    'use strict';
+
+    // Navigation parameters object format:
+    //
+    // Each object has a `view` property with a value from Gerrit.Nav.View. The
+    // remaining properties depend on the value used for view.
+    //
+    //  - Gerrit.Nav.View.CHANGE:
+    //    - `id`, required, String: the numeric ID of the change.
+    //
+    // - Gerrit.Nav.View.SEARCH:
+    //    - `owner`, optional, String: the owner name.
+    //    - `project`, optional, String: the project name.
+    //    - `branch`, optional, String: the branch name.
+    //    - `topic`, optional, String: the topic name.
+    //    - `statuses`, optional, Array<String>: the list of change statuses to
+    //        search for. If more than one is provided, the search will OR them
+    //        together.
+
+    window.Gerrit = window.Gerrit || {};
+
+    // Prevent redefinition.
+    if (window.Gerrit.hasOwnProperty('Nav')) { return; }
+
+    const uninitialized = () => {
+      console.warn('Use of uninitialized routing');
+    };
+
+    window.Gerrit.Nav = {
+
+      View: {
+        CHANGE: 'change',
+        SEARCH: 'search',
+      },
+
+      /** @type {Function} */
+      _navigate: uninitialized,
+
+      /** @type {Function} */
+      _generateUrl: uninitialized,
+
+      /**
+       * Setup router implementation.
+       * @param {Function} handleNavigate
+       * @param {Function} generateUrl
+       */
+      setup(navigate, generateUrl) {
+        this._navigate = navigate;
+        this._generateUrl = generateUrl;
+      },
+
+      destroy() {
+        this._navigate = uninitialized;
+        this._generateUrl = uninitialized;
+      },
+
+      /**
+       * Generate a URL for the given route parameters.
+       * @param {Object} params
+       * @return {String}
+       */
+      _getUrlFor(params) {
+        return this._generateUrl(params);
+      },
+
+      /**
+       * @param {String} project The name of the project.
+       * @return {String}
+       */
+      getUrlForProject(project) {
+        return this._getUrlFor({
+          view: Gerrit.Nav.View.SEARCH,
+          project,
+        });
+      },
+
+      /**
+       * @param {String} branch The name of the branch.
+       * @param {String} project The name of the project.
+       * @param {String} status The status to search.
+       * @return {String}
+       */
+      getUrlForBranch(branch, project, status) {
+        return this._getUrlFor({
+          view: Gerrit.Nav.View.SEARCH,
+          branch,
+          project,
+          statuses: [status],
+        });
+      },
+
+      /**
+       * @param {String} topic The name of the topic.
+       * @return {String}
+       */
+      getUrlForTopic(topic) {
+        return this._getUrlFor({
+          view: Gerrit.Nav.View.SEARCH,
+          topic,
+          statuses: ['open', 'merged'],
+        });
+      },
+
+      /**
+       * @param {Object} change The change object.
+       * @return {String}
+       */
+      getUrlForChange(change) {
+        return this._getUrlFor({
+          view: Gerrit.Nav.View.CHANGE,
+          id: change._number,
+        });
+      },
+
+      /**
+       * @param {Object} change The change object.
+       * @return {String}
+       */
+      navigateToChange(change) {
+        this._navigate(this.getUrlForChange(change));
+      },
+
+      /**
+       * @param {String} owner The name of the owner.
+       * @return {String}
+       */
+      getUrlForOwner(owner) {
+        return this._getUrlFor({
+          view: Gerrit.Nav.View.SEARCH,
+          owner,
+        });
+      },
+    };
+  })(window);
+</script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.html b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
index 5a494b1..d311a6b 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
@@ -14,7 +14,9 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-reporting/gr-reporting.html">
 
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 b1f6ecf..7675670 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -37,7 +37,11 @@
     getReporting().timeEnd('WebComponentsReady');
   });
 
-  function startRouter() {
+  function encode(s) {
+    return window.Gerrit.URLEncodingBehavior.encodeURL(s, false);
+  }
+
+  function startRouter(generateUrl) {
     const base = window.Gerrit.BaseUrlBehavior.getBaseUrl();
     if (base) {
       page.base(base);
@@ -46,6 +50,8 @@
     const restAPI = document.createElement('gr-rest-api-interface');
     const reporting = getReporting();
 
+    Gerrit.Nav.setup(url => { page.show(url); }, generateUrl);
+
     // Middleware
     page((ctx, next) => {
       document.body.scrollTop = 0;
@@ -150,6 +156,22 @@
       });
     });
 
+    // Matches /admin/create-project.
+    page('/admin/create-project', loadUser, data => {
+      restAPI.getLoggedIn().then(loggedIn => {
+        restAPI.getAccountCapabilities(false).then(permission => {
+          if (loggedIn &&
+              (permission.administrateServer || permission.createProject)) {
+            app.params = {
+              view: 'gr-admin-create-project',
+            };
+          } else {
+            page.redirect('/login/' + encodeURIComponent(data.canonicalPath));
+          }
+        });
+      });
+    });
+
     // Matches /admin/projects[,<offset>][/].
     page(/^\/admin\/projects(,(\d+))?(\/)?$/, loadUser, data => {
       app.params = {
@@ -336,7 +358,45 @@
     is: 'gr-router',
     start() {
       if (!app) { return; }
-      startRouter();
+      startRouter(this._generateUrl);
+    },
+
+    _generateUrl(params) {
+      const base = window.Gerrit.BaseUrlBehavior.getBaseUrl();
+      let url = '';
+
+      if (params.view === Gerrit.Nav.View.SEARCH) {
+        const operators = [];
+        if (params.owner) {
+          operators.push('owner:' + encode(params.owner));
+        }
+        if (params.project) {
+          operators.push('project:' + encode(params.project));
+        }
+        if (params.branch) {
+          operators.push('branch:' + encode(params.branch));
+        }
+        if (params.topic) {
+          operators.push('topic:"' + encode(params.topic) + '"');
+        }
+        if (params.statuses) {
+          if (params.statuses.length === 1) {
+            operators.push('status:' + encode(params.statuses[0]));
+          } else if (params.statuses.length > 1) {
+            operators.push(
+                '(' +
+                params.statuses.map(s => `status:${encode(s)}`).join(' OR ') +
+                ')');
+          }
+        }
+        url = '/q/' + operators.join('+');
+      } else if (params.view === Gerrit.Nav.View.CHANGE) {
+        url = '/c/' + params.id;
+      } else {
+        throw new Error('Can\'t generate');
+      }
+
+      return base + url;
     },
   });
 })();
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
new file mode 100644
index 0000000..36ff2e4
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-router</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-router.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-router></gr-router>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-router tests', () => {
+    suite('generateUrl', () => {
+      let element;
+
+      setup(() => {
+        element = fixture('basic');
+      });
+
+      test('search', () => {
+        let params = {
+          view: Gerrit.Nav.View.SEARCH,
+          owner: 'a%b',
+          project: 'c%d',
+          branch: 'e%f',
+          topic: 'g%h',
+          statuses: ['op%en'],
+        };
+        assert.equal(element._generateUrl(params),
+            '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
+            'topic:"g%2525h"+status:op%2525en');
+
+        params = {
+          view: Gerrit.Nav.View.SEARCH,
+          statuses: ['a', 'b', 'c'],
+        };
+        assert.equal(element._generateUrl(params),
+            '/q/(status:a OR status:b OR status:c)');
+      });
+
+      test('change', () => {
+        const params = {
+          view: Gerrit.Nav.View.CHANGE,
+          id: '1234',
+        };
+        assert.equal(element._generateUrl(params), '/c/1234');
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index a47a695..5056927 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -141,6 +141,10 @@
       .separator {
         margin: 0 .25em;
       }
+      .noOverflow {
+        display: block;
+        overflow: auto;
+      }
       @media screen and (max-width: 50em) {
         header {
           padding: .5em var(--default-horizontal-margin);
@@ -188,6 +192,7 @@
       }
     </style>
     <gr-fixed-panel
+        floating-disabled="[[_panelFloatingDisabled]]"
         keep-on-scroll
         ready-for-measure="[[!_loading]]">
       <header>
@@ -297,26 +302,25 @@
       </div>
     </gr-fixed-panel>
     <div class="loading" hidden$="[[!_loading]]">Loading...</div>
-    <div hidden hidden$="[[_loading]]">
-      <gr-diff-preferences
-          id="diffPreferences"
-          prefs="{{_prefs}}"
-          local-prefs="{{_localPrefs}}"></gr-diff-preferences>
-      <gr-diff
-          id="diff"
-          hidden
-          hidden$="[[_loading]]"
-          is-image-diff="{{_isImageDiff}}"
-          files-weblinks="{{_filesWeblinks}}"
-          change-num="[[_changeNum]]"
-          patch-range="[[_patchRange]]"
-          path="[[_path]]"
-          prefs="[[_prefs]]"
-          project-config="[[_projectConfig]]"
-          view-mode="[[_diffMode]]"
-          on-line-selected="_onLineSelected">
-      </gr-diff>
-    </div>
+    <gr-diff
+        id="diff"
+        hidden
+        hidden$="[[_loading]]"
+        class$="[[_computeDiffClass(_panelFloatingDisabled)]]"
+        is-image-diff="{{_isImageDiff}}"
+        files-weblinks="{{_filesWeblinks}}"
+        change-num="[[_changeNum]]"
+        patch-range="[[_patchRange]]"
+        path="[[_path]]"
+        prefs="[[_prefs]]"
+        project-config="[[_projectConfig]]"
+        view-mode="[[_diffMode]]"
+        on-line-selected="_onLineSelected">
+    </gr-diff>
+    <gr-diff-preferences
+        id="diffPreferences"
+        prefs="{{_prefs}}"
+        local-prefs="{{_localPrefs}}"></gr-diff-preferences>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
     <gr-diff-cursor id="cursor"></gr-diff-cursor>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 95e813a..1b918db 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -104,6 +104,10 @@
         type: Object,
         computed: '_computeCommentSkips(_commentMap, _fileList, _path)',
       },
+      _panelFloatingDisabled: {
+        type: Boolean,
+        value: () => { return window.PANEL_FLOATING_DISABLED; },
+      },
     },
 
     behaviors: [
@@ -718,5 +722,11 @@
 
       return skips;
     },
+
+    _computeDiffClass(panelFloatingDisabled) {
+      if (panelFloatingDisabled) {
+        return 'noOverflow';
+      }
+    },
   });
 })();
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 5738b40..5319ace 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -198,6 +198,18 @@
         font-size: var(--font-size, 12px);
         padding: 0.5em 0 0.5em 4em;
       }
+      #sizeWarning {
+        display: none;
+        margin: 1em auto;
+        max-width: 60em;
+        text-align: center;
+      }
+      #sizeWarning gr-button {
+        margin: 1em;
+      }
+      #sizeWarning.warn {
+        display: block;
+      }
     </style>
     <style include="gr-theme-default"></style>
     <div id="diffHeader" hidden$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]">
@@ -232,6 +244,18 @@
         </gr-diff-highlight>
       </gr-diff-selection>
     </div>
+    <div id="sizeWarning" class$="[[_computeWarningClass(_showWarning)]]">
+      <p>
+        Prevented render because "Whole file" is enabled and this diff is very
+        large (about [[_diffLength(_diff)]] lines).
+      </p>
+      <gr-button on-tap="_handleLimitedBypass">
+        Render with limited context
+      </gr-button>
+      <gr-button on-tap="_handleFullBypass">
+        Render anyway (may be slow)
+      </gr-button>
+    </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-diff-line.js"></script>
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 386ac08..8103cb3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -24,6 +24,10 @@
     RIGHT: 'right',
   };
 
+  const LARGE_DIFF_THRESHOLD_LINES = 10000;
+  const FULL_CONTEXT = -1;
+  const LIMITED_CONTEXT = 10;
+
   Polymer({
     is: 'gr-diff',
 
@@ -100,6 +104,19 @@
       _comments: Object,
       _baseImage: Object,
       _revisionImage: Object,
+
+      /**
+       * Whether the safety check for large diffs when whole-file is set has
+       * been bypassed. If the value is null, then the safety has not been
+       * bypassed. If the value is a number, then that number represents the
+       * context preference to use when rendering the bypassed diff.
+       */
+      _safetyBypass: {
+        type: Number,
+        value: null,
+      },
+
+      _showWarning: Boolean,
     },
 
     listeners: {
@@ -124,6 +141,8 @@
 
     reload() {
       this.$.diffBuilder.cancel();
+      this._safetyBypass = null;
+      this._showWarning = false;
       this._clearDiffContent();
 
       const promises = [];
@@ -447,7 +466,25 @@
     },
 
     _renderDiffTable() {
-      return this.$.diffBuilder.render(this._comments, this.prefs);
+      if (this.prefs.context === -1 &&
+          this._diffLength(this._diff) >= LARGE_DIFF_THRESHOLD_LINES &&
+          this._safetyBypass === null) {
+        this._showWarning = true;
+        return Promise.resolve();
+      }
+
+      this._showWarning = false;
+      return this.$.diffBuilder.render(this._comments, this._getBypassPrefs());
+    },
+
+    /**
+     * Get the preferences object including the safety bypass context (if any).
+     */
+    _getBypassPrefs() {
+      if (this._safetyBypass !== null) {
+        return Object.assign({}, this.prefs, {context: this._safetyBypass});
+      }
+      return this.prefs;
     },
 
     _clearDiffContent() {
@@ -600,5 +637,38 @@
     _computeDiffHeaderHidden(items) {
       return items.length === 0;
     },
+
+    /**
+     * The number of lines in the diff. For delta chunks that are different
+     * sizes on the left and the right, the longer side is used.
+     * @param {!Object} diff
+     * @return {Number}
+     */
+    _diffLength(diff) {
+      return diff.content.reduce((sum, sec) => {
+        if (sec.hasOwnProperty('ab')) {
+          return sum + sec.ab.length;
+        } else {
+          return sum + Math.max(
+              sec.hasOwnProperty('a') ? sec.a.length : 0,
+              sec.hasOwnProperty('b') ? sec.b.length : 0
+          );
+        }
+      }, 0);
+    },
+
+    _handleFullBypass() {
+      this._safetyBypass = FULL_CONTEXT;
+      this._renderDiffTable();
+    },
+
+    _handleLimitedBypass() {
+      this._safetyBypass = LIMITED_CONTEXT;
+      this._renderDiffTable();
+    },
+
+    _computeWarningClass(showWarning) {
+      return showWarning ? 'warn' : '';
+    },
   });
 })();
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 e7b81be..921d285 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
@@ -60,6 +60,12 @@
       assert.isTrue(cancelStub.called);
     });
 
+    test('_diffLength', () => {
+      element = fixture('basic');
+      const mock = document.createElement('mock-diff-response');
+      assert.equal(element._diffLength(mock.diffResponse), 52);
+    });
+
     suite('not logged in', () => {
       setup(() => {
         stub('gr-rest-api-interface', {
@@ -991,6 +997,48 @@
         assert.equal(element._diffHeaderItems.length, 0);
       });
     });
+
+    suite('safety and bypass', () => {
+      let renderStub;
+
+      setup(() => {
+        element = fixture('basic');
+        renderStub = sandbox.stub(element.$.diffBuilder, 'render',
+            () => Promise.resolve());
+        const mock = document.createElement('mock-diff-response');
+        element._diff = mock.diffResponse;
+        element._comments = {left: [], right: []};
+        element.noRenderOnPrefsChange = true;
+      });
+
+      test('lage render w/ context = 10', () => {
+        element.prefs = {context: 10};
+        sandbox.stub(element, '_diffLength', () => 10000);
+        return element._renderDiffTable().then(() => {
+          assert.isTrue(renderStub.called);
+          assert.isFalse(element._showWarning);
+        });
+      });
+
+      test('lage render w/ whole file and bypass', () => {
+        element.prefs = {context: -1};
+        element._safetyBypass = 10;
+        sandbox.stub(element, '_diffLength', () => 10000);
+        return element._renderDiffTable().then(() => {
+          assert.isTrue(renderStub.called);
+          assert.isFalse(element._showWarning);
+        });
+      });
+
+      test('lage render w/ whole file and no bypass', () => {
+        element.prefs = {context: -1};
+        sandbox.stub(element, '_diffLength', () => 10000);
+        return element._renderDiffTable().then(() => {
+          assert.isFalse(renderStub.called);
+          assert.isTrue(element._showWarning);
+        });
+      });
+    });
   });
 
   a11ySuite('basic');
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index 7736c81..3ab8a5f 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -31,6 +31,7 @@
     'text/x-go': 'go',
     'text/x-haskell': 'haskell',
     'text/x-java': 'java',
+    'text/x-kotlin': 'kotlin',
     'text/x-lua': 'lua',
     'text/x-markdown': 'markdown',
     'text/x-objectivec': 'objectivec',
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
index fabc347..31a276f 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
@@ -29,8 +29,7 @@
          color: #FF1717;
       }
       .gr-syntax-keyword {
-        color: #7F0055;
-        font-weight: bold;
+        color: #9E0069;
         line-height: 1em;
       }
       .gr-syntax-number,
@@ -80,7 +79,7 @@
         font-style: italic;
       }
       .gr-syntax-strong {
-        font-weight: bold;
+        font-weight: 700;
       }
     </style>
   </template>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index aa670aa..5115271 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -18,10 +18,11 @@
 <link rel="import" href="../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../styles/app-theme.html">
+<link rel="import" href="./admin/gr-admin-create-project/gr-admin-create-project.html">
 <link rel="import" href="./admin/gr-admin-group-list/gr-admin-group-list.html">
+<link rel="import" href="./admin/gr-admin-plugin-list/gr-admin-plugin-list.html">
 <link rel="import" href="./admin/gr-admin-project-list/gr-admin-project-list.html">
 <link rel="import" href="./admin/gr-admin-project/gr-admin-project.html">
-<link rel="import" href="./admin/gr-admin-plugin-list/gr-admin-plugin-list.html">
 <link rel="import" href="./admin/gr-admin-view/gr-admin-view.html">
 <link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html">
 <link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html">
@@ -72,6 +73,7 @@
         display: flex;
         justify-content: space-between;
         padding: .5rem var(--default-horizontal-margin);
+        z-index: 100;
       }
       main {
         flex: 1;
@@ -158,6 +160,11 @@
       <template is="dom-if" if="[[_showPluginListView]]" restamp="true">
         <gr-admin-plugin-list id="pluginList"></gr-admin-plugin-list>
       </template>
+      <template is="dom-if" if="[[_createProject]]" restamp="true">
+        <gr-admin-create-project
+            params="[[params]]"
+            id="createProject"></gr-admin-create-project>
+      </template>
       <template is="dom-if" if="[[_showAdminView]]" restamp="true">
         <gr-admin-view path="[[_path]]"></gr-admin-view>
       </template>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 0526b4d..abc2127d 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -51,6 +51,7 @@
       _showProjectListView: Boolean,
       _showAdminProject: Boolean,
       _showPluginListView: Boolean,
+      _createProject: Boolean,
       _showAdminView: Boolean,
       _showCLAView: Boolean,
       _showGroupListView: Boolean,
@@ -144,6 +145,7 @@
       this.set('_showProjectListView', view === 'gr-admin-project-list');
       this.set('_showAdminProject', view === 'gr-admin-project');
       this.set('_showPluginListView', view === 'gr-admin-plugin-list');
+      this.set('_createProject', view === 'gr-admin-create-project');
       this.set('_showAdminView', view === 'gr-admin-view');
       this.set('_showCLAView', view === 'gr-cla-view');
       if (this.params.justRegistered) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index e35d881..aa568b3 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -23,6 +23,7 @@
 <link rel="import" href="../gr-menu-editor/gr-menu-editor.html">
 <link rel="import" href="../gr-ssh-editor/gr-ssh-editor.html">
 <link rel="import" href="../gr-watched-projects-editor/gr-watched-projects-editor.html">
+<link rel="import" href="../../settings/gr-change-table-editor/gr-change-table-editor.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
index dc4024e..42a6f6b 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
@@ -16,6 +16,7 @@
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../gr-account-label/gr-account-label.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index b30a26c..9a287fc 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -31,8 +31,7 @@
 
     _computeOwnerLink(account) {
       if (!account) { return; }
-      const accountID = account.email || account._account_id;
-      return this.getBaseUrl() + '/q/owner:' + encodeURIComponent(accountID);
+      return Gerrit.Nav.getUrlForOwner(account.email || account._account_id);
     },
 
     _computeShowEmail(account) {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
index 7e2e97b..11b099b 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -43,18 +43,7 @@
     });
 
     test('computed fields', () => {
-      assert.equal(element._computeOwnerLink(
-          {
-            _account_id: 123,
-            email: 'andybons+gerrit@gmail.com',
-          }),
-          '/q/owner:andybons%2Bgerrit%40gmail.com');
-
-      assert.equal(element._computeOwnerLink({_account_id: 42}),
-          '/q/owner:42');
-
       assert.equal(element._computeShowEmail({name: 'asd'}), false);
-
       assert.equal(element._computeShowEmail({}), true);
     });
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
index 8be89c4..68800a1 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -102,14 +102,12 @@
       }
     },
 
-    // TODO @beckysiegel make this work with shadow dom.
     cursorDown(e) {
       if (!this.hidden) {
         this.$.cursor.next();
       }
     },
 
-    // TODO @beckysiegel make this work with shadow dom.
     cursorUp(e) {
       if (!this.hidden) {
         this.$.cursor.previous();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index b347bb6..19c948f 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -196,8 +196,7 @@
 
     _resetCursorStops() {
       Polymer.dom.flush();
-      // TODO(kaspern): This is broken in shadow DOM.
-      this._listElements = this.querySelectorAll('li');
+      this._listElements = Polymer.dom(this.root).querySelectorAll('li');
     },
   });
-})();
\ No newline at end of file
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
index 27cb373..fc38daa 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -18,6 +18,7 @@
     is: 'gr-fixed-panel',
 
     properties: {
+      floatingDisabled: Boolean,
       readyForMeasure: {
         type: Boolean,
         observer: '_readyForMeasureObserver',
@@ -45,6 +46,9 @@
     },
 
     attached() {
+      if (this.floatingDisabled) {
+        return;
+      }
       // Enable content measure unless blocked by param.
       if (this.readyForMeasure !== false) {
         this.readyForMeasure = true;
@@ -58,7 +62,9 @@
     detached() {
       this.unlisten(window, 'scroll', '_updateOnScroll');
       this.unlisten(window, 'resize', 'update');
-      this._observer.disconnect();
+      if (this._observer) {
+        this._observer.disconnect();
+      }
     },
 
     _readyForMeasureObserver(readyForMeasure) {
@@ -76,6 +82,9 @@
     },
 
     unfloat() {
+      if (this.floatingDisabled) {
+        return;
+      }
       this.$.header.style.top = '';
       this._headerFloating = false;
       this.customStyle['--header-height'] = '';
@@ -95,6 +104,9 @@
     },
 
     _updateDebounced() {
+      if (this.floatingDisabled) {
+        return;
+      }
       this._isMeasured = false;
       this._maybeFloatHeader();
       this._reposition();
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
index 97c92b7..d813a44 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
@@ -39,8 +39,8 @@
     let sandbox;
 
     setup(() => {
-      element = fixture('basic');
       sandbox = sinon.sandbox.create();
+      element = fixture('basic');
       element.readyForMeasure = true;
     });
 
@@ -48,6 +48,14 @@
       sandbox.restore();
     });
 
+    test('can be disabled with floatingDisabled', () => {
+      element.floatingDisabled = true;
+      sandbox.stub(element, '_reposition');
+      window.dispatchEvent(new CustomEvent('resize'));
+      element.flushDebouncer('update');
+      assert.isFalse(element._reposition.called);
+    });
+
     test('header is the height of the content', () => {
       assert.equal(element.getBoundingClientRect().height, 100);
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
index 5fa21e4..3209590 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
@@ -22,8 +22,10 @@
   <template>
     <style include="shared-styles">
       #nav {
+        background-color: #f5f5f5;
         border: 1px solid #eee;
         border-top: none;
+        height: 100%;
         position: absolute;
         top: 0;
         width: 14em;
@@ -32,7 +34,29 @@
         position: fixed;
       }
       #nav ::content ul {
-        margin: 1em 2em;
+        padding: 1em 0;
+      }
+      #nav ::content li {
+        border-bottom: 1px solid transparent;
+        border-top: 1px solid transparent;
+        padding: 0 2em;
+      }
+      #nav ::content li.sectionTitle {
+        padding: 0 2em 0 1.5em;
+      }
+      #nav ::content li.sectionTitle:not(:first-child) {
+        padding-top: 1em;
+      }
+      #nav ::content .title {
+        display: flex;
+        font-weight: bold;
+        margin: .4em 0;
+      }
+      #nav ::content .selected {
+        background-color: #fff;
+        border-bottom: 1px dotted #808080;
+        border-top: 1px dotted #808080;
+        font-weight: bold;
       }
       #nav ::content a {
         color: black;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth.js
new file mode 100644
index 0000000..2f60268
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth.js
@@ -0,0 +1,193 @@
+// Copyright (C) 2017 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.
+(function(window) {
+  'use strict';
+
+  // Prevent redefinition.
+  if (window.GrGapiAuth) { return; }
+
+  const EMAIL_SCOPE = 'email';
+
+  function GrGapiAuth() {}
+
+  GrGapiAuth._loadGapiPromise = null;
+  GrGapiAuth._setupPromise = null;
+  GrGapiAuth._refreshTokenPromise = null;
+  GrGapiAuth._sharedAuthToken = null;
+  GrGapiAuth._oauthClientId = null;
+  GrGapiAuth._oauthEmail = null;
+
+  GrGapiAuth.prototype.fetch = function(url, options) {
+    options = Object.assign({}, options);
+    return this._getAccessToken().then(
+        token => window.FASTER_GERRIT_CORS ?
+            this._fasterGerritCors(url, options, token) :
+            this._defaultFetch(url, options, token)
+    );
+  };
+
+  GrGapiAuth.prototype._defaultFetch = function(url, options, token) {
+    if (token) {
+      options.headers = options.headers || new Headers();
+      options.headers.append('Authorization', `Bearer ${token}`);
+      if (!url.startsWith('/a/')) {
+        url = '/a' + url;
+      }
+    }
+    return fetch(url, options);
+  };
+
+  GrGapiAuth.prototype._fasterGerritCors = function(url, options, token) {
+    const method = options.method || 'GET';
+    if (method === 'GET') {
+      return fetch(url, options);
+    }
+    const params = [];
+    if (token) {
+      params.push(`access_token=${token}`);
+    }
+    const contentType = options.headers && options.headers.get('Content-Type');
+    if (contentType) {
+      options.headers.set('Content-Type', 'text/plain');
+      params.push(`$ct=${encodeURIComponent(contentType)}`);
+    }
+    params.push(`$m=${method}`);
+    url = url + (url.indexOf('?') === -1 ? '?' : '') + params.join('&');
+    options.method = 'POST';
+    return fetch(url, options);
+  };
+
+  GrGapiAuth.prototype._getAccessToken = function() {
+    if (this._isTokenValid(GrGapiAuth._sharedAuthToken)) {
+      return Promise.resolve(GrGapiAuth._sharedAuthToken.access_token);
+    }
+    if (!GrGapiAuth._refreshTokenPromise) {
+      GrGapiAuth._refreshTokenPromise = this._loadGapi()
+        .then(() => this._configureOAuthLibrary())
+        .then(() => this._refreshToken())
+        .then(token => {
+          GrGapiAuth._sharedAuthToken = token;
+          GrGapiAuth._refreshTokenPromise = null;
+          return this._getAccessToken();
+        }).catch(err => {
+          console.error(err);
+        });
+    }
+    return GrGapiAuth._refreshTokenPromise;
+  };
+
+  GrGapiAuth.prototype._isTokenValid = function(token) {
+    if (!token) { return false; }
+    if (!token.access_token || !token.expires_at) { return false; }
+
+    const expiration = new Date(parseInt(token.expires_at, 10) * 1000);
+    if (Date.now() >= expiration) { return false; }
+
+    return true;
+  };
+
+  GrGapiAuth.prototype._loadGapi = function() {
+    if (!GrGapiAuth._loadGapiPromise) {
+      GrGapiAuth._loadGapiPromise = new Promise((resolve, reject) => {
+        const scriptEl = document.createElement('script');
+        scriptEl.defer = true;
+        scriptEl.async = true;
+        scriptEl.src = 'https://apis.google.com/js/platform.js';
+        scriptEl.onerror = reject;
+        scriptEl.onload = resolve;
+        document.body.appendChild(scriptEl);
+      });
+    }
+    return GrGapiAuth._loadGapiPromise;
+  };
+
+  GrGapiAuth.prototype._configureOAuthLibrary = function() {
+    if (!GrGapiAuth._setupPromise) {
+      GrGapiAuth._setupPromise = new Promise(
+        resolve => gapi.load('config_min', resolve)
+      )
+        .then(() => this._getOAuthConfig())
+        .then(config => {
+          if (config.hasOwnProperty('auth_url') && config.auth_url) {
+            gapi.config.update('oauth-flow/authUrl', config.auth_url);
+          }
+          if (config.hasOwnProperty('proxy_url') && config.proxy_url) {
+            gapi.config.update('oauth-flow/proxyUrl', config.proxy_url);
+          }
+          GrGapiAuth._oauthClientId = config.client_id;
+          GrGapiAuth._oauthEmail = config.email;
+
+          // Loading auth has a side-effect. The URLs should be set before
+          // loading it.
+          return new Promise(
+            resolve => gapi.load('auth', () => gapi.auth.init(resolve))
+          );
+        });
+    }
+    return GrGapiAuth._setupPromise;
+  };
+
+  GrGapiAuth.prototype._refreshToken = function() {
+    const opts = {
+      client_id: GrGapiAuth._oauthClientId,
+      immediate: true,
+      scope: EMAIL_SCOPE,
+      login_hint: GrGapiAuth._oauthEmail,
+    };
+    return new Promise((resolve, reject) => {
+      gapi.auth.authorize(opts, token => {
+        if (!token) {
+          reject('No token returned');
+        } else if (token.error) {
+          reject(token.error);
+        } else {
+          resolve(token);
+        }
+      });
+    });
+  };
+
+  GrGapiAuth.prototype._getOAuthConfig = function() {
+    const authConfigURL = '/accounts/self/oauthconfig';
+    const opts = {
+      headers: new Headers({Accept: 'application/json'}),
+      credentials: 'same-origin',
+    };
+    return fetch(authConfigURL, opts).then(response => {
+      if (!response.ok) {
+        console.error(response.statusText);
+        if (response.body && response.body.then) {
+          return response.body.then(text => {
+            return Promise.reject(text);
+          });
+        }
+        if (response.statusText) {
+          return Promise.reject(response.statusText);
+        } else {
+          return Promise.reject('_getOAuthConfig' + response.status);
+        }
+      }
+      return this._getResponseObject(response);
+    });
+  };
+
+  GrGapiAuth.prototype._getResponseObject = function(response) {
+    const JSON_PREFIX = ')]}\'';
+    return response.text().then(text => {
+      return JSON.parse(text.substring(JSON_PREFIX.length));
+    });
+  },
+
+  window.GrGapiAuth = GrGapiAuth;
+})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth_test.html
new file mode 100644
index 0000000..3890fd6
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth_test.html
@@ -0,0 +1,199 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-gapi-auth</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+
+<script src="gr-gapi-auth.js"></script>
+
+<script>
+  suite('gr-rest-api-interface tests', () => {
+    let auth;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      auth = new GrGapiAuth();
+      window.gapi = {
+        load: sandbox.stub().callsArg(1),
+        config: {
+          update: sandbox.stub(),
+        },
+        auth: {
+          init: sandbox.stub().callsArg(0),
+          authorize: sandbox.stub(),
+        },
+      };
+      sandbox.stub(window, 'fetch').returns(Promise.resolve());
+    });
+
+    teardown(() => {
+      delete window.gapi;
+      sandbox.restore();
+    });
+
+    test('exists', () => {
+      assert.isOk(auth);
+    });
+
+    test('fetch signed in', () => {
+      sandbox.stub(auth, '_getAccessToken').returns(Promise.resolve('foo'));
+      return auth.fetch('/url', {bar: 'bar'}).then(() => {
+        assert.isTrue(auth._getAccessToken.called);
+        const [url, options] = fetch.lastCall.args;
+        assert.equal(url, '/a/url');
+        assert.equal(options.bar, 'bar');
+        assert.equal(options.headers.get('Authorization'), 'Bearer foo');
+      });
+    });
+
+    test('fetch not signed in', () => {
+      sandbox.stub(auth, '_getAccessToken').returns(Promise.resolve());
+      return auth.fetch('/url', {bar: 'bar'}).then(() => {
+        assert.isTrue(auth._getAccessToken.called);
+        const [url, options] = fetch.lastCall.args;
+        assert.equal(url, '/url');
+        assert.equal(options.bar, 'bar');
+        assert.isUndefined(options.headers);
+      });
+    });
+
+    test('_getAccessToken returns valid shared token', () => {
+      GrGapiAuth._sharedAuthToken = {access_token: 'foo'};
+      sandbox.stub(auth, '_isTokenValid').returns(true);
+      return auth._getAccessToken().then(token => {
+        assert.equal(token, 'foo');
+      });
+    });
+
+    test('_getAccessToken refreshes token', () => {
+      const token = {access_token: 'foo'};
+      sandbox.stub(auth, '_loadGapi').returns(Promise.resolve());
+      sandbox.stub(auth, '_configureOAuthLibrary').returns(Promise.resolve());
+      sandbox.stub(auth, '_refreshToken').returns(Promise.resolve(token));
+      sandbox.stub(auth, '_isTokenValid').returns(true)
+          .onFirstCall().returns(false);
+      return auth._getAccessToken().then(token => {
+        assert.isTrue(auth._loadGapi.called);
+        assert.isTrue(auth._configureOAuthLibrary.called);
+        assert.isTrue(auth._refreshToken.called);
+        assert.equal(token, 'foo');
+      });
+    });
+
+    test('_isTokenValid', () => {
+      assert.isFalse(auth._isTokenValid());
+      assert.isFalse(auth._isTokenValid({}));
+      assert.isFalse(auth._isTokenValid({access_token: 'foo'}));
+      assert.isFalse(auth._isTokenValid({
+        access_token: 'foo',
+        expires_at: Date.now()/1000 - 1,
+      }));
+      assert.isTrue(auth._isTokenValid({
+        access_token: 'foo',
+        expires_at: Date.now()/1000 + 1,
+      }));
+    });
+
+    test('_configureOAuthLibrary', () => {
+      sandbox.stub(auth, '_getOAuthConfig').returns({
+        auth_url: 'some_auth_url',
+        proxy_url: 'some_proxy_url',
+        client_id: 'some_client_id',
+        email: 'some_email',
+      });
+      return auth._configureOAuthLibrary().then(() => {
+        assert.isTrue(gapi.load.calledWith('config_min'));
+        assert.isTrue(auth._getOAuthConfig.called);
+        assert.isTrue(gapi.config.update.calledWith(
+            'oauth-flow/authUrl', 'some_auth_url'));
+        assert.isTrue(gapi.config.update.calledWith(
+            'oauth-flow/proxyUrl', 'some_proxy_url'));
+        assert.equal(GrGapiAuth._oauthClientId, 'some_client_id');
+        assert.equal(GrGapiAuth._oauthEmail, 'some_email');
+        assert.isTrue(gapi.auth.init.called);
+        assert.isTrue(gapi.load.calledWith('auth'));
+      });
+    });
+
+    test('_refreshToken no token', () => {
+      gapi.auth.authorize.callsArgWith(1, null);
+      return auth._refreshToken().catch(reason => {
+        assert.equal(reason, 'No token returned');
+      });
+    });
+
+    test('_refreshToken error', () => {
+      gapi.auth.authorize.callsArgWith(1, {error: 'some error'});
+      return auth._refreshToken().catch(reason => {
+        assert.equal(reason, 'some error');
+      });
+    });
+
+    test('_refreshToken', () => {
+      const token = {};
+      gapi.auth.authorize.callsArgWith(1, token);
+      return auth._refreshToken().then(t => {
+        assert.strictEqual(token, t);
+      });
+    });
+
+    test('_getOAuthConfig', () => {
+      const config = {};
+      fetch.returns(Promise.resolve({ok: true}));
+      sandbox.stub(auth, '_getResponseObject').returns(config);
+      return auth._getOAuthConfig().then(c => {
+        const [url, options] = fetch.lastCall.args;
+        assert.equal(url, '/accounts/self/oauthconfig');
+        assert.equal(options.credentials, 'same-origin');
+        assert.equal(options.headers.get('Accept'), 'application/json');
+        assert.strictEqual(c, config);
+      });
+    });
+
+    suite('faster gerrit cors', () => {
+      setup(() => {
+        window.FASTER_GERRIT_CORS = true;
+      });
+
+      teardown(() => {
+        delete window.FASTER_GERRIT_CORS;
+      });
+
+      test('PUT works', () => {
+        sandbox.stub(auth, '_getAccessToken').returns(Promise.resolve('foo'));
+        const originalOptions = {
+          method: 'PUT',
+          headers: new Headers({'Content-Type': 'mail/pigeon'}),
+        };
+        return auth.fetch('/url', originalOptions).then(() => {
+          assert.isTrue(auth._getAccessToken.called);
+          const [url, options] = fetch.lastCall.args;
+          assert.include(url, '$ct=mail%2Fpigeon');
+          assert.include(url, '$m=PUT');
+          assert.include(url, 'access_token=foo');
+          assert.equal(options.method, 'POST');
+          assert.equal(options.headers.get('Content-Type'), 'text/plain');
+        });
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gerrit-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gerrit-auth.js
new file mode 100644
index 0000000..b70790d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gerrit-auth.js
@@ -0,0 +1,49 @@
+// Copyright (C) 2017 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.
+(function(window) {
+  'use strict';
+
+  // Prevent redefinition.
+  if (window.GrGerritAuth) { return; }
+
+  function GrGerritAuth() {}
+
+  GrGerritAuth.prototype._getCookie = function(name) {
+    const key = name + '=';
+    let result = '';
+    document.cookie.split(';').some(c => {
+      c = c.trim();
+      if (c.startsWith(key)) {
+        result = c.substring(key.length);
+        return true;
+      }
+    });
+    return result;
+  };
+
+  GrGerritAuth.prototype.fetch = function(url, opt_options) {
+    const options = Object.assign({}, opt_options);
+    if (options.method && options.method !== 'GET') {
+      const token = this._getCookie('XSRF_TOKEN');
+      if (token) {
+        options.headers = options.headers || new Headers();
+        options.headers.append('X-Gerrit-Auth', token);
+      }
+    }
+    options.credentials = 'same-origin';
+    return fetch(url, options);
+  };
+
+  window.GrGerritAuth = GrGerritAuth;
+})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index da85fe0..6149abe 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -17,10 +17,14 @@
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<!-- NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 -->
 <script src="../../../bower_components/es6-promise/dist/es6-promise.min.js"></script>
 <script src="../../../bower_components/fetch/fetch.js"></script>
 
 <dom-module id="gr-rest-api-interface">
-  <script src="gr-rest-api-interface.js"></script>
+  <!-- NB: Order is important, because of namespaced classes. -->
+  <script src="gr-gerrit-auth.js"></script>
+  <script src="gr-gapi-auth.js"></script>
   <script src="gr-reviewer-updates-parser.js"></script>
+  <script src="gr-rest-api-interface.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index ae25a81..0481c17 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -19,6 +19,7 @@
     UNIFIED: 'UNIFIED_DIFF',
   };
   const JSON_PREFIX = ')]}\'';
+  const MAX_PROJECT_RESULTS = 25;
   const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 900;
   const PARENT_PATCH_NUM = 'PARENT';
 
@@ -26,6 +27,8 @@
     SEND_DIFF_DRAFT: 'sendDiffDraft',
   };
 
+  let auth = null;
+
   Polymer({
     is: 'gr-rest-api-interface',
 
@@ -61,20 +64,13 @@
       },
     },
 
-    fetchJSON(url, opt_errFn, opt_cancelCondition, opt_params,
-        opt_opts) {
-      opt_opts = opt_opts || {};
-      // Issue 5715, This can be reverted back once
-      // iOS 10.3 and mac os 10.12.4 has the fetch api fix.
-      const fetchOptions = {
-        credentials: 'same-origin',
-      };
-      if (opt_opts.headers !== undefined) {
-        fetchOptions['headers'] = opt_opts.headers;
-      }
+    created() {
+      auth = window.USE_GAPI_AUTH ? new GrGapiAuth() : new GrGerritAuth();
+    },
 
+    fetchJSON(url, opt_errFn, opt_cancelCondition, opt_params) {
       const urlWithParams = this._urlWithParams(url, opt_params);
-      return fetch(urlWithParams, fetchOptions).then(response => {
+      return auth.fetch(urlWithParams).then(response => {
         if (opt_cancelCondition && opt_cancelCondition()) {
           response.body.cancel();
           return;
@@ -148,6 +144,14 @@
           opt_ctx);
     },
 
+    createProject(config, opt_errFn, opt_ctx) {
+      if (!config.name) {
+        return '';
+      }
+      return this.send('PUT', `/projects/${config.name}`, config, opt_errFn,
+          opt_ctx);
+    },
+
     getVersion() {
       return this._fetchSharedCacheURL('/config/server/version');
     },
@@ -526,7 +530,11 @@
     },
 
     getSuggestedProjects(inputVal, opt_n, opt_errFn, opt_ctx) {
-      const params = {p: inputVal};
+      const params = {
+        m: inputVal,
+        n: MAX_PROJECT_RESULTS,
+        type: 'ALL',
+      };
       if (opt_n) { params.n = opt_n; }
       return this.fetchJSON('/projects/', opt_errFn, opt_ctx, params);
     },
@@ -703,22 +711,17 @@
     },
 
     send(method, url, opt_body, opt_errFn, opt_ctx, opt_contentType) {
-      const headers = new Headers({
-        'X-Gerrit-Auth': this._getCookie('XSRF_TOKEN'),
-      });
-      const options = {
-        method,
-        headers,
-        credentials: 'same-origin',
-      };
+      const options = {method};
       if (opt_body) {
-        headers.append('Content-Type', opt_contentType || 'application/json');
+        options.headers = new Headers({
+          'Content-Type': opt_contentType || 'application/json',
+        });
         if (typeof opt_body !== 'string') {
           opt_body = JSON.stringify(opt_body);
         }
         options.body = opt_body;
       }
-      return fetch(this.getBaseUrl() + url, options).then(response => {
+      return auth.fetch(this.getBaseUrl() + url, options).then(response => {
         if (!response.ok) {
           if (opt_errFn) {
             opt_errFn.call(opt_ctx || null, response);
@@ -888,21 +891,6 @@
       });
     },
 
-    _getCookie(name) {
-      const key = name + '=';
-      const cookies = document.cookie.split(';');
-      for (let i = 0; i < cookies.length; i++) {
-        let c = cookies[i];
-        while (c.charAt(0) == ' ') {
-          c = c.substring(1);
-        }
-        if (c.startsWith(key)) {
-          return c.substring(key.length, c.length);
-        }
-      }
-      return '';
-    },
-
     getCommitInfo(project, commit) {
       return this.fetchJSON(
           '/projects/' + encodeURIComponent(project) +
@@ -910,7 +898,7 @@
     },
 
     _fetchB64File(url) {
-      return fetch(this.getBaseUrl() + url, {credentials: 'same-origin'})
+      return auth.fetch(this.getBaseUrl() + url)
           .then(response => {
             if (!response.ok) { return Promise.reject(response.statusText); }
             const type = response.headers.get('X-FYI-Content-Type');
@@ -983,7 +971,7 @@
 
     setChangeTopic(changeNum, topic) {
       return this.send('PUT', '/changes/' + encodeURIComponent(changeNum) +
-          '/topic', {topic});
+          '/topic', {topic}).then(this.getResponseObject);
     },
 
     deleteAccountHttpPassword() {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 1450a5a..1f83589 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -658,5 +658,20 @@
       assert.isTrue(element._fetchSharedCacheURL.lastCall
           .calledWithExactly('/projects/?d&n=26&S=25&m=test'));
     });
+
+    test('gerrit auth is used by default', () => {
+      sandbox.stub(GrGerritAuth.prototype, 'fetch').returns(Promise.resolve());
+      element.fetchJSON('foo');
+      assert(GrGerritAuth.prototype.fetch.called);
+    });
+
+    test('gapi auth is enabled with USE_GAPI_AUTH', () => {
+      window.USE_GAPI_AUTH = true;
+      sandbox.stub(GrGapiAuth.prototype, 'fetch').returns(Promise.resolve());
+      element = fixture('basic');
+      element.fetchJSON('foo');
+      assert(GrGapiAuth.prototype.fetch.called);
+      delete window.USE_GAPI_AUTH;
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
index add4b6f..07454e5 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -165,6 +165,7 @@
       e.preventDefault();
       e.stopPropagation();
       this.$.emojiSuggestions.cursorUp();
+      this.$.textarea.textarea.focus();
     },
 
     _handleDownKey(e) {
@@ -172,6 +173,7 @@
       e.preventDefault();
       e.stopPropagation();
       this.$.emojiSuggestions.cursorDown();
+      this.$.textarea.textarea.focus();
     },
 
     _handleEnterByKey(e) {
diff --git a/polygerrit-ui/app/index.html b/polygerrit-ui/app/index.html
index db9a1c5..5e9cde8 100644
--- a/polygerrit-ui/app/index.html
+++ b/polygerrit-ui/app/index.html
@@ -21,11 +21,11 @@
 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
 
 <!--
-SourceCodePro fonts are used in styles/fonts.css
+RobotoMono fonts are used in styles/fonts.css
 @see https://github.com/w3c/preload/issues/32 regarding crossorigin
 -->
-<link rel="preload" href="/fonts/SourceCodePro-Regular.woff2" as="font" type="font/woff2" crossorigin>
-<link rel="preload" href="/fonts/SourceCodePro-Regular.woff" as="font" type="font/woff" crossorigin>
+<link rel="preload" href="/fonts/RobotoMono-Regular.woff2" as="font" type="font/woff2" crossorigin>
+<link rel="preload" href="/fonts/RobotoMono-Regular.woff" as="font" type="font/woff" crossorigin>
 <link rel="stylesheet" href="/styles/fonts.css">
 <link rel="stylesheet" href="/styles/main.css">
 <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html
index c998f03..4728fe6 100644
--- a/polygerrit-ui/app/styles/app-theme.html
+++ b/polygerrit-ui/app/styles/app-theme.html
@@ -30,7 +30,7 @@
   --view-background-color: #fff;
   --default-horizontal-margin: 1rem;
   --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
-  --monospace-font-family: 'Source Code Pro', Menlo, 'Lucida Console', Monaco, monospace;
+  --monospace-font-family: 'Roboto Mono', Menlo, 'Lucida Console', Monaco, monospace;
   --iron-overlay-backdrop: {
     transition: none;
   };
diff --git a/polygerrit-ui/app/styles/fonts.css b/polygerrit-ui/app/styles/fonts.css
index b5bf9ae..d339e16 100644
--- a/polygerrit-ui/app/styles/fonts.css
+++ b/polygerrit-ui/app/styles/fonts.css
@@ -1,20 +1,20 @@
 /* latin-ext */
 @font-face {
-  font-family: 'Source Code Pro';
+  font-family: 'Roboto Mono';
   font-style: normal;
   font-weight: 400;
-  src: local('Source Code Pro'), local('SourceCodePro-Regular'),
-       url(../fonts/SourceCodePro-Regular.woff2) format('woff2'),
-       url(../fonts/SourceCodePro-Regular.woff) format('woff');
+  src: local('Roboto Mono'), local('RobotoMono-Regular'),
+       url('../fonts/RobotoMono-Regular.woff2') format('woff2'),
+       url('../fonts/RobotoMono-Regular.woff') format('woff');
   unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
 }
 /* latin */
 @font-face {
-  font-family: 'Source Code Pro';
+  font-family: 'Roboto Mono';
   font-style: normal;
   font-weight: 400;
-  src: local('Source Code Pro'), local('SourceCodePro-Regular'),
-       url(../fonts/SourceCodePro-Regular.woff2) format('woff2'),
-       url(../fonts/SourceCodePro-Regular.woff) format('woff');
+  src: local('Roboto Mono'), local('RobotoMono-Regular'),
+       url('../fonts/RobotoMono-Regular.woff2') format('woff2'),
+       url('../fonts/RobotoMono-Regular.woff') format('woff');
   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
-}
+}
\ No newline at end of file
diff --git a/polygerrit-ui/app/test/common-test-setup.html b/polygerrit-ui/app/test/common-test-setup.html
index 00bd1e7..fb24af2d 100644
--- a/polygerrit-ui/app/test/common-test-setup.html
+++ b/polygerrit-ui/app/test/common-test-setup.html
@@ -15,5 +15,5 @@
 limitations under the License.
 -->
 
-<link rel="import"
-    href="../bower_components/iron-test-helpers/iron-test-helpers.html" />
+<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html" />
+<link rel="import" href="test-router.html" />
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 64af85b..3dd9329 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -30,6 +30,7 @@
     // This seemed to be flakey when it was farther down the list. Keep at the
     // beginning.
     'gr-app_test.html',
+    'admin/gr-admin-create-project/gr-admin-create-project_test.html',
     'admin/gr-admin-group-list/gr-admin-group-list_test.html',
     'admin/gr-admin-plugin-list/gr-admin-plugin-list_test.html',
     'admin/gr-admin-project/gr-admin-project_test.html',
@@ -61,6 +62,7 @@
     'core/gr-account-dropdown/gr-account-dropdown_test.html',
     'core/gr-error-manager/gr-error-manager_test.html',
     'core/gr-main-header/gr-main-header_test.html',
+    'core/gr-router/gr-router_test.html',
     'core/gr-reporting/gr-reporting_test.html',
     'core/gr-search-bar/gr-search-bar_test.html',
     'diff/gr-diff-builder/gr-diff-builder_test.html',
@@ -117,6 +119,7 @@
     'shared/gr-linked-chip/gr-linked-chip_test.html',
     'shared/gr-linked-text/gr-linked-text_test.html',
     'shared/gr-list-view/gr-list-view_test.html',
+    'shared/gr-rest-api-interface/gr-gapi-auth_test.html',
     'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
     'shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html',
     'shared/gr-select/gr-select_test.html',
diff --git a/polygerrit-ui/app/test/test-router.html b/polygerrit-ui/app/test/test-router.html
new file mode 100644
index 0000000..37a20c4
--- /dev/null
+++ b/polygerrit-ui/app/test/test-router.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 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.
+-->
+
+<link rel="import" href="../elements/core/gr-navigation/gr-navigation.html">
+<script>
+  Gerrit.Nav.setup(url => { /* noop */ }, params => '');
+</script>