Merge "Move capability evaluation to DefaultPermissionBackend"
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 7440634..8cddce2 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -111,6 +111,10 @@
 * `reviewer_suggestion/query_groups`: Latency for querying groups for reviewer
 suggestion.
 
+=== Repo Sequences
+
+* `sequence/next_id_latency`: Latency of requesting IDs from repo sequences.
+
 === Replication Plugin
 
 * `plugins/replication/replication_latency`: Time spent pushing to remote
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 16e61f6..d64054a 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2121,7 +2121,7 @@
 ----
 
 [[set-work-in-pogress]]
-== Set Work-In-Progress
+=== Set Work-In-Progress
 --
 'POST /changes/link:#change-id[\{change-id\}]/wip'
 --
@@ -2149,7 +2149,7 @@
 ----
 
 [[set-ready-for-review]]
-== Set Ready-For-Review
+=== Set Ready-For-Review
 --
 'POST /changes/link:#change-id[\{change-id\}]/ready'
 --
@@ -2807,6 +2807,8 @@
 Suggest the reviewers for a given query `q` and result limit `n`. If result
 limit is not passed, then the default 10 is used.
 
+Groups can be excluded from the results by specifying 'e=f'.
+
 As result a list of link:#suggested-reviewer-info[SuggestedReviewerInfo] entries is returned.
 
 .Request
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index cb72519b..434a7a4 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -1130,7 +1130,7 @@
 
 - [[line-wrapping]]`Line Wrapping`:
 +
-Controls weather to enable line wrapping or not.
+Controls whether to enable line wrapping or not.
 +
 If `false` is selected then line wrapping is disabled.
 This is the default option.
diff --git a/WORKSPACE b/WORKSPACE
index 63d10c6..2db3a6c 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -883,13 +883,13 @@
 maven_jar(
     name = "codemirror_minified",
     artifact = "org.webjars.npm:codemirror-minified:" + CM_VERSION,
-    sha1 = "a62e6dc43acc09df962edeb8013c987da7739e7a",
+    sha1 = "7464d1bf59a36b081981b855a51839bf3a1f302d",
 )
 
 maven_jar(
     name = "codemirror_original",
     artifact = "org.webjars.npm:codemirror:" + CM_VERSION,
-    sha1 = "7a5ae457aa3bc0023d21bc75793bac86b83b0c51",
+    sha1 = "8bcf4d541eba5c1c6916cb449fe4baf73d2ebd6f",
 )
 
 maven_jar(
@@ -1101,6 +1101,13 @@
 )
 
 bower_archive(
+    name = "polymer-resin",
+    package = "polymer/polymer-resin",
+    sha1 = "d759c8c09054a7ec04608a6cb586801c904f79a2",
+    version = "1.2.6-beta",
+)
+
+bower_archive(
     name = "promise-polyfill",
     package = "polymerlabs/promise-polyfill",
     sha1 = "a3b598c06cbd7f441402e666ff748326030905d6",
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 87105f8..918b7a6 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
@@ -19,7 +19,6 @@
 import static java.nio.charset.StandardCharsets.US_ASCII;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
@@ -109,10 +108,15 @@
       }
       externalIdsUpdate.create().insert(extIds);
 
-      Account a = new Account(id, TimeUtil.nowTs());
-      a.setFullName(fullName);
-      a.setPreferredEmail(email);
-      accountsUpdate.create().insert(db, a);
+      accountsUpdate
+          .create()
+          .insert(
+              db,
+              id,
+              a -> {
+                a.setFullName(fullName);
+                a.setPreferredEmail(email);
+              });
 
       if (groups != null) {
         for (String n : groups) {
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 2b13df0..72d646e 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
@@ -46,6 +46,7 @@
 import java.net.URI;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.BrokenBarrierException;
@@ -54,6 +55,7 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
 import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
 import org.eclipse.jgit.lib.Config;
@@ -233,7 +235,7 @@
     if (!desc.memory()) {
       init(desc, baseConfig, site);
     }
-    return start(desc, baseConfig, site);
+    return start(desc, baseConfig, site, null);
   }
 
   /**
@@ -244,10 +246,18 @@
    * @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.
+   * @param testSysModule optional additional module to add to the system injector.
+   * @param additionalArgs additional command-line arguments for the daemon program; only allowed if
+   *     the test is not in-memory.
    * @return started server.
    * @throws Exception
    */
-  public static GerritServer start(Description desc, Config baseConfig, Path site)
+  public static GerritServer start(
+      Description desc,
+      Config baseConfig,
+      Path site,
+      @Nullable Module testSysModule,
+      String... additionalArgs)
       throws Exception {
     checkArgument(site != null, "site is required (even for in-memory server");
     desc.checkValidAnnotations();
@@ -264,12 +274,14 @@
             },
             site);
     daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
+    daemon.setAdditionalSysModuleForTesting(testSysModule);
     daemon.setEnableSshd(desc.useSsh());
 
     if (desc.memory()) {
+      checkArgument(additionalArgs.length == 0, "cannot pass args to in-memory server");
       return startInMemory(desc, baseConfig, daemon);
     }
-    return startOnDisk(desc, site, daemon, serverStarted);
+    return startOnDisk(desc, site, daemon, serverStarted, additionalArgs);
   }
 
   private static GerritServer startInMemory(Description desc, Config baseConfig, Daemon daemon)
@@ -291,18 +303,25 @@
   }
 
   private static GerritServer startOnDisk(
-      Description desc, Path site, Daemon daemon, CyclicBarrier serverStarted) throws Exception {
+      Description desc,
+      Path site,
+      Daemon daemon,
+      CyclicBarrier serverStarted,
+      String[] additionalArgs)
+      throws Exception {
     checkNotNull(site);
     ExecutorService daemonService = Executors.newSingleThreadExecutor();
+    String[] args =
+        Stream.concat(
+                Stream.of(
+                    "-d", site.toString(), "--headless", "--console-log", "--show-stack-trace"),
+                Arrays.stream(additionalArgs))
+            .toArray(String[]::new);
     @SuppressWarnings("unused")
     Future<?> possiblyIgnoredError =
         daemonService.submit(
             () -> {
-              int rc =
-                  daemon.main(
-                      new String[] {
-                        "-d", site.toString(), "--headless", "--console-log", "--show-stack-trace",
-                      });
+              int rc = daemon.main(args);
               if (rc != 0) {
                 System.err.println("Failed to start Gerrit daemon");
                 serverStarted.reset();
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index be34ba6..93273c4 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.fail;
 
 import com.google.common.collect.Streams;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -29,6 +30,7 @@
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.inject.Injector;
+import com.google.inject.Module;
 import com.google.inject.Provider;
 import java.util.Arrays;
 import org.eclipse.jgit.lib.Config;
@@ -113,19 +115,26 @@
   }
 
   protected ServerContext startServer() throws Exception {
-    return new ServerContext(startImpl());
+    return startServer(null);
+  }
+
+  protected ServerContext startServer(@Nullable Module testSysModule, String... additionalArgs)
+      throws Exception {
+    return new ServerContext(startImpl(testSysModule, additionalArgs));
   }
 
   protected void assertServerStartupFails() throws Exception {
-    try (GerritServer server = startImpl()) {
+    try (GerritServer server = startImpl(null)) {
       fail("expected server startup to fail");
     } catch (GerritServer.StartupException e) {
       // Expected.
     }
   }
 
-  private GerritServer startImpl() throws Exception {
-    return GerritServer.start(serverDesc, baseConfig, sitePaths.site_path);
+  private GerritServer startImpl(@Nullable Module testSysModule, String... additionalArgs)
+      throws Exception {
+    return GerritServer.start(
+        serverDesc, baseConfig, sitePaths.site_path, testSysModule, additionalArgs);
   }
 
   protected static void runGerrit(String... args) throws Exception {
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 5fbf8ca..50bc0be 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
@@ -32,6 +32,7 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toSet;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 import static org.junit.Assert.fail;
 
 import com.google.common.collect.FluentIterable;
@@ -74,6 +75,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountByEmailCache;
+import com.google.gerrit.server.account.AccountConfig;
 import com.google.gerrit.server.account.WatchConfig;
 import com.google.gerrit.server.account.WatchConfig.NotifyType;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -91,7 +93,6 @@
 import java.io.ByteArrayOutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -106,6 +107,7 @@
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
@@ -114,6 +116,7 @@
 import org.eclipse.jgit.transport.PushCertificateIdent;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.treewalk.TreeWalk;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -162,8 +165,8 @@
     externalIdsUpdate = externalIdsUpdateFactory.create();
 
     savedExternalIds = new ArrayList<>();
-    savedExternalIds.addAll(getExternalIds(admin));
-    savedExternalIds.addAll(getExternalIds(user));
+    savedExternalIds.addAll(externalIds.byAccount(admin.id));
+    savedExternalIds.addAll(externalIds.byAccount(user.id));
   }
 
   @After
@@ -172,8 +175,8 @@
       // savedExternalIds is null when we don't run SSH tests and the assume in
       // @Before in AbstractDaemonTest prevents this class' @Before method from
       // being executed.
-      externalIdsUpdate.delete(getExternalIds(admin));
-      externalIdsUpdate.delete(getExternalIds(user));
+      externalIdsUpdate.delete(externalIds.byAccount(admin.id));
+      externalIdsUpdate.delete(externalIds.byAccount(user.id));
       externalIdsUpdate.insert(savedExternalIds);
     }
   }
@@ -190,10 +193,6 @@
     }
   }
 
-  private Collection<ExternalId> getExternalIds(TestAccount account) throws Exception {
-    return accountCache.get(account.getId()).getExternalIds();
-  }
-
   @After
   public void deleteGpgKeys() throws Exception {
     String ref = REFS_GPG_KEYS;
@@ -220,6 +219,67 @@
     create(3); // account creation + external ID creation + adding SSH keys
   }
 
+  private void create(int expectedAccountReindexCalls) throws Exception {
+    String name = "foo";
+    TestAccount foo = accountCreator.create(name);
+    AccountInfo info = gApi.accounts().id(foo.id.get()).get();
+    assertThat(info.username).isEqualTo(name);
+    assertThat(info.name).isEqualTo(name);
+    accountIndexedCounter.assertReindexOf(foo, expectedAccountReindexCalls);
+
+    // check user branch
+    try (Repository repo = repoManager.openRepository(allUsers);
+        RevWalk rw = new RevWalk(repo);
+        ObjectReader or = repo.newObjectReader()) {
+      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);
+
+      // Check the 'account.config' file.
+      try (TreeWalk tw = TreeWalk.forPath(or, AccountConfig.ACCOUNT_CONFIG, c.getTree())) {
+        assertThat(tw).isNotNull();
+        Config cfg = new Config();
+        cfg.fromText(new String(or.open(tw.getObjectId(0), OBJ_BLOB).getBytes(), UTF_8));
+        assertThat(cfg.getString(AccountConfig.ACCOUNT, null, AccountConfig.KEY_FULL_NAME))
+            .isEqualTo(name);
+      }
+    }
+  }
+
+  @Test
+  public void createAnonymousCoward() throws Exception {
+    TestAccount anonymousCoward = accountCreator.create();
+    accountIndexedCounter.assertReindexOf(anonymousCoward);
+
+    // check user branch
+    try (Repository repo = repoManager.openRepository(allUsers);
+        RevWalk rw = new RevWalk(repo);
+        ObjectReader or = repo.newObjectReader()) {
+      Ref ref = repo.exactRef(RefNames.refsUsers(anonymousCoward.getId()));
+      assertThat(ref).isNotNull();
+      RevCommit c = rw.parseCommit(ref.getObjectId());
+      long timestampDiffMs =
+          Math.abs(
+              c.getCommitTime() * 1000L
+                  - accountCache
+                      .get(anonymousCoward.getId())
+                      .getAccount()
+                      .getRegisteredOn()
+                      .getTime());
+      assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS);
+
+      // No account properties were set, hence an 'account.config' file was not created.
+      try (TreeWalk tw = TreeWalk.forPath(or, AccountConfig.ACCOUNT_CONFIG, c.getTree())) {
+        assertThat(tw).isNull();
+      }
+    }
+  }
+
   @Test
   public void get() throws Exception {
     AccountInfo info = gApi.accounts().id("admin").get();
@@ -694,6 +754,36 @@
   }
 
   @Test
+  public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmit() throws Exception {
+    String userRefName = RefNames.refsUsers(admin.id);
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    fetch(allUsersRepo, userRefName + ":userRef");
+    allUsersRepo.reset("userRef");
+
+    Config ac = getAccountConfig(allUsersRepo);
+    ac.setString(AccountConfig.ACCOUNT, null, AccountConfig.KEY_STATUS, "OOO");
+
+    PushOneCommit.Result r =
+        pushFactory
+            .create(
+                db,
+                admin.getIdent(),
+                allUsersRepo,
+                "Update account config",
+                AccountConfig.ACCOUNT_CONFIG,
+                ac.toText())
+            .to(MagicBranch.NEW_CHANGE + userRefName);
+    r.assertOkStatus();
+    accountIndexedCounter.assertNoReindex();
+    assertThat(r.getChange().change().getDest().get()).isEqualTo(userRefName);
+    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage(
+        String.format("update of %s not allowed", AccountConfig.ACCOUNT_CONFIG));
+    gApi.changes().id(r.getChangeId()).current().submit();
+  }
+
+  @Test
   public void pushWatchConfigToUserBranch() throws Exception {
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
@@ -735,6 +825,28 @@
   }
 
   @Test
+  public void pushAccountConfigToUserBranchIsRejected() throws Exception {
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
+    allUsersRepo.reset("userRef");
+
+    Config ac = getAccountConfig(allUsersRepo);
+    ac.setString(AccountConfig.ACCOUNT, null, AccountConfig.KEY_STATUS, "OOO");
+
+    PushOneCommit.Result r =
+        pushFactory
+            .create(
+                db,
+                admin.getIdent(),
+                allUsersRepo,
+                "Update account config",
+                AccountConfig.ACCOUNT_CONFIG,
+                ac.toText())
+            .to(RefNames.REFS_USERS_SELF);
+    r.assertErrorStatus("account update not allowed");
+  }
+
+  @Test
   @Sandboxed
   public void cannotCreateUserBranch() throws Exception {
     grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
@@ -1071,26 +1183,6 @@
     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) {
@@ -1212,6 +1304,26 @@
     assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.getId());
   }
 
+  private Config getAccountConfig(TestRepository<?> allUsersRepo) throws Exception {
+    Config ac = new Config();
+    try (TreeWalk tw =
+        TreeWalk.forPath(
+            allUsersRepo.getRepository(),
+            AccountConfig.ACCOUNT_CONFIG,
+            getHead(allUsersRepo.getRepository()).getTree())) {
+      assertThat(tw).isNotNull();
+      ac.fromText(
+          new String(
+              allUsersRepo
+                  .getRevWalk()
+                  .getObjectReader()
+                  .open(tw.getObjectId(0), OBJ_BLOB)
+                  .getBytes(),
+              UTF_8));
+    }
+    return ac;
+  }
+
   private static class AccountIndexedCounter implements AccountIndexedListener {
     private final AtomicLongMap<Integer> countsByAccount = AtomicLongMap.create();
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 8607c02..275b06a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -2926,6 +2926,7 @@
       RevisionApi rApi = gApi.changes().id(r.getChangeId()).current();
       assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt");
       assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage);
+      assertThat(rApi.description()).isEqualTo("Edit commit message");
     }
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD
index f405e19..3f45711c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD
@@ -4,4 +4,12 @@
     srcs = glob(["*IT.java"]),
     group = "pgm",
     labels = ["pgm"],
+    deps = [":util"],
+)
+
+java_library(
+    name = "util",
+    testonly = 1,
+    srcs = ["IndexUpgradeController.java"],
+    deps = ["//gerrit-acceptance-tests:lib"],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/IndexUpgradeController.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/IndexUpgradeController.java
new file mode 100644
index 0000000..9cdcb40
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/IndexUpgradeController.java
@@ -0,0 +1,121 @@
+// 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.pgm;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.index.OnlineUpgradeListener;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+class IndexUpgradeController implements OnlineUpgradeListener {
+  @AutoValue
+  abstract static class UpgradeAttempt {
+    static UpgradeAttempt create(String name, int oldVersion, int newVersion) {
+      return new AutoValue_IndexUpgradeController_UpgradeAttempt(name, oldVersion, newVersion);
+    }
+
+    abstract String name();
+
+    abstract int oldVersion();
+
+    abstract int newVersion();
+  }
+
+  private final int numExpected;
+  private final CountDownLatch readyToStart;
+  private final CountDownLatch started;
+  private final CountDownLatch finished;
+
+  private final List<UpgradeAttempt> startedAttempts;
+  private final List<UpgradeAttempt> succeededAttempts;
+  private final List<UpgradeAttempt> failedAttempts;
+
+  IndexUpgradeController(int numExpected) {
+    this.numExpected = numExpected;
+    readyToStart = new CountDownLatch(1);
+    started = new CountDownLatch(numExpected);
+    finished = new CountDownLatch(numExpected);
+    startedAttempts = new ArrayList<>();
+    succeededAttempts = new ArrayList<>();
+    failedAttempts = new ArrayList<>();
+  }
+
+  Module module() {
+    return new AbstractModule() {
+      @Override
+      public void configure() {
+        DynamicSet.bind(binder(), OnlineUpgradeListener.class)
+            .toInstance(IndexUpgradeController.this);
+      }
+    };
+  }
+
+  @Override
+  public synchronized void onStart(String name, int oldVersion, int newVersion) {
+    UpgradeAttempt a = UpgradeAttempt.create(name, oldVersion, newVersion);
+    try {
+      readyToStart.await();
+    } catch (InterruptedException e) {
+      throw new AssertionError("interrupted waiting to start " + a, e);
+    }
+    checkState(
+        started.getCount() > 0, "already started %s upgrades, can't start %s", numExpected, a);
+    startedAttempts.add(a);
+    started.countDown();
+  }
+
+  @Override
+  public synchronized void onSuccess(String name, int oldVersion, int newVersion) {
+    finish(UpgradeAttempt.create(name, oldVersion, newVersion), succeededAttempts);
+  }
+
+  @Override
+  public synchronized void onFailure(String name, int oldVersion, int newVersion) {
+    finish(UpgradeAttempt.create(name, oldVersion, newVersion), failedAttempts);
+  }
+
+  private synchronized void finish(UpgradeAttempt a, List<UpgradeAttempt> out) {
+    checkState(readyToStart.getCount() == 0, "shouldn't be finishing upgrade before starting");
+    checkState(
+        finished.getCount() > 0, "already finished %s upgrades, can't finish %s", numExpected, a);
+    out.add(a);
+    finished.countDown();
+  }
+
+  void runUpgrades() throws Exception {
+    readyToStart.countDown();
+    started.await();
+    finished.await();
+  }
+
+  synchronized ImmutableList<UpgradeAttempt> getStartedAttempts() {
+    return ImmutableList.copyOf(startedAttempts);
+  }
+
+  synchronized ImmutableList<UpgradeAttempt> getSucceededAttempts() {
+    return ImmutableList.copyOf(succeededAttempts);
+  }
+
+  synchronized ImmutableList<UpgradeAttempt> getFailedAttempts() {
+    return ImmutableList.copyOf(failedAttempts);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
index 2dcaf7d..94250e6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
@@ -14,38 +14,48 @@
 
 package com.google.gerrit.acceptance.pgm;
 
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.common.io.MoreFiles;
 import com.google.common.io.RecursiveDeleteOption;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.acceptance.pgm.IndexUpgradeController.UpgradeAttempt;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.index.GerritIndexStatus;
+import com.google.gerrit.server.index.change.ChangeIndexCollection;
+import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.inject.Provider;
 import java.nio.file.Files;
+import java.util.Set;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
 import org.junit.Test;
 
 @NoHttpd
 public class ReindexIT extends StandaloneSiteTest {
+  private static final String CHANGES = ChangeSchemaDefinitions.NAME;
+
+  private Project.NameKey project;
+  private String changeId;
+
   @Test
   public void reindexFromScratch() throws Exception {
-    Project.NameKey project = new Project.NameKey("project");
-    String changeId;
-    try (ServerContext ctx = startServer()) {
-      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
-      gApi.projects().create("project");
-
-      ChangeInput in = new ChangeInput(project.get(), "master", "Test change");
-      in.newBranch = true;
-      changeId = gApi.changes().create(in).info().changeId;
-    }
+    setUpChange();
 
     MoreFiles.deleteRecursively(sitePaths.index_dir, RecursiveDeleteOption.ALLOW_INSECURE);
     Files.createDirectory(sitePaths.index_dir);
     assertServerStartupFails();
 
     runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
+    assertReady(ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion());
 
     try (ServerContext ctx = startServer()) {
       GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
@@ -53,4 +63,105 @@
           .containsExactly(changeId);
     }
   }
+
+  @Test
+  public void onlineUpgradeChanges() throws Exception {
+    int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion();
+    int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
+
+    // Before storing any changes, switch back to the previous version.
+    GerritIndexStatus status = new GerritIndexStatus(sitePaths);
+    status.setReady(CHANGES, currVersion, false);
+    status.setReady(CHANGES, prevVersion, true);
+    status.save();
+    assertReady(prevVersion);
+
+    setOnlineUpgradeConfig(false);
+    setUpChange();
+    setOnlineUpgradeConfig(true);
+
+    IndexUpgradeController u = new IndexUpgradeController(1);
+    try (ServerContext ctx = startServer(u.module())) {
+      assertSearchVersion(ctx, prevVersion);
+      assertWriteVersions(ctx, prevVersion, currVersion);
+
+      // Updating and searching old schema version works.
+      Provider<InternalChangeQuery> queryProvider =
+          ctx.getInjector().getProvider(InternalChangeQuery.class);
+      assertThat(queryProvider.get().byKey(new Change.Key(changeId))).hasSize(1);
+      assertThat(queryProvider.get().byTopicOpen("topic1")).isEmpty();
+
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      gApi.changes().id(changeId).topic("topic1");
+      assertThat(queryProvider.get().byTopicOpen("topic1")).hasSize(1);
+
+      u.runUpgrades();
+      assertThat(u.getStartedAttempts())
+          .containsExactly(UpgradeAttempt.create(CHANGES, prevVersion, currVersion));
+      assertThat(u.getSucceededAttempts())
+          .containsExactly(UpgradeAttempt.create(CHANGES, prevVersion, currVersion));
+      assertThat(u.getFailedAttempts()).isEmpty();
+
+      assertReady(currVersion);
+      assertSearchVersion(ctx, currVersion);
+      assertWriteVersions(ctx, currVersion);
+
+      // Updating and searching new schema version works.
+      assertThat(queryProvider.get().byTopicOpen("topic1")).hasSize(1);
+      assertThat(queryProvider.get().byTopicOpen("topic2")).isEmpty();
+      gApi.changes().id(changeId).topic("topic2");
+      assertThat(queryProvider.get().byTopicOpen("topic1")).isEmpty();
+      assertThat(queryProvider.get().byTopicOpen("topic2")).hasSize(1);
+    }
+  }
+
+  private void setUpChange() throws Exception {
+    project = new Project.NameKey("project");
+    try (ServerContext ctx = startServer()) {
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      gApi.projects().create(project.get());
+
+      ChangeInput in = new ChangeInput(project.get(), "master", "Test change");
+      in.newBranch = true;
+      changeId = gApi.changes().create(in).info().changeId;
+    }
+  }
+
+  private void setOnlineUpgradeConfig(boolean enable) throws Exception {
+    FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
+    cfg.load();
+    cfg.setBoolean("index", null, "onlineUpgrade", enable);
+    cfg.save();
+  }
+
+  private void assertSearchVersion(ServerContext ctx, int expected) {
+    assertThat(
+            ctx.getInjector()
+                .getInstance(ChangeIndexCollection.class)
+                .getSearchIndex()
+                .getSchema()
+                .getVersion())
+        .named("search version")
+        .isEqualTo(expected);
+  }
+
+  private void assertWriteVersions(ServerContext ctx, Integer... expected) {
+    assertThat(
+            ctx.getInjector()
+                .getInstance(ChangeIndexCollection.class)
+                .getWriteIndexes()
+                .stream()
+                .map(i -> i.getSchema().getVersion()))
+        .named("write versions")
+        .containsExactlyElementsIn(ImmutableSet.copyOf(expected));
+  }
+
+  private void assertReady(int expectedReady) throws Exception {
+    Set<Integer> allVersions = ChangeSchemaDefinitions.INSTANCE.getSchemas().keySet();
+    GerritIndexStatus status = new GerritIndexStatus(sitePaths);
+    assertThat(
+            allVersions.stream().collect(toImmutableMap(v -> v, v -> status.getReady(CHANGES, v))))
+        .named("ready state for index versions")
+        .isEqualTo(allVersions.stream().collect(toImmutableMap(v -> v, v -> v == expectedReady)));
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/OfflineNoteDbMigrationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
similarity index 79%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/OfflineNoteDbMigrationIT.java
rename to gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
index 4b3e703..6ba5b07 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/OfflineNoteDbMigrationIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.GerritIndexStatus;
+import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
 import com.google.gerrit.server.notedb.ConfigNotesMigration;
 import com.google.gerrit.server.notedb.NoteDbChangeState;
@@ -49,14 +50,15 @@
 import org.junit.Test;
 
 /**
- * Tests for offline {@code migrate-to-note-db} program.
+ * Tests for NoteDb migrations where the entry point is through a program, {@code
+ * migrate-to-note-db} or {@code daemon}.
  *
  * <p><strong>Note:</strong> These tests are very slow due to the repeated daemon startup. Prefer
  * adding tests to {@link com.google.gerrit.acceptance.server.notedb.OnlineNoteDbMigrationIT} if
  * possible.
  */
 @NoHttpd
-public class OfflineNoteDbMigrationIT extends StandaloneSiteTest {
+public class StandaloneNoteDbMigrationIT extends StandaloneSiteTest {
   private StoredConfig gerritConfig;
 
   private Project.NameKey project;
@@ -145,6 +147,36 @@
     assertThat(status.getReady(ChangeSchemaDefinitions.NAME, version)).isTrue();
   }
 
+  @Test
+  public void onlineMigrationViaDaemon() throws Exception {
+    assertNotesMigrationState(NotesMigrationState.REVIEW_DB);
+    int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion();
+    int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
+
+    // Before storing any changes, switch back to the previous version.
+    GerritIndexStatus status = new GerritIndexStatus(sitePaths);
+    status.setReady(ChangeSchemaDefinitions.NAME, currVersion, false);
+    status.setReady(ChangeSchemaDefinitions.NAME, prevVersion, true);
+    status.save();
+
+    setOnlineUpgradeConfig(false);
+    setUpOneChange();
+    setOnlineUpgradeConfig(true);
+
+    IndexUpgradeController u = new IndexUpgradeController(1);
+    try (ServerContext ctx = startServer(u.module(), "--migrate-to-note-db", "true")) {
+      ChangeIndexCollection indexes = ctx.getInjector().getInstance(ChangeIndexCollection.class);
+      assertThat(indexes.getSearchIndex().getSchema().getVersion()).isEqualTo(prevVersion);
+
+      // Index schema upgrades happen after NoteDb migration, so waiting for those to complete
+      // should be sufficient.
+      u.runUpgrades();
+
+      assertThat(indexes.getSearchIndex().getSchema().getVersion()).isEqualTo(currVersion);
+      assertNotesMigrationState(NotesMigrationState.NOTE_DB_UNFUSED);
+    }
+  }
+
   private void setUpOneChange() throws Exception {
     project = new Project.NameKey("project");
     try (ServerContext ctx = startServer()) {
@@ -175,4 +207,10 @@
         .getInstance(Key.get(new TypeLiteral<SchemaFactory<ReviewDb>>() {}, ReviewDbFactory.class))
         .open();
   }
+
+  private void setOnlineUpgradeConfig(boolean enable) throws Exception {
+    gerritConfig.load();
+    gerritConfig.setBoolean("index", null, "onlineUpgrade", enable);
+    gerritConfig.save();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 92cf30e..2c6b32f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -28,7 +30,6 @@
 import org.junit.Before;
 import org.junit.Test;
 
-@NoHttpd
 public class DeleteBranchIT extends AbstractDaemonTest {
 
   private Branch.NameKey branch;
@@ -86,6 +87,15 @@
     assertDeleteSucceeds();
   }
 
+  @Test
+  public void deleteBranchByRestWithoutRefsHeadsPrefix() throws Exception {
+    grantDelete();
+    String ref = branch.getShortName();
+    assertThat(ref).doesNotMatch(R_HEADS);
+    RestResponse r = userRestSession.delete("/projects/" + project.get() + "/branches/" + ref);
+    r.assertNoContent();
+  }
+
   private void blockForcePush() throws Exception {
     block("refs/heads/*", Permission.PUSH, ANONYMOUS_USERS).setForce(true);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
index 1ca6c15..dc18a58 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
@@ -16,6 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.RefAssert.assertRefNames;
+import static java.util.stream.Collectors.toList;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
 import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableList;
@@ -37,7 +39,7 @@
 @NoHttpd
 public class DeleteBranchesIT extends AbstractDaemonTest {
   private static final ImmutableList<String> BRANCHES =
-      ImmutableList.of("refs/heads/test-1", "refs/heads/test-2", "refs/heads/test-3");
+      ImmutableList.of("refs/heads/test-1", "refs/heads/test-2", "test-3");
 
   @Before
   public void setUp() throws Exception {
@@ -138,7 +140,7 @@
     for (String branch : branches) {
       message
           .append("Cannot delete ")
-          .append(branch)
+          .append(prefixRef(branch))
           .append(": it doesn't exist or you do not have permission ")
           .append("to delete it\n");
     }
@@ -156,17 +158,22 @@
   private void assertRefUpdatedEvents(HashMap<String, RevCommit> revisions) throws Exception {
     for (String branch : revisions.keySet()) {
       RevCommit revision = revisions.get(branch);
-      eventRecorder.assertRefUpdatedEvents(project.get(), branch, null, revision, revision, null);
+      eventRecorder.assertRefUpdatedEvents(
+          project.get(), prefixRef(branch), null, revision, revision, null);
     }
   }
 
+  private String prefixRef(String ref) {
+    return ref.startsWith(R_HEADS) ? ref : R_HEADS + ref;
+  }
+
   private ProjectApi project() throws Exception {
     return gApi.projects().name(project.get());
   }
 
   private void assertBranches(List<String> branches) throws Exception {
     List<String> expected = Lists.newArrayList("HEAD", RefNames.REFS_CONFIG, "refs/heads/master");
-    expected.addAll(branches);
+    expected.addAll(branches.stream().map(b -> prefixRef(b)).collect(toList()));
     assertRefNames(expected, project().branches().get());
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
index 40fe4ae..e37071e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
@@ -16,9 +16,10 @@
 
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.TagApi;
 import com.google.gerrit.extensions.api.projects.TagInput;
@@ -27,7 +28,6 @@
 import org.junit.Before;
 import org.junit.Test;
 
-@NoHttpd
 public class DeleteTagIT extends AbstractDaemonTest {
   private final String TAG = "refs/tags/test";
 
@@ -82,6 +82,14 @@
     assertDeleteSucceeds();
   }
 
+  @Test
+  public void deleteTagByRestWithoutRefsTagsPrefix() throws Exception {
+    grantDelete();
+    String ref = TAG.substring(R_TAGS.length());
+    RestResponse r = userRestSession.delete("/projects/" + project.get() + "/tags/" + ref);
+    r.assertNoContent();
+  }
+
   private void blockForcePush() throws Exception {
     block("refs/tags/*", Permission.PUSH, ANONYMOUS_USERS).setForce(true);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
index 69cd29a..8f24609 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.toList;
+import static org.eclipse.jgit.lib.Constants.R_TAGS;
 import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableList;
@@ -36,7 +38,7 @@
 @NoHttpd
 public class DeleteTagsIT extends AbstractDaemonTest {
   private static final ImmutableList<String> TAGS =
-      ImmutableList.of("refs/tags/test-1", "refs/tags/test-2", "refs/tags/test-3");
+      ImmutableList.of("refs/tags/test-1", "refs/tags/test-2", "refs/tags/test-3", "test-4");
 
   @Before
   public void setUp() throws Exception {
@@ -112,7 +114,7 @@
     for (String tag : tags) {
       message
           .append("Cannot delete ")
-          .append(tag)
+          .append(prefixRef(tag))
           .append(": it doesn't exist or you do not have permission ")
           .append("to delete it\n");
     }
@@ -122,18 +124,24 @@
   private HashMap<String, RevCommit> initialRevisions(List<String> tags) throws Exception {
     HashMap<String, RevCommit> result = new HashMap<>();
     for (String tag : tags) {
-      result.put(tag, getRemoteHead(project, tag));
+      String ref = prefixRef(tag);
+      result.put(ref, getRemoteHead(project, ref));
     }
     return result;
   }
 
   private void assertRefUpdatedEvents(HashMap<String, RevCommit> revisions) throws Exception {
     for (String tag : revisions.keySet()) {
-      RevCommit revision = revisions.get(tag);
-      eventRecorder.assertRefUpdatedEvents(project.get(), tag, null, revision, revision, null);
+      RevCommit revision = revisions.get(prefixRef(tag));
+      eventRecorder.assertRefUpdatedEvents(
+          project.get(), prefixRef(tag), null, revision, revision, null);
     }
   }
 
+  private String prefixRef(String ref) {
+    return ref.startsWith(R_TAGS) ? ref : R_TAGS + ref;
+  }
+
   private ProjectApi project() throws Exception {
     return gApi.projects().name(project.get());
   }
@@ -141,7 +149,9 @@
   private void assertTags(List<String> expected) throws Exception {
     List<TagInfo> actualTags = project().tags().get();
     Iterable<String> actualNames = Iterables.transform(actualTags, b -> b.ref);
-    assertThat(actualNames).containsExactlyElementsIn(expected).inOrder();
+    assertThat(actualNames)
+        .containsExactlyElementsIn(expected.stream().map(t -> prefixRef(t)).collect(toList()))
+        .inOrder();
   }
 
   private void assertTagsDeleted() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
index bb72a58..29b8ee7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
@@ -104,11 +104,15 @@
     assertMigrationException(
         "Cannot set both changes and projects", b -> b.setChanges(cs).setProjects(ps), m -> {});
     assertMigrationException(
-        "Cannot set changes or projects during auto-migration",
+        "Auto-migration cannot be used with trial mode",
+        b -> b.setAutoMigrate(true).setTrialMode(true),
+        m -> {});
+    assertMigrationException(
+        "Cannot set changes or projects during full migration",
         b -> b.setChanges(cs),
         NoteDbMigrator::migrate);
     assertMigrationException(
-        "Cannot set changes or projects during auto-migration",
+        "Cannot set changes or projects during full migration",
         b -> b.setProjects(ps),
         NoteDbMigrator::migrate);
 
@@ -294,49 +298,85 @@
   }
 
   @Test
-  public void fullMigration() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
+  public void fullMigrationSameThread() throws Exception {
+    testFullMigration(1);
+  }
 
-    migrate(b -> b);
+  @Test
+  public void fullMigrationMultipleThreads() throws Exception {
+    testFullMigration(2);
+  }
+
+  private void testFullMigration(int threads) throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = createChange();
+    Change.Id id1 = r1.getChange().getId();
+    Change.Id id2 = r2.getChange().getId();
+
+    migrate(b -> b.setThreads(threads));
     assertNotesMigrationState(NOTE_DB_UNFUSED);
 
-    assertThat(sequences.nextChangeId()).isEqualTo(502);
+    assertThat(sequences.nextChangeId()).isEqualTo(503);
 
-    ObjectId oldMetaId;
-    int rowVersion;
+    ObjectId oldMetaId = null;
+    int rowVersion = 0;
     try (ReviewDb db = schemaFactory.open();
         Repository repo = repoManager.openRepository(project)) {
-      Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
-      assertThat(ref).isNotNull();
-      oldMetaId = ref.getObjectId();
+      for (Change.Id id : ImmutableList.of(id1, id2)) {
+        String refName = RefNames.changeMetaRef(id);
+        Ref ref = repo.exactRef(refName);
+        assertThat(ref).named(refName).isNotNull();
 
-      Change c = db.changes().get(id);
-      assertThat(c.getTopic()).isNull();
-      rowVersion = c.getRowVersion();
-      NoteDbChangeState s = NoteDbChangeState.parse(c);
-      assertThat(s.getPrimaryStorage()).isEqualTo(PrimaryStorage.NOTE_DB);
-      assertThat(s.getRefState()).isEmpty();
+        Change c = db.changes().get(id);
+        assertThat(c.getTopic()).named("topic of change %s", id).isNull();
+        NoteDbChangeState s = NoteDbChangeState.parse(c);
+        assertThat(s.getPrimaryStorage())
+            .named("primary storage of change %s", id)
+            .isEqualTo(PrimaryStorage.NOTE_DB);
+        assertThat(s.getRefState()).named("ref state of change %s").isEmpty();
+
+        if (id.equals(id1)) {
+          oldMetaId = ref.getObjectId();
+          rowVersion = c.getRowVersion();
+        }
+      }
     }
 
     // Do not open a new context, to simulate races with other threads that opened a context earlier
     // in the migration process; this needs to work.
-    gApi.changes().id(id.get()).topic(name("a-topic"));
+    gApi.changes().id(id1.get()).topic(name("a-topic"));
 
     // Of course, it should also work with a new context.
     resetCurrentApiUser();
-    gApi.changes().id(id.get()).topic(name("another-topic"));
+    gApi.changes().id(id1.get()).topic(name("another-topic"));
 
     try (ReviewDb db = schemaFactory.open();
         Repository repo = repoManager.openRepository(project)) {
-      assertThat(repo.exactRef(RefNames.changeMetaRef(id)).getObjectId()).isNotEqualTo(oldMetaId);
+      assertThat(repo.exactRef(RefNames.changeMetaRef(id1)).getObjectId()).isNotEqualTo(oldMetaId);
 
-      Change c = db.changes().get(id);
+      Change c = db.changes().get(id1);
       assertThat(c.getTopic()).isNull();
       assertThat(c.getRowVersion()).isEqualTo(rowVersion);
     }
   }
 
+  @Test
+  public void autoMigrationConfig() throws Exception {
+    createChange();
+
+    migrate(b -> b.setStopAtStateForTesting(WRITE));
+    assertNotesMigrationState(WRITE);
+    assertThat(NoteDbMigrator.getAutoMigrate(gerritConfig)).isFalse();
+
+    migrate(b -> b.setAutoMigrate(true).setStopAtStateForTesting(READ_WRITE_NO_SEQUENCE));
+    assertNotesMigrationState(READ_WRITE_NO_SEQUENCE);
+    assertThat(NoteDbMigrator.getAutoMigrate(gerritConfig)).isTrue();
+
+    migrate(b -> b);
+    assertNotesMigrationState(NOTE_DB_UNFUSED);
+    assertThat(NoteDbMigrator.getAutoMigrate(gerritConfig)).isFalse();
+  }
+
   private void assertNotesMigrationState(NotesMigrationState expected) throws Exception {
     assertThat(NotesMigrationState.forNotesMigration(notesMigration)).hasValue(expected);
     gerritConfig.load();
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
index 2108815..a690136 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
@@ -14,36 +14,51 @@
 
 package com.google.gerrit.elasticsearch;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.OnlineUpgrader;
 import com.google.gerrit.server.index.SingleVersionModule;
+import com.google.gerrit.server.index.VersionManager;
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.group.GroupIndex;
+import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
 import java.util.Map;
 import org.eclipse.jgit.lib.Config;
 
-public class ElasticIndexModule extends LifecycleModule {
-  private final int threads;
-  private final Map<String, Integer> singleVersions;
-
+public class ElasticIndexModule extends AbstractModule {
   public static ElasticIndexModule singleVersionWithExplicitVersions(
       Map<String, Integer> versions, int threads) {
-    return new ElasticIndexModule(versions, threads);
+    return new ElasticIndexModule(versions, threads, false);
   }
 
   public static ElasticIndexModule latestVersionWithOnlineUpgrade() {
-    return new ElasticIndexModule(null, 0);
+    return new ElasticIndexModule(null, 0, true);
   }
 
-  private ElasticIndexModule(Map<String, Integer> singleVersions, int threads) {
+  public static ElasticIndexModule latestVersionWithoutOnlineUpgrade() {
+    return new ElasticIndexModule(null, 0, false);
+  }
+
+  private final Map<String, Integer> singleVersions;
+  private final int threads;
+  private final boolean onlineUpgrade;
+
+  private ElasticIndexModule(
+      Map<String, Integer> singleVersions, int threads, boolean onlineUpgrade) {
+    if (singleVersions != null) {
+      checkArgument(!onlineUpgrade, "online upgrade is incompatible with single version map");
+    }
     this.singleVersions = singleVersions;
     this.threads = threads;
+    this.onlineUpgrade = onlineUpgrade;
   }
 
   @Override
@@ -63,7 +78,7 @@
 
     install(new IndexModule(threads));
     if (singleVersions == null) {
-      listener().to(ElasticVersionManager.class);
+      install(new MultiVersionModule());
     } else {
       install(new SingleVersionModule(singleVersions));
     }
@@ -74,4 +89,15 @@
   IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
     return IndexConfig.fromConfig(cfg).separateChangeSubIndexes(true).build();
   }
+
+  private class MultiVersionModule extends LifecycleModule {
+    @Override
+    public void configure() {
+      bind(VersionManager.class).to(ElasticVersionManager.class);
+      listener().to(ElasticVersionManager.class);
+      if (onlineUpgrade) {
+        listener().to(OnlineUpgrader.class);
+      }
+    }
+  }
 }
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
index 74a6b69..609c4d9 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
@@ -16,14 +16,15 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.AbstractVersionManager;
 import com.google.gerrit.server.index.GerritIndexStatus;
 import com.google.gerrit.server.index.Index;
 import com.google.gerrit.server.index.IndexDefinition;
+import com.google.gerrit.server.index.OnlineUpgradeListener;
 import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.VersionManager;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -34,7 +35,7 @@
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class ElasticVersionManager extends AbstractVersionManager implements LifecycleListener {
+public class ElasticVersionManager extends VersionManager {
   private static final Logger log = LoggerFactory.getLogger(ElasticVersionManager.class);
 
   private final String prefix;
@@ -44,9 +45,10 @@
   ElasticVersionManager(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
+      DynamicSet<OnlineUpgradeListener> listeners,
       Collection<IndexDefinition<?, ?, ?>> defs,
       ElasticIndexVersionDiscovery versionDiscovery) {
-    super(cfg, sitePaths, defs);
+    super(sitePaths, listeners, defs, VersionManager.getOnlineUpgrade(cfg));
     this.versionDiscovery = versionDiscovery;
     prefix = MoreObjects.firstNonNull(cfg.getString("index", null, "prefix"), "gerrit");
   }
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index 524af50..669b610 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -37,7 +37,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -71,8 +70,6 @@
 
 /** Unit tests for {@link GerritPublicKeyChecker}. */
 public class GerritPublicKeyCheckerTest {
-  @Inject private Accounts accounts;
-
   @Inject private AccountsUpdate.Server accountsUpdate;
 
   @Inject private AccountManager accountManager;
@@ -117,10 +114,8 @@
     db = schemaFactory.open();
     schemaCreator.create(db);
     userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
-    Account userAccount = accounts.get(db, userId);
     // Note: does not match any key in TestKeys.
-    userAccount.setPreferredEmail("user@example.com");
-    accountsUpdate.create().update(db, userAccount);
+    accountsUpdate.create().update(db, userId, a -> a.setPreferredEmail("user@example.com"));
     user = reloadUser();
 
     requestContext.setContext(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index 7a1a450..753d421 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -53,7 +53,6 @@
                           if ("self".startsWith(request.getQuery())) {
                             final ArrayList<SuggestOracle.Suggestion> r =
                                 new ArrayList<>(response.getSuggestions().size() + 1);
-                            r.addAll(response.getSuggestions());
                             r.add(
                                 new SuggestOracle.Suggestion() {
                                   @Override
@@ -66,6 +65,7 @@
                                     return "self";
                                   }
                                 });
+                            r.addAll(response.getSuggestions());
                             response.setSuggestions(r);
                           }
                           done.onSuggestionsReady(request, response);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
index 15169ac..8bbc988 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -542,16 +542,24 @@
       }
     }
 
-    Configuration cfg = Configuration.create().set("autoCloseBrackets", prefs.autoCloseBrackets())
-        .set("cursorBlinkRate", prefs.cursorBlinkRate()).set("cursorHeight", 0.85)
-        .set("indentUnit", prefs.indentUnit())
-        .set("keyMap", prefs.keyMapType().name().toLowerCase())
-        .set("lineNumbers", prefs.hideLineNumbers()).set("lineWrapping", false)
-        .set("matchBrackets", prefs.matchBrackets()).set("mode", mode != null ? mode.mime() : null)
-        .set("origLeft", editContent).set("scrollbarStyle", "overlay")
-        .set("showTrailingSpace", prefs.showWhitespaceErrors()).set("styleSelectedText", true)
-        .set("tabSize", prefs.tabSize()).set("theme", prefs.theme().name().toLowerCase())
-        .set("value", "");
+    Configuration cfg =
+        Configuration.create()
+            .set("autoCloseBrackets", prefs.autoCloseBrackets())
+            .set("cursorBlinkRate", prefs.cursorBlinkRate())
+            .set("cursorHeight", 0.85)
+            .set("indentUnit", prefs.indentUnit())
+            .set("keyMap", prefs.keyMapType().name().toLowerCase())
+            .set("lineNumbers", prefs.hideLineNumbers())
+            .set("lineWrapping", false)
+            .set("matchBrackets", prefs.matchBrackets())
+            .set("mode", mode != null ? mode.mime() : null)
+            .set("origLeft", editContent)
+            .set("scrollbarStyle", "overlay")
+            .set("showTrailingSpace", prefs.showWhitespaceErrors())
+            .set("styleSelectedText", true)
+            .set("tabSize", prefs.tabSize())
+            .set("theme", prefs.theme().name().toLowerCase())
+            .set("value", "");
 
     if (editContent.contains("\r\n")) {
       cfg.set("lineSeparator", "\r\n");
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index 47850c4..0ee720a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -42,6 +42,7 @@
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -111,7 +112,7 @@
       Account target;
       try {
         target = accountResolver.find(db.get(), runas);
-      } catch (OrmException e) {
+      } catch (OrmException | IOException | ConfigInvalidException e) {
         log.warn("cannot resolve account for " + RUN_AS, e);
         replyError(req, res, SC_INTERNAL_SERVER_ERROR, "cannot resolve " + RUN_AS, e);
         return;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index ec7fb68..e721b7a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -49,6 +49,7 @@
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
@@ -231,7 +232,7 @@
     }
     try (ReviewDb db = schema.open()) {
       return auth(accounts.get(db, id));
-    } catch (OrmException e) {
+    } catch (OrmException | IOException | ConfigInvalidException e) {
       getServletContext().log("cannot query database", e);
       return null;
     }
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 6dbebf1..558cc78 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
@@ -44,6 +44,12 @@
   <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}
+  // Content between webcomponents-lite and the load of the main app element
+  // run before polymer-resin is installed so may have security consequences.
+  // Contact your local security engineer if you have any questions, and
+  // CC them on any changes that load content before gr-app.html.
+  //
+  // github.com/Polymer/polymer-resin/blob/master/getting-started.md#integrating
   <link rel="preload" href="{$staticResourcePath}/elements/gr-app.js" as="script" crossorigin="anonymous">{\n}
   <link rel="import" href="{$staticResourcePath}/elements/gr-app.html">{\n}
 
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
index 89fd819..b5531d5 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -14,15 +14,20 @@
 
 package com.google.gerrit.lucene;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.OnlineUpgrader;
 import com.google.gerrit.server.index.SingleVersionModule;
+import com.google.gerrit.server.index.VersionManager;
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.group.GroupIndex;
+import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
@@ -30,30 +35,40 @@
 import org.apache.lucene.search.BooleanQuery;
 import org.eclipse.jgit.lib.Config;
 
-public class LuceneIndexModule extends LifecycleModule {
+public class LuceneIndexModule extends AbstractModule {
   public static LuceneIndexModule singleVersionAllLatest(int threads) {
-    return new LuceneIndexModule(ImmutableMap.<String, Integer>of(), threads);
+    return new LuceneIndexModule(ImmutableMap.<String, Integer>of(), threads, false);
   }
 
   public static LuceneIndexModule singleVersionWithExplicitVersions(
       Map<String, Integer> versions, int threads) {
-    return new LuceneIndexModule(versions, threads);
+    return new LuceneIndexModule(versions, threads, false);
   }
 
   public static LuceneIndexModule latestVersionWithOnlineUpgrade() {
-    return new LuceneIndexModule(null, 0);
+    return new LuceneIndexModule(null, 0, true);
+  }
+
+  public static LuceneIndexModule latestVersionWithoutOnlineUpgrade() {
+    return new LuceneIndexModule(null, 0, false);
   }
 
   static boolean isInMemoryTest(Config cfg) {
     return cfg.getBoolean("index", "lucene", "testInmemory", false);
   }
 
-  private final int threads;
   private final Map<String, Integer> singleVersions;
+  private final int threads;
+  private final boolean onlineUpgrade;
 
-  private LuceneIndexModule(Map<String, Integer> singleVersions, int threads) {
+  private LuceneIndexModule(
+      Map<String, Integer> singleVersions, int threads, boolean onlineUpgrade) {
+    if (singleVersions != null) {
+      checkArgument(!onlineUpgrade, "online upgrade is incompatible with single version map");
+    }
     this.singleVersions = singleVersions;
     this.threads = threads;
+    this.onlineUpgrade = onlineUpgrade;
   }
 
   @Override
@@ -87,10 +102,14 @@
     return IndexConfig.fromConfig(cfg).separateChangeSubIndexes(true).build();
   }
 
-  private static class MultiVersionModule extends LifecycleModule {
+  private class MultiVersionModule extends LifecycleModule {
     @Override
     public void configure() {
+      bind(VersionManager.class).to(LuceneVersionManager.class);
       listener().to(LuceneVersionManager.class);
+      if (onlineUpgrade) {
+        listener().to(OnlineUpgrader.class);
+      }
     }
   }
 }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index ad13066..9e8007c 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -15,14 +15,15 @@
 package com.google.gerrit.lucene;
 
 import com.google.common.primitives.Ints;
-import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.AbstractVersionManager;
 import com.google.gerrit.server.index.GerritIndexStatus;
 import com.google.gerrit.server.index.Index;
 import com.google.gerrit.server.index.IndexDefinition;
+import com.google.gerrit.server.index.OnlineUpgradeListener;
 import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.VersionManager;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -36,10 +37,10 @@
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class LuceneVersionManager extends AbstractVersionManager implements LifecycleListener {
+public class LuceneVersionManager extends VersionManager {
   private static final Logger log = LoggerFactory.getLogger(LuceneVersionManager.class);
 
-  private static class Version<V> extends AbstractVersionManager.Version<V> {
+  private static class Version<V> extends VersionManager.Version<V> {
     private final boolean exists;
 
     private Version(Schema<V> schema, int version, boolean exists, boolean ready) {
@@ -56,22 +57,22 @@
   LuceneVersionManager(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
+      DynamicSet<OnlineUpgradeListener> listeners,
       Collection<IndexDefinition<?, ?, ?>> defs) {
-    super(cfg, sitePaths, defs);
+    super(sitePaths, listeners, defs, VersionManager.getOnlineUpgrade(cfg));
   }
 
   @Override
   protected <V> boolean isDirty(
-      Collection<com.google.gerrit.server.index.AbstractVersionManager.Version<V>> inUse,
-      com.google.gerrit.server.index.AbstractVersionManager.Version<V> v) {
+      Collection<com.google.gerrit.server.index.VersionManager.Version<V>> inUse,
+      com.google.gerrit.server.index.VersionManager.Version<V> v) {
     return !inUse.contains(v) && ((Version<V>) v).exists;
   }
 
   @Override
-  protected <K, V, I extends Index<K, V>>
-      TreeMap<Integer, AbstractVersionManager.Version<V>> scanVersions(
-          IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
-    TreeMap<Integer, AbstractVersionManager.Version<V>> versions = new TreeMap<>();
+  protected <K, V, I extends Index<K, V>> TreeMap<Integer, VersionManager.Version<V>> scanVersions(
+      IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
+    TreeMap<Integer, VersionManager.Version<V>> versions = new TreeMap<>();
     for (Schema<V> schema : def.getSchemas().values()) {
       // This part is Lucene-specific.
       Path p = getDir(sitePaths, def.getName(), schema);
diff --git a/gerrit-pgm/BUILD b/gerrit-pgm/BUILD
index ea90c3e..b58891b 100644
--- a/gerrit-pgm/BUILD
+++ b/gerrit-pgm/BUILD
@@ -9,6 +9,7 @@
 INIT_API_SRCS = glob([SRCS + "init/api/*.java"])
 
 BASE_JETTY_DEPS = [
+    "//gerrit-common:annotations",
     "//gerrit-common:server",
     "//gerrit-extension-api:api",
     "//gerrit-gwtexpui:linker_server",
@@ -35,7 +36,7 @@
     name = "init-api",
     srcs = INIT_API_SRCS,
     visibility = ["//visibility:public"],
-    deps = DEPS + ["//gerrit-common:annotations"],
+    deps = DEPS,
 )
 
 java_library(
@@ -46,7 +47,6 @@
     deps = DEPS + [
         ":init-api",
         ":util",
-        "//gerrit-common:annotations",
         "//gerrit-elasticsearch:elasticsearch",
         "//gerrit-launcher:launcher",  # We want this dep to be provided_deps
         "//gerrit-lucene:lucene",
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 fc2fdef..757b130 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
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.pgm;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.EventBroker;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.elasticsearch.ElasticIndexModule;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.gpg.GpgModule;
@@ -69,10 +71,13 @@
 import com.google.gerrit.server.index.DummyIndexModule;
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.index.IndexModule.IndexType;
+import com.google.gerrit.server.index.VersionManager;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.receive.MailReceiver;
 import com.google.gerrit.server.mail.send.SmtpEmailSender;
 import com.google.gerrit.server.mime.MimeUtil2Module;
+import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
+import com.google.gerrit.server.notedb.rebuild.OnlineNoteDbMigrator;
 import com.google.gerrit.server.patch.DiffExecutorModule;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginModule;
@@ -111,6 +116,7 @@
 import javax.servlet.http.HttpServletRequest;
 import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.spi.ExplicitBooleanOptionHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -162,6 +168,13 @@
   @Option(name = "--stop-only", usage = "Stop the daemon", hidden = true)
   private boolean stopOnly;
 
+  @Option(
+    name = "--migrate-to-note-db",
+    usage = "(EXPERIMENTAL) Automatically migrate changes to NoteDb",
+    handler = ExplicitBooleanOptionHandler.class
+  )
+  private boolean migrateToNoteDb;
+
   private final LifecycleManager manager = new LifecycleManager();
   private Injector dbInjector;
   private Injector cfgInjector;
@@ -174,6 +187,7 @@
   private boolean test;
   private AbstractModule luceneModule;
   private Module emailModule;
+  private Module testSysModule;
 
   private Runnable serverStarted;
   private IndexType indexType;
@@ -297,6 +311,11 @@
   }
 
   @VisibleForTesting
+  public void setAdditionalSysModuleForTesting(@Nullable Module m) {
+    testSysModule = m;
+  }
+
+  @VisibleForTesting
   public void start() throws IOException {
     if (dbInjector == null) {
       dbInjector = createDbInjector(true /* enableMetrics */, MULTI_USER);
@@ -442,9 +461,19 @@
       modules.add(new ChangeCleanupRunner.Module());
     }
     modules.addAll(LibModuleLoader.loadModules(cfgInjector));
+    if (migrateToNoteDb()) {
+      modules.add(new OnlineNoteDbMigrator.Module());
+    }
+    if (testSysModule != null) {
+      modules.add(testSysModule);
+    }
     return cfgInjector.createChildInjector(modules);
   }
 
+  private boolean migrateToNoteDb() {
+    return migrateToNoteDb || NoteDbMigrator.getAutoMigrate(checkNotNull(config));
+  }
+
   private Module createIndexModule() {
     if (slave) {
       return new DummyIndexModule();
@@ -452,11 +481,19 @@
     if (luceneModule != null) {
       return luceneModule;
     }
+    boolean onlineUpgrade =
+        VersionManager.getOnlineUpgrade(config)
+            // Schema upgrade is handled by OnlineNoteDbMigrator in this case.
+            && !migrateToNoteDb();
     switch (indexType) {
       case LUCENE:
-        return LuceneIndexModule.latestVersionWithOnlineUpgrade();
+        return onlineUpgrade
+            ? LuceneIndexModule.latestVersionWithOnlineUpgrade()
+            : LuceneIndexModule.latestVersionWithoutOnlineUpgrade();
       case ELASTICSEARCH:
-        return ElasticIndexModule.latestVersionWithOnlineUpgrade();
+        return onlineUpgrade
+            ? ElasticIndexModule.latestVersionWithOnlineUpgrade()
+            : ElasticIndexModule.latestVersionWithoutOnlineUpgrade();
       default:
         throw new IllegalStateException("unsupported index.type = " + indexType);
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
index 9465a54..81435e0 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
@@ -19,11 +19,13 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.File;
@@ -57,7 +59,15 @@
           ObjectInserter oi = repo.newObjectInserter()) {
         PersonIdent serverIdent = new GerritPersonIdentProvider(flags.cfg).get();
         AccountsUpdate.createUserBranch(
-            repo, oi, serverIdent, serverIdent, account.getId(), account.getRegisteredOn());
+            repo,
+            new Project.NameKey(allUsers),
+            GitReferenceUpdated.DISABLED,
+            null,
+            oi,
+            serverIdent,
+            serverIdent,
+            account.getId(),
+            account.getRegisteredOn());
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
index b79f496..2fad708 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
@@ -44,6 +44,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
@@ -60,6 +61,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Stream;
 import org.apache.commons.lang.mutable.MutableDouble;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -73,7 +75,7 @@
       new double[] {
         BASE_REVIEWER_WEIGHT, BASE_OWNER_WEIGHT, BASE_COMMENT_WEIGHT,
       };
-  private static final long PLUGIN_QUERY_TIMEOUT = 500; //ms
+  private static final long PLUGIN_QUERY_TIMEOUT = 500; // ms
 
   private final ChangeQueryBuilder changeQueryBuilder;
   private final Config config;
@@ -108,7 +110,7 @@
       SuggestReviewers suggestReviewers,
       ProjectControl projectControl,
       List<Account.Id> candidateList)
-      throws OrmException {
+      throws OrmException, IOException, ConfigInvalidException {
     String query = suggestReviewers.getQuery();
     double baseWeight = config.getInt("addReviewer", "baseWeight", 1);
 
@@ -196,7 +198,7 @@
   }
 
   private Map<Account.Id, MutableDouble> baseRankingForEmptyQuery(double baseWeight)
-      throws OrmException {
+      throws OrmException, IOException, ConfigInvalidException {
     // Get the user's last 25 changes, check approvals
     try {
       List<ChangeData> result =
@@ -225,7 +227,7 @@
 
   private Map<Account.Id, MutableDouble> baseRankingForCandidateList(
       List<Account.Id> candidates, ProjectControl projectControl, double baseWeight)
-      throws OrmException {
+      throws OrmException, IOException, ConfigInvalidException {
     // Get each reviewer's activity based on number of applied labels
     // (weighted 10d), number of comments (weighted 0.5d) and number of owned
     // changes (weighted 1d).
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
index 410dc5c..d3083e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -56,6 +56,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 public class ReviewersUtil {
   @Singleton
@@ -146,7 +147,7 @@
       ProjectControl projectControl,
       VisibilityControl visibilityControl,
       boolean excludeGroups)
-      throws IOException, OrmException {
+      throws IOException, OrmException, ConfigInvalidException {
     String query = suggestReviewers.getQuery();
     int limit = suggestReviewers.getLimit();
 
@@ -212,7 +213,7 @@
       SuggestReviewers suggestReviewers,
       ProjectControl projectControl,
       List<Account.Id> candidateList)
-      throws OrmException {
+      throws OrmException, IOException, ConfigInvalidException {
     try (Timer0.Context ctx = metrics.recommendAccountsLatency.start()) {
       return reviewerRecommender.suggestReviewers(
           changeNotes, suggestReviewers, projectControl, candidateList);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java b/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java
index f1f0e6f..010ed32 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java
@@ -18,6 +18,11 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer2;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -32,18 +37,22 @@
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
 
-@SuppressWarnings("deprecation")
 @Singleton
 public class Sequences {
-  public static final String CHANGES = "changes";
+  public static final String NAME_CHANGES = "changes";
 
   public static int getChangeSequenceGap(Config cfg) {
     return cfg.getInt("noteDb", "changes", "initialSequenceGap", 1000);
   }
 
+  private enum SequenceType {
+    CHANGES;
+  }
+
   private final Provider<ReviewDb> db;
   private final NotesMigration migration;
   private final RepoSequence changeSeq;
+  private final Timer2<SequenceType, Boolean> nextIdLatency;
 
   @Inject
   Sequences(
@@ -51,30 +60,41 @@
       Provider<ReviewDb> db,
       NotesMigration migration,
       GitRepositoryManager repoManager,
-      AllProjectsName allProjects) {
+      AllProjectsName allProjects,
+      MetricMaker metrics) {
     this.db = db;
     this.migration = migration;
 
     int gap = getChangeSequenceGap(cfg);
-    changeSeq =
-        new RepoSequence(
-            repoManager,
-            allProjects,
-            CHANGES,
-            () -> db.get().nextChangeId() + gap,
-            cfg.getInt("noteDb", "changes", "sequenceBatchSize", 20));
+    @SuppressWarnings("deprecation")
+    RepoSequence.Seed seed = () -> db.get().nextChangeId() + gap;
+    int batchSize = cfg.getInt("noteDb", "changes", "sequenceBatchSize", 20);
+    changeSeq = new RepoSequence(repoManager, allProjects, NAME_CHANGES, seed, batchSize);
+
+    nextIdLatency =
+        metrics.newTimer(
+            "sequence/next_id_latency",
+            new Description("Latency of requesting IDs from repo sequences")
+                .setCumulative()
+                .setUnit(Units.MILLISECONDS),
+            Field.ofEnum(SequenceType.class, "sequence"),
+            Field.ofBoolean("multiple"));
   }
 
   public int nextChangeId() throws OrmException {
     if (!migration.readChangeSequence()) {
-      return db.get().nextChangeId();
+      return nextChangeId(db.get());
     }
-    return changeSeq.next();
+    try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, false)) {
+      return changeSeq.next();
+    }
   }
 
   public ImmutableList<Integer> nextChangeIds(int count) throws OrmException {
     if (migration.readChangeSequence()) {
-      return changeSeq.next(count);
+      try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, count > 1)) {
+        return changeSeq.next(count);
+      }
     }
 
     if (count == 0) {
@@ -84,7 +104,7 @@
     List<Integer> ids = new ArrayList<>(count);
     ReviewDb db = this.db.get();
     for (int i = 0; i < count; i++) {
-      ids.add(db.nextChangeId());
+      ids.add(nextChangeId(db));
     }
     return ImmutableList.copyOf(ids);
   }
@@ -93,4 +113,9 @@
   public RepoSequence getChangeIdRepoSequence() {
     return changeSeq;
   }
+
+  @SuppressWarnings("deprecation")
+  private static int nextChangeId(ReviewDb db) throws OrmException {
+    return db.nextChangeId();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountConfig.java
new file mode 100644
index 0000000..70ed29e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountConfig.java
@@ -0,0 +1,260 @@
+// 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.account;
+
+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.Nullable;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.ValidationError;
+import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevSort;
+
+/**
+ * ‘account.config’ file in the user branch in the All-Users repository that contains the properties
+ * of the account.
+ *
+ * <p>The 'account.config' file is a git config file that has one 'account' section with the
+ * properties of the account:
+ *
+ * <pre>
+ *   [account]
+ *     active = false
+ *     fullName = John Doe
+ *     preferredEmail = john.doe@foo.com
+ *     status = Overloaded with reviews
+ * </pre>
+ *
+ * <p>All keys are optional. This means 'account.config' may not exist on the user branch if no
+ * properties are set.
+ *
+ * <p>Not setting a key and setting a key to an empty string are treated the same way and result in
+ * a {@code null} value.
+ *
+ * <p>If no value for 'active' is specified, by default the account is considered as active.
+ *
+ * <p>The commit date of the first commit on the user branch is used as registration date of the
+ * account. The first commit may be an empty commit (if no properties were set and 'account.config'
+ * doesn't exist).
+ */
+public class AccountConfig extends VersionedMetaData implements ValidationError.Sink {
+  public static final String ACCOUNT_CONFIG = "account.config";
+  public static final String ACCOUNT = "account";
+  public static final String KEY_ACTIVE = "active";
+  public static final String KEY_FULL_NAME = "fullName";
+  public static final String KEY_PREFERRED_EMAIL = "preferredEmail";
+  public static final String KEY_STATUS = "status";
+
+  @Nullable private final OutgoingEmailValidator emailValidator;
+  private final Account.Id accountId;
+  private final String ref;
+
+  private boolean isLoaded;
+  private Account account;
+  private Timestamp registeredOn;
+  private List<ValidationError> validationErrors;
+
+  public AccountConfig(@Nullable OutgoingEmailValidator emailValidator, Account.Id accountId) {
+    this.emailValidator = emailValidator;
+    this.accountId = accountId;
+    this.ref = RefNames.refsUsers(accountId);
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  /**
+   * Get the loaded account.
+   *
+   * @return loaded account.
+   * @throws IllegalStateException if the account was not loaded yet
+   */
+  public Account getAccount() {
+    checkLoaded();
+    return account;
+  }
+
+  /**
+   * Sets the account. This means the loaded account will be overwritten with the given account.
+   *
+   * <p>Changing the registration date of an account is not supported.
+   *
+   * @param account account that should be set
+   * @throws IllegalStateException if the account was not loaded yet
+   */
+  public void setAccount(Account account) {
+    checkLoaded();
+    this.account = account;
+    this.registeredOn = account.getRegisteredOn();
+  }
+
+  /**
+   * Creates a new account.
+   *
+   * @return the new account
+   * @throws OrmDuplicateKeyException if the user branch already exists
+   */
+  public Account getNewAccount() throws OrmDuplicateKeyException {
+    checkLoaded();
+    if (revision != null) {
+      throw new OrmDuplicateKeyException(String.format("account %s already exists", accountId));
+    }
+    this.registeredOn = TimeUtil.nowTs();
+    this.account = new Account(accountId, registeredOn);
+    return account;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    if (revision != null) {
+      rw.markStart(revision);
+      rw.sort(RevSort.REVERSE);
+      registeredOn = new Timestamp(rw.next().getCommitTime() * 1000L);
+
+      Config cfg = readConfig(ACCOUNT_CONFIG);
+
+      account = parse(cfg);
+    }
+
+    isLoaded = true;
+  }
+
+  private Account parse(Config cfg) {
+    Account account = new Account(accountId, registeredOn);
+    account.setActive(cfg.getBoolean(ACCOUNT, null, KEY_ACTIVE, true));
+    account.setFullName(get(cfg, KEY_FULL_NAME));
+
+    String preferredEmail = get(cfg, KEY_PREFERRED_EMAIL);
+    account.setPreferredEmail(preferredEmail);
+    if (emailValidator != null && !emailValidator.isValid(preferredEmail)) {
+      error(
+          new ValidationError(
+              ACCOUNT_CONFIG, String.format("Invalid preferred email: %s", preferredEmail)));
+    }
+
+    account.setStatus(get(cfg, KEY_STATUS));
+    return account;
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
+    checkLoaded();
+
+    if (revision != null) {
+      commit.setMessage("Update account\n");
+    } else if (account != null) {
+      commit.setMessage("Create account\n");
+      commit.setAuthor(new PersonIdent(commit.getAuthor(), registeredOn));
+      commit.setCommitter(new PersonIdent(commit.getCommitter(), registeredOn));
+    }
+
+    Config cfg = readConfig(ACCOUNT_CONFIG);
+    setActive(cfg, account.isActive());
+    set(cfg, KEY_FULL_NAME, account.getFullName());
+    set(cfg, KEY_PREFERRED_EMAIL, account.getPreferredEmail());
+    set(cfg, KEY_STATUS, account.getStatus());
+    saveConfig(ACCOUNT_CONFIG, cfg);
+    return true;
+  }
+
+  /**
+   * Sets/Unsets {@code account.active} in the given config.
+   *
+   * <p>{@code account.active} is set to {@code false} if the account is inactive.
+   *
+   * <p>If the account is active {@code account.active} is unset since {@code true} is the default
+   * if this field is missing.
+   *
+   * @param cfg the config
+   * @param value whether the account is active
+   */
+  private static void setActive(Config cfg, boolean value) {
+    if (!value) {
+      cfg.setBoolean(ACCOUNT, null, KEY_ACTIVE, false);
+    } else {
+      cfg.unset(ACCOUNT, null, KEY_ACTIVE);
+    }
+  }
+
+  /**
+   * Sets/Unsets the given key in the given config.
+   *
+   * <p>The key unset if the value is {@code null}.
+   *
+   * @param cfg the config
+   * @param key the key
+   * @param value the value
+   */
+  private static void set(Config cfg, String key, String value) {
+    if (!Strings.isNullOrEmpty(value)) {
+      cfg.setString(ACCOUNT, null, key, value);
+    } else {
+      cfg.unset(ACCOUNT, null, key);
+    }
+  }
+
+  /**
+   * Gets the given key from the given config.
+   *
+   * <p>Empty values are returned as {@code null}
+   *
+   * @param cfg the config
+   * @param key the key
+   * @return the value, {@code null} if key was not set or key was set to empty string
+   */
+  private static String get(Config cfg, String key) {
+    return Strings.emptyToNull(cfg.getString(ACCOUNT, null, key));
+  }
+
+  private void checkLoaded() {
+    checkState(isLoaded, "account not loaded yet");
+  }
+
+  /**
+   * Get the validation errors, if any were discovered during load.
+   *
+   * @return list of errors; empty list if there are no errors.
+   */
+  public List<ValidationError> getValidationErrors() {
+    if (validationErrors != null) {
+      return ImmutableList.copyOf(validationErrors);
+    }
+    return ImmutableList.of();
+  }
+
+  @Override
+  public void error(ValidationError error) {
+    if (validationErrors == null) {
+      validationErrors = new ArrayList<>(4);
+    }
+    validationErrors.add(error);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 861667b..a6f1e2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -16,7 +16,6 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.audit.AuditService;
-import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
@@ -39,10 +38,13 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
@@ -150,7 +152,7 @@
   private void update(ReviewDb db, AuthRequest who, ExternalId extId)
       throws OrmException, IOException, ConfigInvalidException {
     IdentifiedUser user = userFactory.create(extId.accountId());
-    Account toUpdate = null;
+    List<Consumer<Account>> accountUpdates = new ArrayList<>();
 
     // If the email address was modified by the authentication provider,
     // update our records to match the changed email.
@@ -159,8 +161,7 @@
     String oldEmail = extId.email();
     if (newEmail != null && !newEmail.equals(oldEmail)) {
       if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) {
-        toUpdate = load(toUpdate, user.getAccountId(), db);
-        toUpdate.setPreferredEmail(newEmail);
+        accountUpdates.add(a -> a.setPreferredEmail(newEmail));
       }
 
       externalIdsUpdateFactory
@@ -172,8 +173,7 @@
     if (!realm.allowsEdit(AccountFieldName.FULL_NAME)
         && !Strings.isNullOrEmpty(who.getDisplayName())
         && !eq(user.getAccount().getFullName(), who.getDisplayName())) {
-      toUpdate = load(toUpdate, user.getAccountId(), db);
-      toUpdate.setFullName(who.getDisplayName());
+      accountUpdates.add(a -> a.setFullName(who.getDisplayName()));
     }
 
     if (!realm.allowsEdit(AccountFieldName.USER_NAME)
@@ -184,8 +184,12 @@
               "Not changing already set username %s to %s", user.getUserName(), who.getUserName()));
     }
 
-    if (toUpdate != null) {
-      accountsUpdateFactory.create().update(db, toUpdate);
+    if (!accountUpdates.isEmpty()) {
+      Account account =
+          accountsUpdateFactory.create().update(db, user.getAccountId(), accountUpdates);
+      if (account == null) {
+        throw new OrmException("Account " + user.getAccountId() + " has been deleted");
+      }
     }
 
     if (newEmail != null && !newEmail.equals(oldEmail)) {
@@ -194,16 +198,6 @@
     }
   }
 
-  private Account load(Account toUpdate, Account.Id accountId, ReviewDb db) throws OrmException {
-    if (toUpdate == null) {
-      toUpdate = accounts.get(db, accountId);
-      if (toUpdate == null) {
-        throw new OrmException("Account " + accountId + " has been deleted");
-      }
-    }
-    return toUpdate;
-  }
-
   private static boolean eq(String a, String b) {
     return (a == null && b == null) || (a != null && a.equals(b));
   }
@@ -211,18 +205,23 @@
   private AuthResult create(ReviewDb db, AuthRequest who)
       throws OrmException, AccountException, IOException, ConfigInvalidException {
     Account.Id newId = new Account.Id(db.nextAccountId());
-    Account account = new Account(newId, TimeUtil.nowTs());
 
     ExternalId extId =
         ExternalId.createWithEmail(who.getExternalIdKey(), newId, who.getEmailAddress());
-    account.setFullName(who.getDisplayName());
-    account.setPreferredEmail(extId.email());
 
     boolean isFirstAccount = awaitsFirstAccountCheck.getAndSet(false) && !accounts.hasAnyAccount();
 
+    Account account;
     try {
       AccountsUpdate accountsUpdate = accountsUpdateFactory.create();
-      accountsUpdate.upsert(db, account);
+      account =
+          accountsUpdate.insert(
+              db,
+              newId,
+              a -> {
+                a.setFullName(who.getDisplayName());
+                a.setPreferredEmail(extId.email());
+              });
 
       ExternalId existingExtId = externalIds.get(extId.key());
       if (existingExtId != null && !existingExtId.accountId().equals(extId.accountId())) {
@@ -364,11 +363,16 @@
             .insert(ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress()));
 
         if (who.getEmailAddress() != null) {
-          Account a = accounts.get(db, to);
-          if (a.getPreferredEmail() == null) {
-            a.setPreferredEmail(who.getEmailAddress());
-            accountsUpdateFactory.create().update(db, a);
-          }
+          accountsUpdateFactory
+              .create()
+              .update(
+                  db,
+                  to,
+                  a -> {
+                    if (a.getPreferredEmail() == null) {
+                      a.setPreferredEmail(who.getEmailAddress());
+                    }
+                  });
           byEmailCache.evict(who.getEmailAddress());
         }
       }
@@ -428,12 +432,17 @@
         externalIdsUpdateFactory.create().delete(extId);
 
         if (who.getEmailAddress() != null) {
-          Account a = accounts.get(db, from);
-          if (a.getPreferredEmail() != null
-              && a.getPreferredEmail().equals(who.getEmailAddress())) {
-            a.setPreferredEmail(null);
-            accountsUpdateFactory.create().update(db, a);
-          }
+          accountsUpdateFactory
+              .create()
+              .update(
+                  db,
+                  from,
+                  a -> {
+                    if (a.getPreferredEmail() != null
+                        && a.getPreferredEmail().equals(who.getEmailAddress())) {
+                      a.setPreferredEmail(null);
+                    }
+                  });
           byEmailCache.evict(who.getEmailAddress());
         }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 8ce5f4c..7f66b9c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -23,12 +23,14 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class AccountResolver {
@@ -61,7 +63,8 @@
    * @return the single account that matches; null if no account matches or there are multiple
    *     candidates.
    */
-  public Account find(ReviewDb db, String nameOrEmail) throws OrmException {
+  public Account find(ReviewDb db, String nameOrEmail)
+      throws OrmException, IOException, ConfigInvalidException {
     Set<Account.Id> r = findAll(db, nameOrEmail);
     if (r.size() == 1) {
       return byId.get(r.iterator().next()).getAccount();
@@ -90,7 +93,8 @@
    *     name ("username").
    * @return the accounts that match, empty collection if none. Never null.
    */
-  public Set<Account.Id> findAll(ReviewDb db, String nameOrEmail) throws OrmException {
+  public Set<Account.Id> findAll(ReviewDb db, String nameOrEmail)
+      throws OrmException, IOException, ConfigInvalidException {
     Matcher m = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(nameOrEmail);
     if (m.matches()) {
       Account.Id id = Account.Id.parse(m.group(1));
@@ -118,7 +122,8 @@
     return findAllByNameOrEmail(db, nameOrEmail);
   }
 
-  private boolean exists(ReviewDb db, Account.Id id) throws OrmException {
+  private boolean exists(ReviewDb db, Account.Id id)
+      throws OrmException, IOException, ConfigInvalidException {
     return accounts.get(db, id) != null;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java
index 1a82f7b..28ed422 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java
@@ -22,35 +22,70 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Stream;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /** Class to access accounts. */
 @Singleton
 public class Accounts {
+  private static final Logger log = LoggerFactory.getLogger(Accounts.class);
+
+  private final boolean readFromGit;
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
+  private final OutgoingEmailValidator emailValidator;
 
   @Inject
-  Accounts(GitRepositoryManager repoManager, AllUsersName allUsersName) {
+  Accounts(
+      @GerritServerConfig Config cfg,
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      OutgoingEmailValidator emailValidator) {
+    this.readFromGit = cfg.getBoolean("user", null, "readAccountsFromGit", false);
     this.repoManager = repoManager;
     this.allUsersName = allUsersName;
+    this.emailValidator = emailValidator;
   }
 
-  public Account get(ReviewDb db, Account.Id accountId) throws OrmException {
+  public Account get(ReviewDb db, Account.Id accountId)
+      throws OrmException, IOException, ConfigInvalidException {
+    if (readFromGit) {
+      try (Repository repo = repoManager.openRepository(allUsersName)) {
+        return read(repo, accountId);
+      }
+    }
+
     return db.accounts().get(accountId);
   }
 
-  public List<Account> get(ReviewDb db, Collection<Account.Id> accountIds) throws OrmException {
+  public List<Account> get(ReviewDb db, Collection<Account.Id> accountIds)
+      throws OrmException, IOException, ConfigInvalidException {
+    if (readFromGit) {
+      List<Account> accounts = new ArrayList<>(accountIds.size());
+      try (Repository repo = repoManager.openRepository(allUsersName)) {
+        for (Account.Id accountId : accountIds) {
+          accounts.add(read(repo, accountId));
+        }
+      }
+      return accounts;
+    }
+
     return db.accounts().get(accountIds).toList();
   }
 
@@ -59,7 +94,22 @@
    *
    * @return all accounts
    */
-  public List<Account> all(ReviewDb db) throws OrmException {
+  public List<Account> all(ReviewDb db) throws OrmException, IOException {
+    if (readFromGit) {
+      Set<Account.Id> accountIds = allIds();
+      List<Account> accounts = new ArrayList<>(accountIds.size());
+      try (Repository repo = repoManager.openRepository(allUsersName)) {
+        for (Account.Id accountId : accountIds) {
+          try {
+            accounts.add(read(repo, accountId));
+          } catch (Exception e) {
+            log.error(String.format("Ignoring invalid account %s", accountId.get()), e);
+          }
+        }
+      }
+      return accounts;
+    }
+
     return db.accounts().all().toList();
   }
 
@@ -103,6 +153,13 @@
     }
   }
 
+  private Account read(Repository allUsersRepository, Account.Id accountId)
+      throws IOException, ConfigInvalidException {
+    AccountConfig accountConfig = new AccountConfig(emailValidator, accountId);
+    accountConfig.load(allUsersRepository);
+    return accountConfig.getAccount();
+  }
+
   private static Stream<Account.Id> readUserRefs(Repository repo) throws IOException {
     return repo.getRefDatabase()
         .getRefs(RefNames.REFS_USERS)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index 081ea26..1669c4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -33,6 +33,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class AccountsCollection
@@ -68,7 +70,8 @@
 
   @Override
   public AccountResource parse(TopLevelResource root, IdString id)
-      throws ResourceNotFoundException, AuthException, OrmException {
+      throws ResourceNotFoundException, AuthException, OrmException, IOException,
+          ConfigInvalidException {
     IdentifiedUser user = parseId(id.get());
     if (user == null) {
       throw new ResourceNotFoundException(id);
@@ -89,7 +92,8 @@
    *     account is not visible to the calling user
    */
   public IdentifiedUser parse(String id)
-      throws AuthException, UnprocessableEntityException, OrmException {
+      throws AuthException, UnprocessableEntityException, OrmException, IOException,
+          ConfigInvalidException {
     return parseOnBehalfOf(null, id);
   }
 
@@ -104,8 +108,11 @@
    * @throws AuthException thrown if 'self' is used as account ID and the current user is not
    *     authenticated
    * @throws OrmException
+   * @throws ConfigInvalidException
+   * @throws IOException
    */
-  public IdentifiedUser parseId(String id) throws AuthException, OrmException {
+  public IdentifiedUser parseId(String id)
+      throws AuthException, OrmException, IOException, ConfigInvalidException {
     return parseIdOnBehalfOf(null, id);
   }
 
@@ -113,7 +120,8 @@
    * Like {@link #parse(String)}, but also sets the {@link CurrentUser#getRealUser()} on the result.
    */
   public IdentifiedUser parseOnBehalfOf(@Nullable CurrentUser caller, String id)
-      throws AuthException, UnprocessableEntityException, OrmException {
+      throws AuthException, UnprocessableEntityException, OrmException, IOException,
+          ConfigInvalidException {
     IdentifiedUser user = parseIdOnBehalfOf(caller, id);
     if (user == null) {
       throw new UnprocessableEntityException(String.format("Account Not Found: %s", id));
@@ -124,7 +132,7 @@
   }
 
   private IdentifiedUser parseIdOnBehalfOf(@Nullable CurrentUser caller, String id)
-      throws AuthException, OrmException {
+      throws AuthException, OrmException, IOException, ConfigInvalidException {
     if (id.equals("self")) {
       CurrentUser user = self.get();
       if (user.isIdentifiedUser()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
index f6ed598..ef501ea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -16,15 +16,21 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
+import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -32,7 +38,9 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.List;
 import java.util.function.Consumer;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -46,8 +54,18 @@
 /**
  * Updates accounts.
  *
- * <p>On updating accounts this class takes care to evict them from the account cache and thus
- * triggers reindex for them.
+ * <p>The account updates are written to both ReviewDb and NoteDb.
+ *
+ * <p>In NoteDb accounts are represented as user branches in the All-Users repository. Optionally a
+ * user branch can contain a 'account.config' file that stores account properties, such as full
+ * name, preferred email, status and the active flag. The timestamp of the first commit on a user
+ * branch denotes the registration date. The initial commit on the user branch may be empty (since
+ * having an 'account.config' is optional). See {@link AccountConfig} for details of the
+ * 'account.config' file format.
+ *
+ * <p>On updating accounts the accounts are evicted from the account cache and thus reindexed. The
+ * eviction from the account cache is done by the {@link ReindexAfterRefUpdate} class which receives
+ * the event about updating the user branch that is triggered by this class.
  */
 @Singleton
 public class AccountsUpdate {
@@ -60,56 +78,38 @@
   @Singleton
   public static class Server {
     private final GitRepositoryManager repoManager;
-    private final AccountCache accountCache;
+    private final GitReferenceUpdated gitRefUpdated;
     private final AllUsersName allUsersName;
+    private final OutgoingEmailValidator emailValidator;
     private final Provider<PersonIdent> serverIdent;
+    private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
 
     @Inject
     public Server(
         GitRepositoryManager repoManager,
-        AccountCache accountCache,
+        GitReferenceUpdated gitRefUpdated,
         AllUsersName allUsersName,
-        @GerritPersonIdent Provider<PersonIdent> serverIdent) {
+        OutgoingEmailValidator emailValidator,
+        @GerritPersonIdent Provider<PersonIdent> serverIdent,
+        Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory) {
       this.repoManager = repoManager;
-      this.accountCache = accountCache;
+      this.gitRefUpdated = gitRefUpdated;
       this.allUsersName = allUsersName;
+      this.emailValidator = emailValidator;
       this.serverIdent = serverIdent;
+      this.metaDataUpdateServerFactory = metaDataUpdateServerFactory;
     }
 
     public AccountsUpdate create() {
       PersonIdent i = serverIdent.get();
-      return new AccountsUpdate(repoManager, accountCache, allUsersName, i, i);
-    }
-  }
-
-  /**
-   * Factory to create an AccountsUpdate instance for updating accounts by the Gerrit server.
-   *
-   * <p>Using this class will not perform reindexing for the updated accounts and they will also not
-   * be evicted from the account cache.
-   *
-   * <p>The Gerrit server identity will be used as author and committer for all commits that update
-   * the accounts.
-   */
-  @Singleton
-  public static class ServerNoReindex {
-    private final GitRepositoryManager repoManager;
-    private final AllUsersName allUsersName;
-    private final Provider<PersonIdent> serverIdent;
-
-    @Inject
-    public ServerNoReindex(
-        GitRepositoryManager repoManager,
-        AllUsersName allUsersName,
-        @GerritPersonIdent Provider<PersonIdent> serverIdent) {
-      this.repoManager = repoManager;
-      this.allUsersName = allUsersName;
-      this.serverIdent = serverIdent;
-    }
-
-    public AccountsUpdate create() {
-      PersonIdent i = serverIdent.get();
-      return new AccountsUpdate(repoManager, null, allUsersName, i, i);
+      return new AccountsUpdate(
+          repoManager,
+          gitRefUpdated,
+          null,
+          allUsersName,
+          emailValidator,
+          i,
+          () -> metaDataUpdateServerFactory.get().create(allUsersName));
     }
   }
 
@@ -122,29 +122,42 @@
   @Singleton
   public static class User {
     private final GitRepositoryManager repoManager;
-    private final AccountCache accountCache;
+    private final GitReferenceUpdated gitRefUpdated;
     private final AllUsersName allUsersName;
+    private final OutgoingEmailValidator emailValidator;
     private final Provider<PersonIdent> serverIdent;
     private final Provider<IdentifiedUser> identifiedUser;
+    private final Provider<MetaDataUpdate.User> metaDataUpdateUserFactory;
 
     @Inject
     public User(
         GitRepositoryManager repoManager,
-        AccountCache accountCache,
+        GitReferenceUpdated gitRefUpdated,
         AllUsersName allUsersName,
+        OutgoingEmailValidator emailValidator,
         @GerritPersonIdent Provider<PersonIdent> serverIdent,
-        Provider<IdentifiedUser> identifiedUser) {
+        Provider<IdentifiedUser> identifiedUser,
+        Provider<MetaDataUpdate.User> metaDataUpdateUserFactory) {
       this.repoManager = repoManager;
-      this.accountCache = accountCache;
+      this.gitRefUpdated = gitRefUpdated;
       this.allUsersName = allUsersName;
       this.serverIdent = serverIdent;
+      this.emailValidator = emailValidator;
       this.identifiedUser = identifiedUser;
+      this.metaDataUpdateUserFactory = metaDataUpdateUserFactory;
     }
 
     public AccountsUpdate create() {
+      IdentifiedUser user = identifiedUser.get();
       PersonIdent i = serverIdent.get();
       return new AccountsUpdate(
-          repoManager, accountCache, allUsersName, createPersonIdent(i, identifiedUser.get()), i);
+          repoManager,
+          gitRefUpdated,
+          user,
+          allUsersName,
+          emailValidator,
+          createPersonIdent(i, user),
+          () -> metaDataUpdateUserFactory.get().create(allUsersName));
     }
 
     private PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
@@ -153,117 +166,178 @@
   }
 
   private final GitRepositoryManager repoManager;
-  @Nullable private final AccountCache accountCache;
+  private final GitReferenceUpdated gitRefUpdated;
+  @Nullable private final IdentifiedUser currentUser;
   private final AllUsersName allUsersName;
+  private final OutgoingEmailValidator emailValidator;
   private final PersonIdent committerIdent;
-  private final PersonIdent authorIdent;
+  private final MetaDataUpdateFactory metaDataUpdateFactory;
 
   private AccountsUpdate(
       GitRepositoryManager repoManager,
-      @Nullable AccountCache accountCache,
+      GitReferenceUpdated gitRefUpdated,
+      @Nullable IdentifiedUser currentUser,
       AllUsersName allUsersName,
+      OutgoingEmailValidator emailValidator,
       PersonIdent committerIdent,
-      PersonIdent authorIdent) {
+      MetaDataUpdateFactory metaDataUpdateFactory) {
     this.repoManager = checkNotNull(repoManager, "repoManager");
-    this.accountCache = accountCache;
+    this.gitRefUpdated = checkNotNull(gitRefUpdated, "gitRefUpdated");
+    this.currentUser = currentUser;
     this.allUsersName = checkNotNull(allUsersName, "allUsersName");
+    this.emailValidator = checkNotNull(emailValidator, "emailValidator");
     this.committerIdent = checkNotNull(committerIdent, "committerIdent");
-    this.authorIdent = checkNotNull(authorIdent, "authorIdent");
+    this.metaDataUpdateFactory = checkNotNull(metaDataUpdateFactory, "metaDataUpdateFactory");
   }
 
   /**
    * Inserts a new account.
    *
+   * @param db ReviewDb
+   * @param accountId ID of the new account
+   * @param init consumer to populate the new account
+   * @return the newly created account
+   * @throws OrmException if updating the database fails
    * @throws OrmDuplicateKeyException if the account already exists
    * @throws IOException if updating the user branch fails
+   * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
-  public void insert(ReviewDb db, Account account) throws OrmException, IOException {
+  public Account insert(ReviewDb db, Account.Id accountId, Consumer<Account> init)
+      throws OrmException, IOException, ConfigInvalidException {
+    AccountConfig accountConfig = read(accountId);
+    Account account = accountConfig.getNewAccount();
+    init.accept(account);
+
+    // Create in ReviewDb
     db.accounts().insert(ImmutableSet.of(account));
-    createUserBranch(account);
-    evictAccount(account.getId());
-  }
 
-  /**
-   * Inserts or updates an account.
-   *
-   * <p>If the account already exists, it is overwritten, otherwise it is inserted.
-   */
-  public void upsert(ReviewDb db, Account account) throws OrmException, IOException {
-    db.accounts().upsert(ImmutableSet.of(account));
-    createUserBranchIfNeeded(account);
-    evictAccount(account.getId());
-  }
-
-  /** Updates the account. */
-  public void update(ReviewDb db, Account account) throws OrmException, IOException {
-    db.accounts().update(ImmutableSet.of(account));
-    evictAccount(account.getId());
+    // Create in NoteDb
+    commitNew(accountConfig);
+    return account;
   }
 
   /**
    * Gets the account and updates it atomically.
    *
+   * <p>Changing the registration date of an account is not supported.
+   *
    * @param db ReviewDb
    * @param accountId ID of the account
    * @param consumer consumer to update the account, only invoked if the account exists
    * @return the updated account, {@code null} if the account doesn't exist
-   * @throws OrmException if updating the account fails
+   * @throws OrmException if updating the database fails
+   * @throws IOException if updating the user branch fails
+   * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
-  public Account atomicUpdate(ReviewDb db, Account.Id accountId, Consumer<Account> consumer)
-      throws OrmException, IOException {
-    Account account =
-        db.accounts()
-            .atomicUpdate(
-                accountId,
-                a -> {
-                  consumer.accept(a);
-                  return a;
-                });
-    evictAccount(accountId);
+  public Account update(ReviewDb db, Account.Id accountId, Consumer<Account> consumer)
+      throws OrmException, IOException, ConfigInvalidException {
+    return update(db, accountId, ImmutableList.of(consumer));
+  }
+
+  /**
+   * Gets the account and updates it atomically.
+   *
+   * <p>Changing the registration date of an account is not supported.
+   *
+   * @param db ReviewDb
+   * @param accountId ID of the account
+   * @param consumers consumers to update the account, only invoked if the account exists
+   * @return the updated account, {@code null} if the account doesn't exist
+   * @throws OrmException if updating the database fails
+   * @throws IOException if updating the user branch fails
+   * @throws ConfigInvalidException if any of the account fields has an invalid value
+   */
+  public Account update(ReviewDb db, Account.Id accountId, List<Consumer<Account>> consumers)
+      throws OrmException, IOException, ConfigInvalidException {
+    if (consumers.isEmpty()) {
+      return null;
+    }
+
+    // Update in ReviewDb
+    db.accounts()
+        .atomicUpdate(
+            accountId,
+            a -> {
+              consumers.stream().forEach(c -> c.accept(a));
+              return a;
+            });
+
+    // Update in NoteDb
+    AccountConfig accountConfig = read(accountId);
+    Account account = accountConfig.getAccount();
+    consumers.stream().forEach(c -> c.accept(account));
+    commit(accountConfig);
+
     return account;
   }
 
-  /** Deletes the account. */
+  /**
+   * Replaces the account.
+   *
+   * <p>The existing account with the same account ID is overwritten by the given account. Choosing
+   * to overwrite an account means that any updates that were done to the account by a racing
+   * request after the account was read are lost. Updates are also lost if the account was read from
+   * a stale account index. This is why using {@link #update(ReviewDb,
+   * com.google.gerrit.reviewdb.client.Account.Id, Consumer)} to do an atomic update is always
+   * preferred.
+   *
+   * <p>Changing the registration date of an account is not supported.
+   *
+   * @param db ReviewDb
+   * @param account the new account
+   * @throws OrmException if updating the database fails
+   * @throws IOException if updating the user branch fails
+   * @throws ConfigInvalidException if any of the account fields has an invalid value
+   * @see #update(ReviewDb, com.google.gerrit.reviewdb.client.Account.Id, Consumer)
+   */
+  public void replace(ReviewDb db, Account account)
+      throws OrmException, IOException, ConfigInvalidException {
+    // Update in ReviewDb
+    db.accounts().update(ImmutableSet.of(account));
+
+    // Update in NoteDb
+    AccountConfig accountConfig = read(account.getId());
+    accountConfig.setAccount(account);
+    commit(accountConfig);
+  }
+
+  /**
+   * Deletes the account.
+   *
+   * @param db ReviewDb
+   * @param account the account that should be deleted
+   * @throws OrmException if updating the database fails
+   * @throws IOException if updating the user branch fails
+   */
   public void delete(ReviewDb db, Account account) throws OrmException, IOException {
+    // Delete in ReviewDb
     db.accounts().delete(ImmutableSet.of(account));
+
+    // Delete in NoteDb
     deleteUserBranch(account.getId());
-    evictAccount(account.getId());
   }
 
-  /** Deletes the account. */
+  /**
+   * Deletes the account.
+   *
+   * @param db ReviewDb
+   * @param accountId the ID of the account that should be deleted
+   * @throws OrmException if updating the database fails
+   * @throws IOException if updating the user branch fails
+   */
   public void deleteByKey(ReviewDb db, Account.Id accountId) throws OrmException, IOException {
+    // Delete in ReviewDb
     db.accounts().deleteKeys(ImmutableSet.of(accountId));
+
+    // Delete in NoteDb
     deleteUserBranch(accountId);
-    evictAccount(accountId);
-  }
-
-  private void createUserBranch(Account account) throws IOException {
-    try (Repository repo = repoManager.openRepository(allUsersName);
-        ObjectInserter oi = repo.newObjectInserter()) {
-      String refName = RefNames.refsUsers(account.getId());
-      if (repo.exactRef(refName) != null) {
-        throw new IOException(
-            String.format(
-                "User branch %s for newly created account %s already exists.",
-                refName, account.getId().get()));
-      }
-      createUserBranch(
-          repo, oi, committerIdent, authorIdent, account.getId(), account.getRegisteredOn());
-    }
-  }
-
-  private void createUserBranchIfNeeded(Account account) throws IOException {
-    try (Repository repo = repoManager.openRepository(allUsersName);
-        ObjectInserter oi = repo.newObjectInserter()) {
-      if (repo.exactRef(RefNames.refsUsers(account.getId())) == null) {
-        createUserBranch(
-            repo, oi, committerIdent, authorIdent, account.getId(), account.getRegisteredOn());
-      }
-    }
   }
 
   public static void createUserBranch(
       Repository repo,
+      Project.NameKey project,
+      GitReferenceUpdated gitRefUpdated,
+      @Nullable IdentifiedUser user,
       ObjectInserter oi,
       PersonIdent committerIdent,
       PersonIdent authorIdent,
@@ -283,6 +357,7 @@
     if (result != Result.NEW) {
       throw new IOException(String.format("Failed to update ref %s: %s", refName, result.name()));
     }
+    gitRefUpdated.fire(project, ru, user != null ? user.getAccount() : null);
   }
 
   private static ObjectId createInitialEmptyCommit(
@@ -307,12 +382,18 @@
 
   private void deleteUserBranch(Account.Id accountId) throws IOException {
     try (Repository repo = repoManager.openRepository(allUsersName)) {
-      deleteUserBranch(repo, committerIdent, accountId);
+      deleteUserBranch(repo, allUsersName, gitRefUpdated, currentUser, committerIdent, accountId);
     }
   }
 
   public static void deleteUserBranch(
-      Repository repo, PersonIdent refLogIdent, Account.Id accountId) throws IOException {
+      Repository repo,
+      Project.NameKey project,
+      GitReferenceUpdated gitRefUpdated,
+      @Nullable IdentifiedUser user,
+      PersonIdent refLogIdent,
+      Account.Id accountId)
+      throws IOException {
     String refName = RefNames.refsUsers(accountId);
     Ref ref = repo.exactRef(refName);
     if (ref == null) {
@@ -329,11 +410,37 @@
     if (result != Result.FORCED) {
       throw new IOException(String.format("Failed to delete ref %s: %s", refName, result.name()));
     }
+    gitRefUpdated.fire(project, ru, user != null ? user.getAccount() : null);
   }
 
-  private void evictAccount(Account.Id accountId) throws IOException {
-    if (accountCache != null) {
-      accountCache.evict(accountId);
+  private AccountConfig read(Account.Id accountId) throws IOException, ConfigInvalidException {
+    try (Repository repo = repoManager.openRepository(allUsersName)) {
+      AccountConfig accountConfig = new AccountConfig(emailValidator, accountId);
+      accountConfig.load(repo);
+      return accountConfig;
     }
   }
+
+  private void commitNew(AccountConfig accountConfig) throws IOException {
+    // When creating a new account we must allow empty commits so that the user branch gets created
+    // with an empty commit when no account properties are set and hence no 'account.config' file
+    // will be created.
+    commit(accountConfig, true);
+  }
+
+  private void commit(AccountConfig accountConfig) throws IOException {
+    commit(accountConfig, false);
+  }
+
+  private void commit(AccountConfig accountConfig, boolean allowEmptyCommit) throws IOException {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create()) {
+      md.setAllowEmpty(allowEmptyCommit);
+      accountConfig.commit(md);
+    }
+  }
+
+  @FunctionalInterface
+  private static interface MetaDataUpdateFactory {
+    MetaDataUpdate create() throws IOException;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
index d617365..ff367e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -17,7 +17,7 @@
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
 
 import com.google.gerrit.audit.AuditService;
-import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
@@ -113,12 +113,15 @@
   }
 
   @Override
-  public Response<AccountInfo> apply(TopLevelResource rsrc, AccountInput input)
+  public Response<AccountInfo> apply(TopLevelResource rsrc, @Nullable AccountInput input)
       throws BadRequestException, ResourceConflictException, UnprocessableEntityException,
           OrmException, IOException, ConfigInvalidException {
-    if (input == null) {
-      input = new AccountInput();
-    }
+    return apply(input != null ? input : new AccountInput());
+  }
+
+  public Response<AccountInfo> apply(AccountInput input)
+      throws BadRequestException, ResourceConflictException, UnprocessableEntityException,
+          OrmException, IOException, ConfigInvalidException {
     if (input.username != null && !username.equals(input.username)) {
       throw new BadRequestException("username must match URL");
     }
@@ -171,10 +174,15 @@
       }
     }
 
-    Account a = new Account(id, TimeUtil.nowTs());
-    a.setFullName(input.name);
-    a.setPreferredEmail(input.email);
-    accountsUpdate.create().insert(db, a);
+    accountsUpdate
+        .create()
+        .insert(
+            db,
+            id,
+            a -> {
+              a.setFullName(input.name);
+              a.setPreferredEmail(input.email);
+            });
 
     for (AccountGroup.Id groupId : groups) {
       AccountGroupMember m = new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
index 3a996f2..1fa8ed3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
@@ -31,6 +31,7 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.concurrent.atomic.AtomicBoolean;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @RequiresCapability(GlobalCapability.MODIFY_ACCOUNT)
 @Singleton
@@ -53,7 +54,7 @@
 
   @Override
   public Response<?> apply(AccountResource rsrc, Input input)
-      throws RestApiException, OrmException, IOException {
+      throws RestApiException, OrmException, IOException, ConfigInvalidException {
     if (self.get() == rsrc.getUser()) {
       throw new ResourceConflictException("cannot deactivate own account");
     }
@@ -62,7 +63,7 @@
     Account account =
         accountsUpdate
             .create()
-            .atomicUpdate(
+            .update(
                 dbProvider.get(),
                 rsrc.getUser().getAccountId(),
                 a -> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
index bc7c1d0..6051a95 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
@@ -28,6 +28,7 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.concurrent.atomic.AtomicBoolean;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @RequiresCapability(GlobalCapability.MODIFY_ACCOUNT)
 @Singleton
@@ -45,12 +46,12 @@
 
   @Override
   public Response<String> apply(AccountResource rsrc, Input input)
-      throws ResourceNotFoundException, OrmException, IOException {
+      throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException {
     AtomicBoolean alreadyActive = new AtomicBoolean(false);
     Account account =
         accountsUpdate
             .create()
-            .atomicUpdate(
+            .update(
                 dbProvider.get(),
                 rsrc.getUser().getAccountId(),
                 a -> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
index 0911820..792e71d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
@@ -35,6 +35,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class PutName implements RestModifyView<AccountResource, Input> {
@@ -65,7 +66,7 @@
   @Override
   public Response<String> apply(AccountResource rsrc, Input input)
       throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
-          IOException, PermissionBackendException {
+          IOException, PermissionBackendException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()) {
       permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
@@ -73,7 +74,8 @@
   }
 
   public Response<String> apply(IdentifiedUser user, Input input)
-      throws MethodNotAllowedException, ResourceNotFoundException, OrmException, IOException {
+      throws MethodNotAllowedException, ResourceNotFoundException, OrmException, IOException,
+          ConfigInvalidException {
     if (input == null) {
       input = new Input();
     }
@@ -86,7 +88,7 @@
     Account account =
         accountsUpdate
             .create()
-            .atomicUpdate(dbProvider.get(), user.getAccountId(), a -> a.setFullName(newName));
+            .update(dbProvider.get(), user.getAccountId(), a -> a.setFullName(newName));
     if (account == null) {
       throw new ResourceNotFoundException("account not found");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
index d473d53..98d4ac5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
@@ -32,6 +32,7 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.concurrent.atomic.AtomicBoolean;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class PutPreferred implements RestModifyView<AccountResource.Email, Input> {
@@ -57,7 +58,7 @@
   @Override
   public Response<String> apply(AccountResource.Email rsrc, Input input)
       throws AuthException, ResourceNotFoundException, OrmException, IOException,
-          PermissionBackendException {
+          PermissionBackendException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()) {
       permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
@@ -65,12 +66,12 @@
   }
 
   public Response<String> apply(IdentifiedUser user, String email)
-      throws ResourceNotFoundException, OrmException, IOException {
+      throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException {
     AtomicBoolean alreadyPreferred = new AtomicBoolean(false);
     Account account =
         accountsUpdate
             .create()
-            .atomicUpdate(
+            .update(
                 dbProvider.get(),
                 user.getAccountId(),
                 a -> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
index 81c0694..136fc68 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
@@ -33,6 +33,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class PutStatus implements RestModifyView<AccountResource, Input> {
@@ -66,7 +67,7 @@
   @Override
   public Response<String> apply(AccountResource rsrc, Input input)
       throws AuthException, ResourceNotFoundException, OrmException, IOException,
-          PermissionBackendException {
+          PermissionBackendException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()) {
       permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
@@ -74,7 +75,7 @@
   }
 
   public Response<String> apply(IdentifiedUser user, Input input)
-      throws ResourceNotFoundException, OrmException, IOException {
+      throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException {
     if (input == null) {
       input = new Input();
     }
@@ -83,7 +84,7 @@
     Account account =
         accountsUpdate
             .create()
-            .atomicUpdate(
+            .update(
                 dbProvider.get(),
                 user.getAccountId(),
                 a -> a.setStatus(Strings.nullToEmpty(newStatus)));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index 7562801..ce31cac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -27,6 +27,7 @@
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.CmdLineParser;
 import org.kohsuke.args4j.OptionDef;
@@ -82,8 +83,12 @@
             throw new CmdLineException(owner, "user \"" + token + "\" not found");
         }
       }
-    } catch (OrmException | IOException e) {
+    } catch (OrmException e) {
       throw new CmdLineException(owner, "database is down");
+    } catch (IOException e) {
+      throw new CmdLineException(owner, "Failed to load account", e);
+    } catch (ConfigInvalidException e) {
+      throw new CmdLineException(owner, "Invalid account config", e);
     }
     setter.addValue(accountId);
     return 1;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index 7bf4214..5073e4a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -41,7 +41,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import java.util.Collection;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class Abandon extends RetryingRestModifyView<ChangeResource, AbandonInput, ChangeInfo>
@@ -68,7 +70,8 @@
   @Override
   protected ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource req, AbandonInput input)
-      throws RestApiException, UpdateException, OrmException, PermissionBackendException {
+      throws RestApiException, UpdateException, OrmException, PermissionBackendException,
+          IOException, ConfigInvalidException {
     req.permissions().database(dbProvider).check(ChangePermission.ABANDON);
 
     NotifyHandling notify = input.notify == null ? defaultNotify(req.getControl()) : input.notify;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index b4867c4..993148e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -39,6 +39,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class CherryPick
@@ -67,7 +68,7 @@
   public ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource rsrc, CherryPickInput input)
       throws OrmException, IOException, UpdateException, RestApiException,
-          PermissionBackendException {
+          PermissionBackendException, ConfigInvalidException {
     input.parent = input.parent == null ? 1 : input.parent;
     if (input.message == null || input.message.trim().isEmpty()) {
       throw new BadRequestException("message must be non-empty");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index 755379d..fbb692c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -62,6 +62,7 @@
 import java.sql.Timestamp;
 import java.util.List;
 import java.util.TimeZone;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -124,7 +125,7 @@
       CherryPickInput input,
       RefControl refControl)
       throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
-          UpdateException, RestApiException {
+          UpdateException, RestApiException, ConfigInvalidException {
     return cherryPick(
         batchUpdateFactory,
         change.getId(),
@@ -148,7 +149,7 @@
       CherryPickInput input,
       RefControl destRefControl)
       throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
-          UpdateException, RestApiException {
+          UpdateException, RestApiException, ConfigInvalidException {
 
     IdentifiedUser identifiedUser = user.get();
     try (Repository git = gitManager.openRepository(project);
@@ -320,7 +321,7 @@
       ChangeControl destCtl,
       CodeReviewCommit cherryPickCommit,
       CherryPickInput input)
-      throws IOException, OrmException, BadRequestException {
+      throws IOException, OrmException, BadRequestException, ConfigInvalidException {
     Change destChange = destCtl.getChange();
     PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
     PatchSet current = psUtil.current(dbProvider.get(), destCtl.getNotes());
@@ -343,7 +344,7 @@
       Branch.NameKey sourceBranch,
       ObjectId sourceCommit,
       CherryPickInput input)
-      throws OrmException, IOException, BadRequestException {
+      throws OrmException, IOException, BadRequestException, ConfigInvalidException {
     Change.Id changeId = new Change.Id(seq.nextChangeId());
     ChangeInserter ins =
         changeInserterFactory.create(changeId, cherryPickCommit, refName).setTopic(topic);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
index 41f0463..ac70e4a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
@@ -39,6 +39,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.revwalk.RevCommit;
 
 @Singleton
@@ -67,7 +68,7 @@
   public ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, CommitResource rsrc, CherryPickInput input)
       throws OrmException, IOException, UpdateException, RestApiException,
-          PermissionBackendException {
+          PermissionBackendException, ConfigInvalidException {
     RevCommit commit = rsrc.getCommit();
     String message = Strings.nullToEmpty(input.message).trim();
     input.message = message.isEmpty() ? commit.getFullMessage() : message;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index d0e489b..9fcb13d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -68,6 +68,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -226,7 +227,7 @@
       if (accounts.get(db.get(), change().getOwner()) == null) {
         problem("Missing change owner: " + change().getOwner());
       }
-    } catch (OrmException e) {
+    } catch (OrmException | IOException | ConfigInvalidException e) {
       error("Failed to look up owner", e);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
index cca9cb6..bbe04f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -74,6 +74,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.TimeZone;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
@@ -151,7 +152,7 @@
   protected Response<ChangeInfo> applyImpl(
       BatchUpdate.Factory updateFactory, TopLevelResource parent, ChangeInput input)
       throws OrmException, IOException, InvalidChangeOperationException, RestApiException,
-          UpdateException, PermissionBackendException {
+          UpdateException, PermissionBackendException, ConfigInvalidException {
     if (Strings.isNullOrEmpty(input.project)) {
       throw new BadRequestException("project must be non-empty");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java
index 8516615..ccc7587 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java
@@ -31,10 +31,12 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class NotifyUtil {
@@ -76,7 +78,7 @@
 
   public ListMultimap<RecipientType, Account.Id> resolveAccounts(
       @Nullable Map<RecipientType, NotifyInfo> notifyDetails)
-      throws OrmException, BadRequestException {
+      throws OrmException, BadRequestException, IOException, ConfigInvalidException {
     if (isNullOrEmpty(notifyDetails)) {
       return ImmutableListMultimap.of();
     }
@@ -96,7 +98,7 @@
   }
 
   private List<Account.Id> find(ReviewDb db, List<String> nameOrEmails)
-      throws OrmException, BadRequestException {
+      throws OrmException, BadRequestException, IOException, ConfigInvalidException {
     List<String> missing = new ArrayList<>(nameOrEmails.size());
     List<Account.Id> r = new ArrayList<>(nameOrEmails.size());
     for (String nameOrEmail : nameOrEmails) {
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 d6ad28b..f5a1856 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
@@ -126,6 +126,7 @@
 import java.util.Objects;
 import java.util.OptionalInt;
 import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
@@ -190,14 +191,14 @@
   protected Response<ReviewResult> applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input)
       throws RestApiException, UpdateException, OrmException, IOException,
-          PermissionBackendException {
+          PermissionBackendException, ConfigInvalidException {
     return apply(updateFactory, revision, input, TimeUtil.nowTs());
   }
 
   public Response<ReviewResult> apply(
       BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input, Timestamp ts)
       throws RestApiException, UpdateException, OrmException, IOException,
-          PermissionBackendException {
+          PermissionBackendException, ConfigInvalidException {
     // Respect timestamp, but truncate at change created-on time.
     ts = Ordering.natural().max(ts, revision.getChange().getCreatedOn());
     if (revision.getEdit().isPresent()) {
@@ -367,7 +368,7 @@
 
   private RevisionResource onBehalfOf(RevisionResource rev, ReviewInput in)
       throws BadRequestException, AuthException, UnprocessableEntityException, OrmException,
-          PermissionBackendException {
+          PermissionBackendException, IOException, ConfigInvalidException {
     if (in.labels == null || in.labels.isEmpty()) {
       throw new AuthException(
           String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index c7b0031..a7711b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -76,6 +76,7 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
@@ -148,7 +149,7 @@
   protected AddReviewerResult applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, AddReviewerInput input)
       throws IOException, OrmException, RestApiException, UpdateException,
-          PermissionBackendException {
+          PermissionBackendException, ConfigInvalidException {
     if (input.reviewer == null) {
       throw new BadRequestException("missing reviewer field");
     }
@@ -170,7 +171,7 @@
 
   public Addition prepareApplication(
       ChangeResource rsrc, AddReviewerInput input, boolean allowGroup)
-      throws OrmException, IOException, PermissionBackendException {
+      throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
     String reviewer = input.reviewer;
     ReviewerState state = input.state();
     NotifyHandling notify = input.notify;
@@ -219,7 +220,7 @@
       ListMultimap<RecipientType, Account.Id> accountsToNotify,
       boolean allowGroup,
       boolean allowByEmail)
-      throws OrmException, PermissionBackendException {
+      throws OrmException, PermissionBackendException, IOException, ConfigInvalidException {
     Account.Id accountId = null;
     try {
       accountId = accounts.parse(reviewer).getAccountId();
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 53f127e..9ef445d 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
@@ -45,6 +45,7 @@
 import java.io.OutputStream;
 import java.util.Collection;
 import org.apache.commons.compress.archivers.ArchiveOutputStream;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
@@ -82,7 +83,7 @@
 
   @Override
   public BinaryResult apply(RevisionResource rsrc)
-      throws OrmException, RestApiException, UpdateException {
+      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
     if (Strings.isNullOrEmpty(format)) {
       throw new BadRequestException("format is not specified");
     }
@@ -110,7 +111,7 @@
   }
 
   private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormat f)
-      throws OrmException, RestApiException, UpdateException {
+      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
     ReviewDb db = dbProvider.get();
     ChangeControl control = rsrc.getControl();
     IdentifiedUser caller = control.getUser().asIdentifiedUser();
@@ -125,7 +126,12 @@
           .setContentType(f.getMimeType())
           .setAttachmentName("submit-preview-" + change.getChangeId() + "." + format);
       return bin;
-    } catch (OrmException | RestApiException | UpdateException | RuntimeException e) {
+    } catch (OrmException
+        | RestApiException
+        | UpdateException
+        | IOException
+        | ConfigInvalidException
+        | RuntimeException e) {
       op.close();
       throw e;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
index eab06fb..cbb5fa3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
@@ -35,6 +35,7 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class PublishChangeEdit
@@ -85,7 +86,8 @@
     @Override
     protected Response<?> applyImpl(
         BatchUpdate.Factory updateFactory, ChangeResource rsrc, PublishChangeEditInput in)
-        throws IOException, OrmException, RestApiException, UpdateException {
+        throws IOException, OrmException, RestApiException, UpdateException,
+            ConfigInvalidException {
       CreateChange.checkValidCLA(rsrc.getControl().getProjectControl());
       Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
       if (!edit.isPresent()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
index b07d24b..a0862d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
@@ -42,6 +42,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class PutAssignee extends RetryingRestModifyView<ChangeResource, AssigneeInput, AccountInfo>
@@ -73,7 +74,7 @@
   protected AccountInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, AssigneeInput input)
       throws RestApiException, UpdateException, OrmException, IOException,
-          PermissionBackendException {
+          PermissionBackendException, ConfigInvalidException {
     rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
 
     input.assignee = Strings.nullToEmpty(input.assignee).trim();
@@ -109,7 +110,7 @@
   }
 
   private Addition addAssigneeAsCC(ChangeResource rsrc, String assignee)
-      throws OrmException, IOException, PermissionBackendException {
+      throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
     AddReviewerInput reviewerInput = new AddReviewerInput();
     reviewerInput.reviewer = assignee;
     reviewerInput.state = ReviewerState.CC;
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
index dcb5766..95e29ea 100644
--- 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
@@ -51,6 +51,7 @@
 import javax.inject.Inject;
 import javax.inject.Provider;
 import javax.inject.Singleton;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -107,7 +108,7 @@
   protected Response<String> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource resource, Input input)
       throws IOException, UnchangedCommitMessageException, RestApiException, UpdateException,
-          PermissionBackendException, OrmException {
+          PermissionBackendException, OrmException, ConfigInvalidException {
     PatchSet ps = psUtil.current(db.get(), resource.getNotes());
     if (ps == null) {
       throw new ResourceConflictException("current revision is missing");
@@ -150,6 +151,7 @@
             psInserterFactory.create(resource.getControl(), psId, newCommit);
         inserter.setMessage(
             String.format("Patch Set %s: Commit message was updated.", psId.getId()));
+        inserter.setDescription("Edit commit message");
         inserter.setNotify(input.notify);
         inserter.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
         bu.addOp(resource.getChange().getId(), inserter);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
index 0762f0e..8794083 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
@@ -30,7 +30,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import java.util.Collection;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class Reviewers implements ChildCollection<ChangeResource, ReviewerResource> {
@@ -69,7 +71,8 @@
 
   @Override
   public ReviewerResource parse(ChangeResource rsrc, IdString id)
-      throws OrmException, ResourceNotFoundException, AuthException {
+      throws OrmException, ResourceNotFoundException, AuthException, IOException,
+          ConfigInvalidException {
     Address address = Address.tryParse(id.get());
 
     Account.Id accountId = null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java
index 2dc7ad8..be8bce0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java
@@ -31,7 +31,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import java.util.Collection;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class RevisionReviewers implements ChildCollection<RevisionResource, ReviewerResource> {
@@ -70,7 +72,8 @@
 
   @Override
   public ReviewerResource parse(RevisionResource rsrc, IdString id)
-      throws OrmException, ResourceNotFoundException, AuthException, MethodNotAllowedException {
+      throws OrmException, ResourceNotFoundException, AuthException, MethodNotAllowedException,
+          IOException, ConfigInvalidException {
     if (!rsrc.isCurrent()) {
       throw new MethodNotAllowedException("Cannot access on non-current patch set");
     }
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 3fb8a79..3dd467a 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
@@ -73,6 +73,7 @@
 import java.util.Map;
 import java.util.Queue;
 import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
@@ -202,7 +203,7 @@
   @Override
   public Output apply(RevisionResource rsrc, SubmitInput input)
       throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
-          PermissionBackendException, UpdateException {
+          PermissionBackendException, UpdateException, ConfigInvalidException {
     input.onBehalfOf = Strings.emptyToNull(input.onBehalfOf);
     IdentifiedUser submitter;
     if (input.onBehalfOf != null) {
@@ -216,7 +217,7 @@
   }
 
   public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
-      throws OrmException, RestApiException, IOException, UpdateException {
+      throws OrmException, RestApiException, IOException, UpdateException, ConfigInvalidException {
     Change change = rsrc.getChange();
     if (!change.getStatus().isOpen()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
@@ -249,7 +250,7 @@
         if (msg != null) {
           throw new ResourceConflictException(msg.getMessage());
         }
-        //$FALL-THROUGH$
+        // $FALL-THROUGH$
       case ABANDONED:
       case DRAFT:
       default:
@@ -478,7 +479,8 @@
   }
 
   private IdentifiedUser onBehalfOf(RevisionResource rsrc, SubmitInput in)
-      throws AuthException, UnprocessableEntityException, OrmException, PermissionBackendException {
+      throws AuthException, UnprocessableEntityException, OrmException, PermissionBackendException,
+          IOException, ConfigInvalidException {
     PermissionBackend.ForChange perm = rsrc.permissions().database(dbProvider);
     perm.check(ChangePermission.SUBMIT);
     perm.check(ChangePermission.SUBMIT_AS);
@@ -527,7 +529,7 @@
     @Override
     public ChangeInfo apply(ChangeResource rsrc, SubmitInput input)
         throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
-            PermissionBackendException, UpdateException {
+            PermissionBackendException, UpdateException, ConfigInvalidException {
       PatchSet ps = psUtil.current(dbProvider.get(), rsrc.getNotes());
       if (ps == null) {
         throw new ResourceConflictException("current revision is missing");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
index 1daa7e3..178aeea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
@@ -34,6 +34,7 @@
 import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
 
@@ -66,7 +67,7 @@
 
   @Override
   public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
-      throws AuthException, BadRequestException, OrmException, IOException {
+      throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException {
     if (!self.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java
index 84db266..c89684c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java
@@ -39,6 +39,7 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class CheckAccess implements RestModifyView<ConfigResource, AccessCheckInput> {
@@ -67,7 +68,8 @@
 
   @Override
   public AccessCheckInfo apply(ConfigResource unused, AccessCheckInput input)
-      throws OrmException, PermissionBackendException, RestApiException, IOException {
+      throws OrmException, PermissionBackendException, RestApiException, IOException,
+          ConfigInvalidException {
     permissionBackend.user(currentUser.get()).check(GlobalPermission.ADMINISTRATE_SERVER);
 
     if (input == null) {
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 6bd7c57..8612737 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
@@ -126,6 +126,7 @@
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.MergeValidationListener;
 import com.google.gerrit.server.git.validators.MergeValidators;
+import com.google.gerrit.server.git.validators.MergeValidators.AccountValidator;
 import com.google.gerrit.server.git.validators.MergeValidators.ProjectConfigValidator;
 import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
 import com.google.gerrit.server.git.validators.OnSubmitValidators;
@@ -389,6 +390,7 @@
     bind(AnonymousUser.class);
 
     factory(AbandonOp.Factory.class);
+    factory(AccountValidator.Factory.class);
     factory(RefOperationValidators.Factory.class);
     factory(OnSubmitValidators.Factory.class);
     factory(MergeValidators.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
index 2ac6695..674a5c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
@@ -159,6 +159,7 @@
   }
 
   public void setGroupReference(String name, GroupReference value) {
-    setString(name, value.toConfigValue());
+    GroupReference groupRef = projectConfig.resolve(value);
+    setString(name, groupRef.toConfigValue());
   }
 }
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 ce6b5c6..353cba2 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
@@ -88,6 +88,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -430,7 +431,7 @@
       boolean checkSubmitRules,
       SubmitInput submitInput,
       boolean dryrun)
-      throws OrmException, RestApiException, UpdateException {
+      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
     this.submitInput = submitInput;
     this.accountsToNotify = notifyUtil.resolveAccounts(submitInput.notifyDetails);
     this.dryrun = dryrun;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 0c73d05..91379fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -416,7 +416,13 @@
   }
 
   public GroupReference resolve(GroupReference group) {
-    return groupList.resolve(group);
+    GroupReference groupRef = groupList.resolve(group);
+    if (groupRef != null
+        && groupRef.getUUID() != null
+        && !groupsByName.containsKey(groupRef.getName())) {
+      groupsByName.put(groupRef.getName(), groupRef);
+    }
+    return groupRef;
   }
 
   /** @return the group reference, if the group is used by at least one rule. */
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 15a0935..6b3f173 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
@@ -84,7 +84,6 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.SetHashtagsOp;
@@ -155,6 +154,7 @@
 import java.util.concurrent.Future;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.Constants;
@@ -301,7 +301,6 @@
   private final Sequences seq;
   private final Provider<InternalChangeQuery> queryProvider;
   private final ChangeNotes.Factory notesFactory;
-  private final Accounts accounts;
   private final AccountsUpdate.Server accountsUpdate;
   private final AccountResolver accountResolver;
   private final PermissionBackend permissionBackend;
@@ -376,7 +375,6 @@
       Sequences seq,
       Provider<InternalChangeQuery> queryProvider,
       ChangeNotes.Factory notesFactory,
-      Accounts accounts,
       AccountsUpdate.Server accountsUpdate,
       AccountResolver accountResolver,
       PermissionBackend permissionBackend,
@@ -416,7 +414,6 @@
     this.seq = seq;
     this.queryProvider = queryProvider;
     this.notesFactory = notesFactory;
-    this.accounts = accounts;
     this.accountsUpdate = accountsUpdate;
     this.accountResolver = accountResolver;
     this.permissionBackend = permissionBackend;
@@ -815,7 +812,11 @@
       } catch (ResourceConflictException e) {
         addMessage(e.getMessage());
         reject(magicBranchCmd, "conflict");
-      } catch (RestApiException | OrmException | UpdateException e) {
+      } catch (RestApiException
+          | OrmException
+          | UpdateException
+          | IOException
+          | ConfigInvalidException e) {
         logError("Error submitting changes to " + project.getName(), e);
         reject(magicBranchCmd, "error during submit");
       }
@@ -1070,15 +1071,19 @@
     }
 
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
-    if (ctl.canCreate(rp.getRepository(), obj)) {
-      if (!validRefOperation(cmd)) {
-        return;
-      }
-      validateNewCommits(ctl, cmd);
-      actualCommands.add(cmd);
-    } else {
-      reject(cmd, "prohibited by Gerrit: create access denied for " + cmd.getRefName());
+    String rejectReason = ctl.canCreate(rp.getRepository(), obj);
+    if (rejectReason != null) {
+      reject(cmd, "prohibited by Gerrit: " + rejectReason);
+      return;
     }
+
+    if (!validRefOperation(cmd)) {
+      // validRefOperation sets messages, so no need to provide more feedback.
+      return;
+    }
+
+    validateNewCommits(ctl, cmd);
+    actualCommands.add(cmd);
   }
 
   private void parseUpdate(ReceiveCommand cmd) throws PermissionBackendException {
@@ -2254,7 +2259,7 @@
   }
 
   private void submit(Collection<CreateRequest> create, Collection<ReplaceRequest> replace)
-      throws OrmException, RestApiException, UpdateException {
+      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
     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);
@@ -2779,13 +2784,22 @@
 
         if (defaultName && user.hasEmailAddress(c.getCommitterIdent().getEmailAddress())) {
           try {
-            Account a = accounts.get(db, user.getAccountId());
-            if (a != null && Strings.isNullOrEmpty(a.getFullName())) {
-              a.setFullName(c.getCommitterIdent().getName());
-              accountsUpdate.create().update(db, a);
-              user.getAccount().setFullName(a.getFullName());
+            String committerName = c.getCommitterIdent().getName();
+            Account account =
+                accountsUpdate
+                    .create()
+                    .update(
+                        db,
+                        user.getAccountId(),
+                        a -> {
+                          if (Strings.isNullOrEmpty(a.getFullName())) {
+                            a.setFullName(committerName);
+                          }
+                        });
+            if (account != null && Strings.isNullOrEmpty(account.getFullName())) {
+              user.getAccount().setFullName(account.getFullName());
             }
-          } catch (OrmException e) {
+          } catch (OrmException | IOException | ConfigInvalidException e) {
             logWarn("Cannot default full_name", e);
           } finally {
             defaultName = false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
index 12ad776..76f1369 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
@@ -25,7 +25,6 @@
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index 6b6d97f..8b7df19 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -75,6 +75,7 @@
   }
 
   protected RevCommit revision;
+  protected RevWalk rw;
   protected ObjectReader reader;
   protected ObjectInserter inserter;
   protected DirCache newTree;
@@ -153,11 +154,13 @@
    * @throws ConfigInvalidException
    */
   public void load(RevWalk walk, ObjectId id) throws IOException, ConfigInvalidException {
+    this.rw = walk;
     this.reader = walk.getObjectReader();
     try {
       revision = id != null ? walk.parseCommit(id) : null;
       onLoad();
     } finally {
+      walk = null;
       reader = null;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index ff755ea..bdb0db9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountConfig;
 import com.google.gerrit.server.account.WatchConfig;
 import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
 import com.google.gerrit.server.config.AllUsersName;
@@ -57,9 +58,11 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.regex.Pattern;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
@@ -67,6 +70,7 @@
 import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.util.SystemReader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -120,7 +124,8 @@
               new ConfigValidator(refctl, rw, allUsers),
               new BannedCommitsValidator(rejectCommits),
               new PluginCommitValidationListener(pluginValidators),
-              new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker)));
+              new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
+              new AccountValidator(allUsers)));
     }
 
     public CommitValidators forGerritCommits(
@@ -135,7 +140,8 @@
               new ChangeIdValidator(refctl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
               new ConfigValidator(refctl, rw, allUsers),
               new PluginCommitValidationListener(pluginValidators),
-              new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker)));
+              new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
+              new AccountValidator(allUsers)));
     }
 
     public CommitValidators forMergedCommits(PermissionBackend.ForRef perm, RefControl refControl) {
@@ -679,6 +685,60 @@
     }
   }
 
+  /** Rejects updates to 'account.config' in user branches. */
+  public static class AccountValidator implements CommitValidationListener {
+    private final AllUsersName allUsers;
+
+    public AccountValidator(AllUsersName allUsers) {
+      this.allUsers = allUsers;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+        throws CommitValidationException {
+      if (!allUsers.equals(receiveEvent.project.getNameKey())) {
+        return Collections.emptyList();
+      }
+
+      if (receiveEvent.command.getRefName().startsWith(MagicBranch.NEW_CHANGE)) {
+        // no validation on push for review, will be checked on submit by
+        // MergeValidators.AccountValidator
+        return Collections.emptyList();
+      }
+
+      Account.Id accountId = Account.Id.fromRef(receiveEvent.refName);
+      if (accountId == null) {
+        return Collections.emptyList();
+      }
+
+      try {
+        ObjectId newBlobId = getAccountConfigBlobId(receiveEvent.revWalk, receiveEvent.commit);
+
+        ObjectId oldId = receiveEvent.command.getOldId();
+        ObjectId oldBlobId =
+            !ObjectId.zeroId().equals(oldId)
+                ? getAccountConfigBlobId(receiveEvent.revWalk, oldId)
+                : null;
+        if (!Objects.equals(oldBlobId, newBlobId)) {
+          throw new CommitValidationException("account update not allowed");
+        }
+      } catch (IOException e) {
+        String m = String.format("Validating update for account %s failed", accountId.get());
+        log.error(m, e);
+        throw new CommitValidationException(m, e);
+      }
+      return Collections.emptyList();
+    }
+
+    private ObjectId getAccountConfigBlobId(RevWalk rw, ObjectId id) throws IOException {
+      RevCommit commit = rw.parseCommit(id);
+      try (TreeWalk tw =
+          TreeWalk.forPath(rw.getObjectReader(), AccountConfig.ACCOUNT_CONFIG, commit.getTree())) {
+        return tw != null ? tw.getObjectId(0) : null;
+      }
+    }
+  }
+
   private static CommitValidationMessage invalidEmail(
       RevCommit c,
       String type,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 298c650..af9f6d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -20,12 +20,16 @@
 import com.google.gerrit.extensions.registration.DynamicMap.Entry;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountConfig;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -35,7 +39,10 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -48,6 +55,7 @@
 
   private final DynamicSet<MergeValidationListener> mergeValidationListeners;
   private final ProjectConfigValidator.Factory projectConfigValidatorFactory;
+  private final AccountValidator.Factory accountValidatorFactory;
 
   public interface Factory {
     MergeValidators create();
@@ -56,9 +64,11 @@
   @Inject
   MergeValidators(
       DynamicSet<MergeValidationListener> mergeValidationListeners,
-      ProjectConfigValidator.Factory projectConfigValidatorFactory) {
+      ProjectConfigValidator.Factory projectConfigValidatorFactory,
+      AccountValidator.Factory accountValidatorFactory) {
     this.mergeValidationListeners = mergeValidationListeners;
     this.projectConfigValidatorFactory = projectConfigValidatorFactory;
+    this.accountValidatorFactory = accountValidatorFactory;
   }
 
   public void validatePreMerge(
@@ -72,7 +82,8 @@
     List<MergeValidationListener> validators =
         ImmutableList.of(
             new PluginMergeValidationListener(mergeValidationListeners),
-            projectConfigValidatorFactory.create());
+            projectConfigValidatorFactory.create(),
+            accountValidatorFactory.create());
 
     for (MergeValidationListener validator : validators) {
       validator.onPreMerge(repo, commit, destProject, destBranch, patchSetId, caller);
@@ -210,4 +221,59 @@
       }
     }
   }
+
+  public static class AccountValidator implements MergeValidationListener {
+    public interface Factory {
+      AccountValidator create();
+    }
+
+    private final Provider<ReviewDb> dbProvider;
+    private final AllUsersName allUsersName;
+    private final ChangeData.Factory changeDataFactory;
+
+    @Inject
+    public AccountValidator(
+        Provider<ReviewDb> dbProvider,
+        AllUsersName allUsersName,
+        ChangeData.Factory changeDataFactory) {
+      this.dbProvider = dbProvider;
+      this.allUsersName = allUsersName;
+      this.changeDataFactory = changeDataFactory;
+    }
+
+    @Override
+    public void onPreMerge(
+        Repository repo,
+        CodeReviewCommit commit,
+        ProjectState destProject,
+        Branch.NameKey destBranch,
+        PatchSet.Id patchSetId,
+        IdentifiedUser caller)
+        throws MergeValidationException {
+      if (!allUsersName.equals(destProject.getProject().getNameKey())
+          || Account.Id.fromRef(destBranch.get()) == null) {
+        return;
+      }
+
+      if (commit.getParentCount() > 1) {
+        // for merge commits we cannot ensure that the 'account.config' file is not modified, since
+        // for merge commits file modifications that come in through the merge don't appear in the
+        // file list that is returned by ChangeData#currentFilePaths()
+        throw new MergeValidationException("cannot submit merge commit to user branch");
+      }
+
+      ChangeData cd =
+          changeDataFactory.create(
+              dbProvider.get(), destProject.getProject().getNameKey(), patchSetId.getParentKey());
+      try {
+        if (cd.currentFilePaths().contains(AccountConfig.ACCOUNT_CONFIG)) {
+          throw new MergeValidationException(
+              String.format("update of %s not allowed", AccountConfig.ACCOUNT_CONFIG));
+        }
+      } catch (OrmException e) {
+        log.error("Cannot validate account update", e);
+        throw new MergeValidationException("account validation unavailable");
+      }
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
index 5c1a292..04be41e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -52,6 +52,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class AddMembers implements RestModifyView<GroupResource, Input> {
@@ -115,7 +116,7 @@
   @Override
   public List<AccountInfo> apply(GroupResource resource, Input input)
       throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
-          IOException {
+          IOException, ConfigInvalidException {
     AccountGroup internalGroup = resource.toAccountGroup();
     if (internalGroup == null) {
       throw new MethodNotAllowedException();
@@ -143,7 +144,8 @@
   }
 
   Account findAccount(String nameOrEmailOrId)
-      throws AuthException, UnprocessableEntityException, OrmException, IOException {
+      throws AuthException, UnprocessableEntityException, OrmException, IOException,
+          ConfigInvalidException {
     try {
       return accounts.parse(nameOrEmailOrId).getAccount();
     } catch (UnprocessableEntityException e) {
@@ -235,7 +237,7 @@
     @Override
     public AccountInfo apply(GroupResource resource, PutMember.Input input)
         throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
-            IOException {
+            IOException, ConfigInvalidException {
       AddMembers.Input in = new AddMembers.Input();
       in._oneMember = id;
       try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
index d692e59..af92acf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
@@ -55,6 +55,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 
@@ -115,7 +116,7 @@
   @Override
   public GroupInfo apply(TopLevelResource resource, GroupInput input)
       throws AuthException, BadRequestException, UnprocessableEntityException,
-          ResourceConflictException, OrmException, IOException {
+          ResourceConflictException, OrmException, IOException, ConfigInvalidException {
     if (input == null) {
       input = new GroupInput();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
index d9b1c3d..6be46d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
@@ -38,6 +38,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class DeleteMembers implements RestModifyView<GroupResource, Input> {
@@ -64,7 +65,7 @@
   @Override
   public Response<?> apply(GroupResource resource, Input input)
       throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
-          IOException {
+          IOException, ConfigInvalidException {
     AccountGroup internalGroup = resource.toAccountGroup();
     if (internalGroup == null) {
       throw new MethodNotAllowedException();
@@ -125,7 +126,7 @@
     @Override
     public Response<?> apply(MemberResource resource, Input input)
         throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
-            IOException {
+            IOException, ConfigInvalidException {
       AddMembers.Input in = new AddMembers.Input();
       in._oneMember = resource.getMember().getAccountId().toString();
       return delete.get().apply(resource, in);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
index 8f4d65e..dbc0676 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
@@ -32,6 +32,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class MembersCollection
@@ -63,7 +65,8 @@
 
   @Override
   public MemberResource parse(GroupResource parent, IdString id)
-      throws MethodNotAllowedException, AuthException, ResourceNotFoundException, OrmException {
+      throws MethodNotAllowedException, AuthException, ResourceNotFoundException, OrmException,
+          IOException, ConfigInvalidException {
     if (parent.toAccountGroup() == null) {
       throw new MethodNotAllowedException();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index a8b423a..636cce6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.WorkQueue;
@@ -108,6 +109,8 @@
     bind(GroupIndexCollection.class);
     listener().to(GroupIndexCollection.class);
     factory(GroupIndexerImpl.Factory.class);
+
+    DynamicSet.setOf(binder(), OnlineUpgradeListener.class);
   }
 
   @Provides
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java
index e40015a..8d14931 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -26,16 +27,26 @@
 public class OnlineReindexer<K, V, I extends Index<K, V>> {
   private static final Logger log = LoggerFactory.getLogger(OnlineReindexer.class);
 
+  private final String name;
   private final IndexCollection<K, V, I> indexes;
   private final SiteIndexer<K, V, I> batchIndexer;
-  private final int version;
+  private final int oldVersion;
+  private final int newVersion;
+  private final DynamicSet<OnlineUpgradeListener> listeners;
   private I index;
   private final AtomicBoolean running = new AtomicBoolean();
 
-  public OnlineReindexer(IndexDefinition<K, V, I> def, int version) {
+  public OnlineReindexer(
+      IndexDefinition<K, V, I> def,
+      int oldVersion,
+      int newVersion,
+      DynamicSet<OnlineUpgradeListener> listeners) {
+    this.name = def.getName();
     this.indexes = def.getIndexCollection();
     this.batchIndexer = def.getSiteIndexer();
-    this.version = version;
+    this.oldVersion = oldVersion;
+    this.newVersion = newVersion;
+    this.listeners = listeners;
   }
 
   public void start() {
@@ -44,14 +55,21 @@
           new Thread() {
             @Override
             public void run() {
+              boolean ok = false;
               try {
                 reindex();
+                ok = true;
               } finally {
                 running.set(false);
+                if (!ok) {
+                  for (OnlineUpgradeListener listener : listeners) {
+                    listener.onFailure(name, oldVersion, newVersion);
+                  }
+                }
               }
             }
           };
-      t.setName(String.format("Reindex v%d-v%d", version(indexes.getSearchIndex()), version));
+      t.setName(String.format("Reindex v%d-v%d", version(indexes.getSearchIndex()), newVersion));
       t.start();
     }
   }
@@ -61,7 +79,7 @@
   }
 
   public int getVersion() {
-    return version;
+    return newVersion;
   }
 
   private static int version(Index<?, ?> i) {
@@ -69,9 +87,14 @@
   }
 
   private void reindex() {
+    for (OnlineUpgradeListener listener : listeners) {
+      listener.onStart(name, oldVersion, newVersion);
+    }
     index =
         checkNotNull(
-            indexes.getWriteIndex(version), "not an active write schema version: %s", version);
+            indexes.getWriteIndex(newVersion),
+            "not an active write schema version: %s",
+            newVersion);
     log.info(
         "Starting online reindex from schema version {} to {}",
         version(indexes.getSearchIndex()),
@@ -88,6 +111,9 @@
     }
     log.info("Reindex to version {} complete", version(index));
     activateIndex();
+    for (OnlineUpgradeListener listener : listeners) {
+      listener.onSuccess(name, oldVersion, newVersion);
+    }
   }
 
   public void activateIndex() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgradeListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgradeListener.java
new file mode 100644
index 0000000..a2d13fe
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgradeListener.java
@@ -0,0 +1,45 @@
+// 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.index;
+
+/** Listener for online schema upgrade events. */
+public interface OnlineUpgradeListener {
+  /**
+   * Called before starting upgrading a single index.
+   *
+   * @param name index definition name.
+   * @param oldVersion old schema version.
+   * @param newVersion new schema version.
+   */
+  void onStart(String name, int oldVersion, int newVersion);
+
+  /**
+   * Called after successfully upgrading a single index.
+   *
+   * @param name index definition name.
+   * @param oldVersion old schema version.
+   * @param newVersion new schema version.
+   */
+  void onSuccess(String name, int oldVersion, int newVersion);
+
+  /**
+   * Called after failing to upgrade a single index.
+   *
+   * @param name index definition name.
+   * @param oldVersion old schema version.
+   * @param newVersion new schema version.
+   */
+  void onFailure(String name, int oldVersion, int newVersion);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgrader.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgrader.java
new file mode 100644
index 0000000..9fc3aa9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgrader.java
@@ -0,0 +1,39 @@
+// 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.index;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.Inject;
+
+/** Listener to handle upgrading index schema versions at startup. */
+public class OnlineUpgrader implements LifecycleListener {
+  private final VersionManager versionManager;
+
+  @Inject
+  OnlineUpgrader(VersionManager versionManager) {
+    this.versionManager = versionManager;
+  }
+
+  @Override
+  public void start() {
+    versionManager.startOnlineUpgrade();
+  }
+
+  @Override
+  public void stop() {
+    // Do nothing; reindexing threadpools are shut down in another listener, and indexes are closed
+    // on demand by IndexCollection.
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/AbstractVersionManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/VersionManager.java
similarity index 77%
rename from gerrit-server/src/main/java/com/google/gerrit/server/index/AbstractVersionManager.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/index/VersionManager.java
index 733fcce..697c9c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/AbstractVersionManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/VersionManager.java
@@ -15,11 +15,13 @@
 package com.google.gerrit.server.index;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexDefinition.IndexFactory;
 import com.google.inject.ProvisionException;
@@ -31,7 +33,11 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 
-public abstract class AbstractVersionManager implements LifecycleListener {
+public abstract class VersionManager implements LifecycleListener {
+  public static boolean getOnlineUpgrade(Config cfg) {
+    return cfg.getBoolean("index", null, "onlineUpgrade", true);
+  }
+
   public static class Version<V> {
     public final Schema<V> schema;
     public final int version;
@@ -48,22 +54,28 @@
   protected final boolean onlineUpgrade;
   protected final String runReindexMsg;
   protected final SitePaths sitePaths;
+
+  private final DynamicSet<OnlineUpgradeListener> listeners;
+
+  // The following fields must be accessed synchronized on this.
   protected final Map<String, IndexDefinition<?, ?, ?>> defs;
   protected final Map<String, OnlineReindexer<?, ?, ?>> reindexers;
 
-  protected AbstractVersionManager(
-      @GerritServerConfig Config cfg,
+  protected VersionManager(
       SitePaths sitePaths,
-      Collection<IndexDefinition<?, ?, ?>> defs) {
+      DynamicSet<OnlineUpgradeListener> listeners,
+      Collection<IndexDefinition<?, ?, ?>> defs,
+      boolean onlineUpgrade) {
     this.sitePaths = sitePaths;
+    this.listeners = listeners;
     this.defs = Maps.newHashMapWithExpectedSize(defs.size());
     for (IndexDefinition<?, ?, ?> def : defs) {
       this.defs.put(def.getName(), def);
     }
 
-    reindexers = Maps.newHashMapWithExpectedSize(defs.size());
-    onlineUpgrade = cfg.getBoolean("index", null, "onlineUpgrade", true);
-    runReindexMsg =
+    this.reindexers = Maps.newHashMapWithExpectedSize(defs.size());
+    this.onlineUpgrade = onlineUpgrade;
+    this.runReindexMsg =
         "No index versions for index '%s' ready; run java -jar "
             + sitePaths.gerrit_war.toAbsolutePath()
             + " reindex";
@@ -162,11 +174,37 @@
     synchronized (this) {
       if (!reindexers.containsKey(def.getName())) {
         int latest = write.get(0).version;
-        OnlineReindexer<K, V, I> reindexer = new OnlineReindexer<>(def, latest);
+        OnlineReindexer<K, V, I> reindexer =
+            new OnlineReindexer<>(def, search.version, latest, listeners);
         reindexers.put(def.getName(), reindexer);
-        if (onlineUpgrade && latest != search.version) {
-          reindexer.start();
-        }
+      }
+    }
+  }
+
+  synchronized void startOnlineUpgrade() {
+    checkState(onlineUpgrade, "online upgrade not enabled");
+    for (IndexDefinition<?, ?, ?> def : defs.values()) {
+      String name = def.getName();
+      IndexCollection<?, ?, ?> indexes = def.getIndexCollection();
+      Index<?, ?> search = indexes.getSearchIndex();
+      checkState(
+          search != null, "no search index ready for %s; should have failed at startup", name);
+      int searchVersion = search.getSchema().getVersion();
+
+      List<Index<?, ?>> write = ImmutableList.copyOf(indexes.getWriteIndexes());
+      checkState(
+          !write.isEmpty(),
+          "no write indexes set for %s; should have been initialized at startup",
+          name);
+      int latestWriteVersion = write.get(0).getSchema().getVersion();
+
+      if (latestWriteVersion != searchVersion) {
+        OnlineReindexer<?, ?, ?> reindexer = reindexers.get(name);
+        checkState(
+            reindexer != null,
+            "no reindexer found for %s; should have been initialized at startup",
+            name);
+        reindexer.start();
       }
     }
   }
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
index 0587b80..0cf78be 100644
--- 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
@@ -23,4 +23,8 @@
   MigrationException(String message) {
     super(message);
   }
+
+  MigrationException(String message, Throwable why) {
+    super(message, why);
+  }
 }
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
index a866314..62d67f5 100644
--- 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
@@ -18,6 +18,7 @@
 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.ConfigNotesMigration.SECTION_NOTE_DB;
 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_NOTE_DB_PRIMARY;
@@ -44,6 +45,8 @@
 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.reviewdb.server.ReviewDbWrapper;
+import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -52,11 +55,14 @@
 import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.notedb.ConfigNotesMigration;
+import com.google.gerrit.server.notedb.NoteDbTable;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.NotesMigrationState;
 import com.google.gerrit.server.notedb.PrimaryStorageMigrator;
 import com.google.gerrit.server.notedb.RepoSequence;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -71,6 +77,7 @@
 import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
@@ -86,12 +93,24 @@
 public class NoteDbMigrator implements AutoCloseable {
   private static final Logger log = LoggerFactory.getLogger(NoteDbMigrator.class);
 
+  private static final String AUTO_MIGRATE = "autoMigrate";
+
+  public static boolean getAutoMigrate(Config cfg) {
+    return cfg.getBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), AUTO_MIGRATE, false);
+  }
+
+  private static void setAutoMigrate(Config cfg, boolean autoMigrate) {
+    cfg.setBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), AUTO_MIGRATE, autoMigrate);
+  }
+
   public static class Builder {
     private final Config cfg;
     private final SitePaths sitePaths;
     private final SchemaFactory<ReviewDb> schemaFactory;
     private final GitRepositoryManager repoManager;
     private final AllProjectsName allProjects;
+    private final InternalUser.Factory userFactory;
+    private final ThreadLocalRequestContext requestContext;
     private final ChangeRebuilder rebuilder;
     private final WorkQueue workQueue;
     private final NotesMigration globalNotesMigration;
@@ -105,6 +124,7 @@
     private boolean trial;
     private boolean forceRebuild;
     private int sequenceGap = -1;
+    private boolean autoMigrate;
 
     @Inject
     Builder(
@@ -113,6 +133,8 @@
         SchemaFactory<ReviewDb> schemaFactory,
         GitRepositoryManager repoManager,
         AllProjectsName allProjects,
+        ThreadLocalRequestContext requestContext,
+        InternalUser.Factory userFactory,
         ChangeRebuilder rebuilder,
         WorkQueue workQueue,
         NotesMigration globalNotesMigration,
@@ -122,6 +144,8 @@
       this.schemaFactory = schemaFactory;
       this.repoManager = repoManager;
       this.allProjects = allProjects;
+      this.requestContext = requestContext;
+      this.userFactory = userFactory;
       this.rebuilder = rebuilder;
       this.workQueue = workQueue;
       this.globalNotesMigration = globalNotesMigration;
@@ -255,12 +279,29 @@
       return this;
     }
 
+    /**
+     * Enable auto-migration on subsequent daemon launches.
+     *
+     * <p>If true, prior to running any migration steps, sets the necessary configuration in {@code
+     * gerrit.config} to make {@code gerrit.war daemon} retry the migration on next startup, if it
+     * fails.
+     *
+     * @param autoMigrate whether to set auto-migration config.
+     * @return this.
+     */
+    public Builder setAutoMigrate(boolean autoMigrate) {
+      this.autoMigrate = autoMigrate;
+      return this;
+    }
+
     public NoteDbMigrator build() throws MigrationException {
       return new NoteDbMigrator(
           sitePaths,
           schemaFactory,
           repoManager,
           allProjects,
+          requestContext,
+          userFactory,
           rebuilder,
           globalNotesMigration,
           primaryStorageMigrator,
@@ -273,7 +314,8 @@
           stopAtState,
           trial,
           forceRebuild,
-          sequenceGap >= 0 ? sequenceGap : Sequences.getChangeSequenceGap(cfg));
+          sequenceGap >= 0 ? sequenceGap : Sequences.getChangeSequenceGap(cfg),
+          autoMigrate);
     }
   }
 
@@ -281,6 +323,8 @@
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final GitRepositoryManager repoManager;
   private final AllProjectsName allProjects;
+  private final ThreadLocalRequestContext requestContext;
+  private final InternalUser.Factory userFactory;
   private final ChangeRebuilder rebuilder;
   private final NotesMigration globalNotesMigration;
   private final PrimaryStorageMigrator primaryStorageMigrator;
@@ -293,12 +337,15 @@
   private final boolean trial;
   private final boolean forceRebuild;
   private final int sequenceGap;
+  private final boolean autoMigrate;
 
   private NoteDbMigrator(
       SitePaths sitePaths,
       SchemaFactory<ReviewDb> schemaFactory,
       GitRepositoryManager repoManager,
       AllProjectsName allProjects,
+      ThreadLocalRequestContext requestContext,
+      InternalUser.Factory userFactory,
       ChangeRebuilder rebuilder,
       NotesMigration globalNotesMigration,
       PrimaryStorageMigrator primaryStorageMigrator,
@@ -309,7 +356,8 @@
       NotesMigrationState stopAtState,
       boolean trial,
       boolean forceRebuild,
-      int sequenceGap)
+      int sequenceGap,
+      boolean autoMigrate)
       throws MigrationException {
     if (!changes.isEmpty() && !projects.isEmpty()) {
       throw new MigrationException("Cannot set both changes and projects");
@@ -322,6 +370,8 @@
     this.rebuilder = rebuilder;
     this.repoManager = repoManager;
     this.allProjects = allProjects;
+    this.requestContext = requestContext;
+    this.userFactory = userFactory;
     this.globalNotesMigration = globalNotesMigration;
     this.primaryStorageMigrator = primaryStorageMigrator;
     this.gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
@@ -333,6 +383,7 @@
     this.trial = trial;
     this.forceRebuild = forceRebuild;
     this.sequenceGap = sequenceGap;
+    this.autoMigrate = autoMigrate;
   }
 
   @Override
@@ -343,7 +394,7 @@
   public void migrate() throws OrmException, IOException {
     if (!changes.isEmpty() || !projects.isEmpty()) {
       throw new MigrationException(
-          "Cannot set changes or projects during auto-migration; call rebuild() instead");
+          "Cannot set changes or projects during full migration; call rebuild() instead");
     }
     Optional<NotesMigrationState> maybeState = loadState();
     if (!maybeState.isPresent()) {
@@ -360,6 +411,12 @@
       throw new MigrationException(
           "Cannot force rebuild changes; NoteDb is already the primary storage for some changes");
     }
+    if (autoMigrate) {
+      if (trial) {
+        throw new MigrationException("Auto-migration cannot be used with trial mode");
+      }
+      enableAutoMigrate();
+    }
 
     boolean rebuilt = false;
     while (state.compareTo(NOTE_DB_UNFUSED) < 0) {
@@ -438,7 +495,7 @@
           new RepoSequence(
               repoManager,
               allProjects,
-              Sequences.CHANGES,
+              Sequences.NAME_CHANGES,
               // If sequenceGap is 0, this writes into the sequence ref the same ID that is returned
               // by the call to seq.next() below. If we actually used this as a change ID, that
               // would be a problem, but we just discard it, so this is safe.
@@ -471,39 +528,40 @@
       allChanges = Streams.stream(db.changes().all()).map(Change::getId).collect(toList());
     }
 
-    List<ListenableFuture<Boolean>> futures =
-        allChanges
-            .stream()
-            .map(
-                id ->
-                    executor.submit(
-                        () -> {
-                          // TODO(dborowitz): Avoid reopening db if using a single thread.
-                          try (ReviewDb db = unwrapDb(schemaFactory.open())) {
-                            primaryStorageMigrator.migrateToNoteDbPrimary(id);
-                            return true;
-                          } catch (Exception e) {
-                            log.error("Error migrating primary storage for " + id, e);
-                            return false;
-                          }
-                        }))
-            .collect(toList());
+    try (ContextHelper contextHelper = new ContextHelper()) {
+      List<ListenableFuture<Boolean>> futures =
+          allChanges
+              .stream()
+              .map(
+                  id ->
+                      executor.submit(
+                          () -> {
+                            try (ManualRequestContext ctx = contextHelper.open()) {
+                              primaryStorageMigrator.migrateToNoteDbPrimary(id);
+                              return true;
+                            } catch (Exception e) {
+                              log.error("Error migrating primary storage for " + id, e);
+                              return false;
+                            }
+                          }))
+              .collect(toList());
 
-    boolean ok = futuresToBoolean(futures, "Error migrating primary storage");
-    double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
-    log.info(
-        String.format(
-            "Migrated primary storage of %d changes in %.01fs (%.01f/s)\n",
-            allChanges.size(), t, allChanges.size() / t));
-    if (!ok) {
-      throw new MigrationException("Migrating primary storage for some changes failed, see log");
+      boolean ok = futuresToBoolean(futures, "Error migrating primary storage");
+      double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
+      log.info(
+          String.format(
+              "Migrated primary storage of %d changes in %.01fs (%.01f/s)\n",
+              allChanges.size(), t, allChanges.size() / t));
+      if (!ok) {
+        throw new MigrationException("Migrating primary storage for some changes failed, see log");
+      }
     }
 
     return disableReviewDb(prev);
   }
 
   private NotesMigrationState disableReviewDb(NotesMigrationState prev) throws IOException {
-    return saveState(prev, NOTE_DB_UNFUSED);
+    return saveState(prev, NOTE_DB_UNFUSED, c -> setAutoMigrate(c, false));
   }
 
   private Optional<NotesMigrationState> loadState() throws IOException {
@@ -518,6 +576,14 @@
 
   private NotesMigrationState saveState(
       NotesMigrationState expectedOldState, NotesMigrationState newState) throws IOException {
+    return saveState(expectedOldState, newState, c -> {});
+  }
+
+  private NotesMigrationState saveState(
+      NotesMigrationState expectedOldState,
+      NotesMigrationState newState,
+      Consumer<Config> additionalUpdates)
+      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.
@@ -535,6 +601,7 @@
                     : "But could not parse the current state"));
       }
       ConfigNotesMigration.setConfigValues(gerritConfig, newState.migration());
+      additionalUpdates.accept(gerritConfig);
       gerritConfig.save();
 
       // Only set in-memory state once it's been persisted to storage.
@@ -544,6 +611,16 @@
     }
   }
 
+  private void enableAutoMigrate() throws MigrationException {
+    try {
+      gerritConfig.load();
+      setAutoMigrate(gerritConfig, true);
+      gerritConfig.save();
+    } catch (ConfigInvalidException | IOException e) {
+      throw new MigrationException("Error saving auto-migration config", e);
+    }
+  }
+
   public void rebuild() throws MigrationException, OrmException {
     if (!globalNotesMigration.commitChangeWrites()) {
       throw new MigrationException("Cannot rebuild without noteDb.changes.write=true");
@@ -552,51 +629,52 @@
     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 (ContextHelper contextHelper = new ContextHelper()) {
+      ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject =
+          getChangesByProject(contextHelper.getReviewDb());
+      List<Project.NameKey> projectNames =
+          Ordering.usingToString().sortedCopy(changesByProject.keySet());
+      for (Project.NameKey project : projectNames) {
+        ListenableFuture<Boolean> future =
+            executor.submit(
+                () -> {
+                  try {
+                    return rebuildProject(contextHelper.getReviewDb(), changesByProject, project);
+                  } catch (Exception e) {
+                    log.error("Error rebuilding project " + project, e);
+                    return false;
+                  }
+                });
+        futures.add(future);
+      }
 
-    boolean ok = futuresToBoolean(futures, "Error rebuilding projects");
-    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");
+      boolean ok = futuresToBoolean(futures, "Error rebuilding projects");
+      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()
+  private ImmutableListMultimap<Project.NameKey, Change.Id> getChangesByProject(ReviewDb db)
       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);
+    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(
@@ -658,4 +736,42 @@
       return false;
     }
   }
+
+  private class ContextHelper implements AutoCloseable {
+    private final Thread callingThread;
+    private ReviewDb db;
+
+    ContextHelper() {
+      callingThread = Thread.currentThread();
+    }
+
+    ManualRequestContext open() throws OrmException {
+      return new ManualRequestContext(
+          userFactory.create(),
+          // Reuse the same lazily-opened ReviewDb on the original calling thread, otherwise open
+          // SchemaFactory in the normal way.
+          Thread.currentThread().equals(callingThread) ? this::getReviewDb : schemaFactory,
+          requestContext);
+    }
+
+    synchronized ReviewDb getReviewDb() throws OrmException {
+      if (db == null) {
+        db =
+            new ReviewDbWrapper(unwrapDb(schemaFactory.open())) {
+              @Override
+              public void close() {
+                // Closed by ContextHelper#close.
+              }
+            };
+      }
+      return db;
+    }
+
+    @Override
+    public synchronized void close() {
+      if (db != null) {
+        db.close();
+      }
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
new file mode 100644
index 0000000..4d5951b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
@@ -0,0 +1,89 @@
+// 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 com.google.common.base.Stopwatch;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.OnlineUpgrader;
+import com.google.gerrit.server.index.VersionManager;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class OnlineNoteDbMigrator implements LifecycleListener {
+  private static final Logger log = LoggerFactory.getLogger(OnlineNoteDbMigrator.class);
+
+  public static class Module extends LifecycleModule {
+    @Override
+    public void configure() {
+      listener().to(OnlineNoteDbMigrator.class);
+    }
+  }
+
+  private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
+  private final OnlineUpgrader indexUpgrader;
+  private final boolean upgradeIndex;
+
+  @Inject
+  OnlineNoteDbMigrator(
+      @GerritServerConfig Config cfg,
+      Provider<NoteDbMigrator.Builder> migratorBuilderProvider,
+      OnlineUpgrader indexUpgrader) {
+    this.migratorBuilderProvider = migratorBuilderProvider;
+    this.indexUpgrader = indexUpgrader;
+    this.upgradeIndex = VersionManager.getOnlineUpgrade(cfg);
+  }
+
+  @Override
+  public void start() {
+    Thread t = new Thread(this::migrate);
+    t.setDaemon(true);
+    t.setName(getClass().getSimpleName());
+    t.start();
+  }
+
+  private void migrate() {
+    log.info("Starting online NoteDb migration");
+    if (upgradeIndex) {
+      log.info("Online index schema upgrades will be deferred until NoteDb migration is complete");
+    }
+    Stopwatch sw = Stopwatch.createStarted();
+    // TODO(dborowitz): Tune threads, maybe expose a progress monitor somewhere.
+    try (NoteDbMigrator migrator = migratorBuilderProvider.get().setAutoMigrate(true).build()) {
+      migrator.migrate();
+    } catch (Exception e) {
+      log.error("Error in online NoteDb migration", e);
+    }
+    log.info("Online NoteDb migration completed in {}s", sw.elapsed(TimeUnit.SECONDS));
+
+    if (upgradeIndex) {
+      log.info("Starting deferred index schema upgrades");
+      indexUpgrader.start();
+    }
+  }
+
+  @Override
+  public void stop() {
+    // Do nothing; upgrade process uses daemon threads and knows how to recover from failures on
+    // next attempt.
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
index e42a1cf..0c15063 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -117,8 +117,9 @@
         }
       }
 
-      if (!refControl.canCreate(repo, object)) {
-        throw new AuthException("Cannot create \"" + ref + "\"");
+      String rejectReason = refControl.canCreate(repo, object);
+      if (rejectReason != null) {
+        throw new AuthException("Cannot create \"" + ref + "\": " + rejectReason);
       }
 
       try {
@@ -155,7 +156,7 @@
               }
               refPrefix = RefUtil.getRefPrefix(refPrefix);
             }
-            //$FALL-THROUGH$
+            // $FALL-THROUGH$
           case FORCED:
           case IO_FAILURE:
           case NOT_ATTEMPTED:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
index 2fad19b..8cd44d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -60,7 +62,7 @@
       throw new ResourceConflictException("branch " + rsrc.getBranchKey() + " has open changes");
     }
 
-    deleteRefFactory.create(rsrc).ref(rsrc.getRef()).delete();
+    deleteRefFactory.create(rsrc).ref(rsrc.getRef()).prefix(R_HEADS).delete();
     return Response.none();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
index 4b45a41..fa7e917 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
 import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -40,7 +42,7 @@
     if (input == null || input.branches == null || input.branches.isEmpty()) {
       throw new BadRequestException("branches must be specified");
     }
-    deleteRefFactory.create(project).refs(input.branches).delete();
+    deleteRefFactory.create(project).refs(input.branches).prefix(R_HEADS).delete();
     return Response.none();
   }
 }
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 de22e23..2a22b1e 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
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
@@ -259,17 +260,18 @@
    *
    * @param repo repository on which user want to create
    * @param object the object the user will start the reference with.
-   * @return {@code true} if the user specified can create a new Git ref
+   * @return {@code null} if the user specified can create a new Git ref, or a String describing why
+   *     the creation is not allowed.
    */
-  public boolean canCreate(Repository repo, RevObject object) {
+  @Nullable
+  public String canCreate(Repository repo, RevObject object) {
     if (!isProjectStatePermittingWrite()) {
-      return false;
+      return "project state does not permit write";
     }
 
     if (object instanceof RevCommit) {
       if (!canPerform(Permission.CREATE)) {
-        // No create permissions.
-        return false;
+        return "lacks permission: " + Permission.CREATE;
       }
       return canCreateCommit(repo, (RevCommit) object);
     } else if (object instanceof RevTag) {
@@ -277,7 +279,13 @@
       try (RevWalk rw = new RevWalk(repo)) {
         rw.parseBody(tag);
       } catch (IOException e) {
-        return false;
+        String msg =
+            String.format(
+                "RevWalk(%s) for pushing tag %s:",
+                projectControl.getProject().getNameKey(), tag.name());
+        log.error(msg, e);
+
+        return "I/O exception for revwalk";
       }
 
       // If tagger is present, require it matches the user's email.
@@ -292,18 +300,20 @@
           valid = false;
         }
         if (!valid && !canForgeCommitter()) {
-          return false;
+          return "lacks permission: " + Permission.FORGE_COMMITTER;
         }
       }
 
       RevObject tagObject = tag.getObject();
       if (tagObject instanceof RevCommit) {
-        if (!canCreateCommit(repo, (RevCommit) tagObject)) {
-          return false;
+        String rejectReason = canCreateCommit(repo, (RevCommit) tagObject);
+        if (rejectReason != null) {
+          return rejectReason;
         }
       } else {
-        if (!canCreate(repo, tagObject)) {
-          return false;
+        String rejectReason = canCreate(repo, tagObject);
+        if (rejectReason != null) {
+          return rejectReason;
         }
       }
 
@@ -311,27 +321,34 @@
       // than if it doesn't have a PGP signature.
       //
       if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
-        return canPerform(Permission.CREATE_SIGNED_TAG);
+        return canPerform(Permission.CREATE_SIGNED_TAG)
+            ? null
+            : "lacks permission: " + Permission.CREATE_SIGNED_TAG;
       }
-      return canPerform(Permission.CREATE_TAG);
-    } else {
-      return false;
+      return canPerform(Permission.CREATE_TAG) ? null : "lacks permission " + Permission.CREATE_TAG;
     }
+
+    return null;
   }
 
-  private boolean canCreateCommit(Repository repo, RevCommit commit) {
+  /**
+   * Check if the user is allowed to create a new commit object if this introduces a new commit to
+   * the project. If not allowed, returns a string describing why it's not allowed.
+   */
+  @Nullable
+  private String canCreateCommit(Repository repo, RevCommit commit) {
     if (canUpdate()) {
       // If the user has push permissions, they can create the ref regardless
       // of whether they are pushing any new objects along with the create.
-      return true;
+      return null;
     } else if (isMergedIntoBranchOrTag(repo, commit)) {
       // If the user has no push permissions, check whether the object is
       // merged into a branch or tag readable by this user. If so, they are
       // not effectively "pushing" more objects, so they can create the ref
       // even if they don't have push permission.
-      return true;
+      return null;
     }
-    return false;
+    return "lacks permission " + Permission.PUSH + " for creating new commit object";
   }
 
   private boolean isMergedIntoBranchOrTag(Repository repo, RevCommit commit) {
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 6c6179b..161233e 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
@@ -486,11 +486,6 @@
 
   @Operator
   public Predicate<ChangeData> change(String query) throws QueryParseException {
-    if (PAT_LEGACY_ID.matcher(query).matches()) {
-      return new LegacyChangeIdPredicate(Change.Id.parse(query));
-    } else if (PAT_CHANGE_ID.matcher(query).matches()) {
-      return new ChangeIdPredicate(parseChangeId(query));
-    }
     Optional<ChangeTriplet> triplet = ChangeTriplet.parse(query);
     if (triplet.isPresent()) {
       return Predicate.and(
@@ -498,6 +493,11 @@
           branch(triplet.get().branch().get()),
           new ChangeIdPredicate(parseChangeId(triplet.get().id().get())));
     }
+    if (PAT_LEGACY_ID.matcher(query).matches()) {
+      return new LegacyChangeIdPredicate(Change.Id.parse(query));
+    } else if (PAT_CHANGE_ID.matcher(query).matches()) {
+      return new ChangeIdPredicate(parseChangeId(query));
+    }
 
     throw new QueryParseException("Invalid change format");
   }
@@ -732,7 +732,8 @@
   }
 
   @Operator
-  public Predicate<ChangeData> label(String name) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> label(String name)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     Set<Account.Id> accounts = null;
     AccountGroup.UUID group = null;
 
@@ -846,7 +847,8 @@
   }
 
   @Operator
-  public Predicate<ChangeData> starredby(String who) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> starredby(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     return starredby(parseAccount(who));
   }
 
@@ -863,7 +865,8 @@
   }
 
   @Operator
-  public Predicate<ChangeData> watchedby(String who) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> watchedby(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     Set<Account.Id> m = parseAccount(who);
     List<IsWatchedByPredicate> p = Lists.newArrayListWithCapacity(m.size());
 
@@ -887,7 +890,8 @@
   }
 
   @Operator
-  public Predicate<ChangeData> draftby(String who) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> draftby(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     Set<Account.Id> m = parseAccount(who);
     List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
     for (Account.Id id : m) {
@@ -905,7 +909,8 @@
   }
 
   @Operator
-  public Predicate<ChangeData> visibleto(String who) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> visibleto(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     if (isSelf(who)) {
       return is_visible();
     }
@@ -942,12 +947,14 @@
   }
 
   @Operator
-  public Predicate<ChangeData> o(String who) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> o(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     return owner(who);
   }
 
   @Operator
-  public Predicate<ChangeData> owner(String who) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> owner(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     return owner(parseAccount(who));
   }
 
@@ -960,7 +967,7 @@
   }
 
   private Predicate<ChangeData> ownerDefaultField(String who)
-      throws QueryParseException, OrmException {
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     Set<Account.Id> accounts = parseAccount(who);
     if (accounts.size() > MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
       return Predicate.any();
@@ -969,7 +976,8 @@
   }
 
   @Operator
-  public Predicate<ChangeData> assignee(String who) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> assignee(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     return assignee(parseAccount(who));
   }
 
@@ -991,22 +999,24 @@
   }
 
   @Operator
-  public Predicate<ChangeData> r(String who) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> r(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     return reviewer(who);
   }
 
   @Operator
-  public Predicate<ChangeData> reviewer(String who) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> reviewer(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     return reviewer(who, false);
   }
 
   private Predicate<ChangeData> reviewerDefaultField(String who)
-      throws QueryParseException, OrmException {
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     return reviewer(who, true);
   }
 
   private Predicate<ChangeData> reviewer(String who, boolean forDefaultField)
-      throws QueryParseException, OrmException {
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     Predicate<ChangeData> byState =
         reviewerByState(who, ReviewerStateInternal.REVIEWER, forDefaultField);
     if (Objects.equals(byState, Predicate.<ChangeData>any())) {
@@ -1020,7 +1030,8 @@
   }
 
   @Operator
-  public Predicate<ChangeData> cc(String who) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> cc(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     return reviewerByState(who, ReviewerStateInternal.CC, false);
   }
 
@@ -1073,7 +1084,8 @@
   }
 
   @Operator
-  public Predicate<ChangeData> commentby(String who) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> commentby(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     return commentby(parseAccount(who));
   }
 
@@ -1086,7 +1098,8 @@
   }
 
   @Operator
-  public Predicate<ChangeData> from(String who) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> from(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     Set<Account.Id> ownerIds = parseAccount(who);
     return Predicate.or(owner(ownerIds), commentby(ownerIds));
   }
@@ -1110,7 +1123,8 @@
   }
 
   @Operator
-  public Predicate<ChangeData> reviewedby(String who) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> reviewedby(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     return IsReviewedPredicate.create(parseAccount(who));
   }
 
@@ -1192,7 +1206,7 @@
       if (!Objects.equals(p, Predicate.<ChangeData>any())) {
         predicates.add(p);
       }
-    } catch (OrmException | QueryParseException e) {
+    } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) {
       // Skip.
     }
     try {
@@ -1200,13 +1214,13 @@
       if (!Objects.equals(p, Predicate.<ChangeData>any())) {
         predicates.add(p);
       }
-    } catch (OrmException | QueryParseException e) {
+    } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) {
       // Skip.
     }
     predicates.add(file(query));
     try {
       predicates.add(label(query));
-    } catch (OrmException | QueryParseException e) {
+    } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) {
       // Skip.
     }
     predicates.add(commit(query));
@@ -1245,7 +1259,8 @@
     return Predicate.and(predicates);
   }
 
-  private Set<Account.Id> parseAccount(String who) throws QueryParseException, OrmException {
+  private Set<Account.Id> parseAccount(String who)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     if (isSelf(who)) {
       return Collections.singleton(self());
     }
@@ -1291,7 +1306,7 @@
 
   public Predicate<ChangeData> reviewerByState(
       String who, ReviewerStateInternal state, boolean forDefaultField)
-      throws QueryParseException, OrmException {
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     Predicate<ChangeData> reviewerByEmailPredicate = null;
     if (args.index.getSchema().hasField(ChangeField.REVIEWER_BY_EMAIL)) {
       Address address = Address.tryParse(who);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 9a56aa4..dfcacb7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -222,11 +222,11 @@
 
   private void initSequences(Repository git, BatchRefUpdate bru) throws IOException {
     if (notesMigration.readChangeSequence()
-        && git.exactRef(REFS_SEQUENCES + Sequences.CHANGES) == null) {
+        && git.exactRef(REFS_SEQUENCES + Sequences.NAME_CHANGES) == null) {
       // Can't easily reuse the inserter from MetaDataUpdate, but this shouldn't slow down site
       // initialization unduly.
       try (ObjectInserter ins = git.newObjectInserter()) {
-        bru.addCommand(RepoSequence.storeNew(ins, Sequences.CHANGES, firstChangeId));
+        bru.addCommand(RepoSequence.storeNew(ins, Sequences.NAME_CHANGES, firstChangeId));
         ins.flush();
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index a0c03b6..3cfd91c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -35,7 +35,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_153> C = Schema_153.class;
+  public static final Class<Schema_154> C = Schema_154.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java
index 4896f3a..6ba1b65 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -88,7 +89,15 @@
           rewriteUserBranch(repo, rw, oi, emptyTree, ref, e.getValue());
         } else {
           AccountsUpdate.createUserBranch(
-              repo, oi, serverIdent, serverIdent, e.getKey(), e.getValue());
+              repo,
+              allUsersName,
+              GitReferenceUpdated.DISABLED,
+              null,
+              oi,
+              serverIdent,
+              serverIdent,
+              e.getKey(),
+              e.getValue());
         }
       }
     } catch (IOException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java
index 48d1e7e..29ae7d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -68,7 +69,8 @@
               .collect(toSet());
       accountIdsFromUserBranches.removeAll(accountIdsFromReviewDb);
       for (Account.Id accountId : accountIdsFromUserBranches) {
-        AccountsUpdate.deleteUserBranch(repo, serverIdent, accountId);
+        AccountsUpdate.deleteUserBranch(
+            repo, allUsersName, GitReferenceUpdated.DISABLED, null, serverIdent, accountId);
       }
     } catch (IOException e) {
       throw new OrmException("Failed to delete user branches for non-existing accounts.", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_154.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_154.java
new file mode 100644
index 0000000..3d686bb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_154.java
@@ -0,0 +1,105 @@
+// 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.schema;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountConfig;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashSet;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+/** Migrate accounts to NoteDb. */
+public class Schema_154 extends SchemaVersion {
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsersName;
+  private final Provider<PersonIdent> serverIdent;
+
+  @Inject
+  Schema_154(
+      Provider<Schema_153> prior,
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      @GerritPersonIdent Provider<PersonIdent> serverIdent) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.allUsersName = allUsersName;
+    this.serverIdent = serverIdent;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+    try {
+      try (Repository repo = repoManager.openRepository(allUsersName)) {
+        for (Account account : scanAccounts(db)) {
+          updateAccountInNoteDb(repo, account);
+        }
+      }
+    } catch (IOException | ConfigInvalidException e) {
+      throw new OrmException("Migrating accounts to NoteDb failed", e);
+    }
+  }
+
+  private Set<Account> scanAccounts(ReviewDb db) throws SQLException {
+    try (Statement stmt = newStatement(db);
+        ResultSet rs =
+            stmt.executeQuery(
+                "SELECT account_id,"
+                    + " registered_on,"
+                    + " full_name, "
+                    + " preferred_email,"
+                    + " status,"
+                    + " inactive"
+                    + " FROM accounts")) {
+      Set<Account> s = new HashSet<>();
+      while (rs.next()) {
+        Account a = new Account(new Account.Id(rs.getInt(1)), rs.getTimestamp(2));
+        a.setFullName(rs.getString(3));
+        a.setPreferredEmail(rs.getString(4));
+        a.setStatus(rs.getString(5));
+        a.setActive(rs.getString(6).equals("N"));
+        s.add(a);
+      }
+      return s;
+    }
+  }
+
+  private void updateAccountInNoteDb(Repository allUsersRepo, Account account)
+      throws IOException, ConfigInvalidException {
+    MetaDataUpdate md =
+        new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, allUsersRepo);
+    PersonIdent ident = serverIdent.get();
+    md.getCommitBuilder().setAuthor(ident);
+    md.getCommitBuilder().setCommitter(ident);
+    AccountConfig accountConfig = new AccountConfig(null, account.getId());
+    accountConfig.load(allUsersRepo);
+    accountConfig.setAccount(account);
+    accountConfig.commit(md);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
index 914e074..fed0be4 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -474,6 +474,14 @@
                 + "\tkey1 = "
                 + staff.toConfigValue()
                 + "\n");
+    assertThat(text(rev, "groups"))
+        .isEqualTo(
+            "# UUID\tGroup Name\n" //
+                + "#\n" //
+                + staff.getUUID().get()
+                + "     \t"
+                + staff.getName()
+                + "\n");
   }
 
   private ProjectConfig read(RevCommit rev) throws IOException, ConfigInvalidException {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 042865e..8323051 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -33,14 +33,20 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountConfig;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
@@ -58,6 +64,8 @@
 import java.util.Iterator;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
@@ -82,6 +90,8 @@
 
   @Inject protected GerritApi gApi;
 
+  @Inject @GerritPersonIdent Provider<PersonIdent> serverIdent;
+
   @Inject protected IdentifiedUser.GenericFactory userFactory;
 
   @Inject private Provider<AnonymousUser> anonymousUser;
@@ -98,6 +108,10 @@
 
   @Inject protected AllProjectsName allProjects;
 
+  @Inject protected AllUsersName allUsers;
+
+  @Inject protected GitRepositoryManager repoManager;
+
   protected LifecycleManager lifecycle;
   protected Injector injector;
   protected ReviewDb db;
@@ -383,12 +397,25 @@
   public void reindex() throws Exception {
     AccountInfo user1 = newAccountWithFullName("tester", "Test Usre");
 
-    // update account in the database so that account index is stale
+    // update account in ReviewDb without reindex so that account index is stale
     String newName = "Test User";
-    Account account = accounts.get(db, new Account.Id(user1._accountId));
+    Account.Id accountId = new Account.Id(user1._accountId);
+    Account account = accounts.get(db, accountId);
     account.setFullName(newName);
     db.accounts().update(ImmutableSet.of(account));
 
+    // update account in NoteDb without reindex so that account index is stale
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsers, repo);
+      PersonIdent ident = serverIdent.get();
+      md.getCommitBuilder().setAuthor(ident);
+      md.getCommitBuilder().setCommitter(ident);
+      AccountConfig accountConfig = new AccountConfig(null, accountId);
+      accountConfig.load(repo);
+      accountConfig.getAccount().setFullName(newName);
+      accountConfig.commit(md);
+    }
+
     assertQuery("name:" + quote(user1.name), user1);
     assertQuery("name:" + quote(newName));
 
@@ -469,11 +496,16 @@
       if (email != null) {
         accountManager.link(id, AuthRequest.forEmail(email));
       }
-      Account a = accounts.get(db, id);
-      a.setFullName(fullName);
-      a.setPreferredEmail(email);
-      a.setActive(active);
-      accountsUpdate.create().update(db, a);
+      accountsUpdate
+          .create()
+          .update(
+              db,
+              id,
+              a -> {
+                a.setFullName(fullName);
+                a.setPreferredEmail(email);
+                a.setActive(active);
+              });
       return id;
     }
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index fdb0091..2b8536b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -208,13 +208,11 @@
     db = schemaFactory.open();
 
     userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
-    Account userAccount = accounts.get(db, userId);
     String email = "user@example.com";
     externalIdsUpdate.create().insert(ExternalId.createEmail(userId, email));
-    userAccount.setPreferredEmail(email);
-    accountsUpdate.create().update(db, userAccount);
+    accountsUpdate.create().update(db, userId, a -> a.setPreferredEmail(email));
     user = userFactory.create(userId);
-    requestContext.setContext(newRequestContext(userAccount.getId()));
+    requestContext.setContext(newRequestContext(userId));
   }
 
   protected RequestContext newRequestContext(Account.Id requestUserId) {
@@ -289,25 +287,25 @@
 
   @Test
   public void byTriplet() throws Exception {
-    TestRepository<Repo> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("iabcde");
     Change change = insert(repo, newChangeForBranch(repo, "branch"));
     String k = change.getKey().get();
 
-    assertQuery("repo~branch~" + k, change);
-    assertQuery("change:repo~branch~" + k, change);
-    assertQuery("repo~refs/heads/branch~" + k, change);
-    assertQuery("change:repo~refs/heads/branch~" + k, change);
-    assertQuery("repo~branch~" + k.substring(0, 10), change);
-    assertQuery("change:repo~branch~" + k.substring(0, 10), change);
+    assertQuery("iabcde~branch~" + k, change);
+    assertQuery("change:iabcde~branch~" + k, change);
+    assertQuery("iabcde~refs/heads/branch~" + k, change);
+    assertQuery("change:iabcde~refs/heads/branch~" + k, change);
+    assertQuery("iabcde~branch~" + k.substring(0, 10), change);
+    assertQuery("change:iabcde~branch~" + k.substring(0, 10), change);
 
     assertQuery("foo~bar");
     assertThatQueryException("change:foo~bar").hasMessageThat().isEqualTo("Invalid change format");
     assertQuery("otherrepo~branch~" + k);
     assertQuery("change:otherrepo~branch~" + k);
-    assertQuery("repo~otherbranch~" + k);
-    assertQuery("change:repo~otherbranch~" + k);
-    assertQuery("repo~branch~I0000000000000000000000000000000000000000");
-    assertQuery("change:repo~branch~I0000000000000000000000000000000000000000");
+    assertQuery("iabcde~otherbranch~" + k);
+    assertQuery("change:iabcde~otherbranch~" + k);
+    assertQuery("iabcde~branch~I0000000000000000000000000000000000000000");
+    assertQuery("change:iabcde~branch~I0000000000000000000000000000000000000000");
   }
 
   @Test
@@ -2340,11 +2338,16 @@
       if (email != null) {
         accountManager.link(id, AuthRequest.forEmail(email));
       }
-      Account a = accounts.get(db, id);
-      a.setFullName(fullName);
-      a.setPreferredEmail(email);
-      a.setActive(active);
-      accountsUpdate.create().update(db, a);
+      accountsUpdate
+          .create()
+          .update(
+              db,
+              id,
+              a -> {
+                a.setFullName(fullName);
+                a.setPreferredEmail(email);
+                a.setActive(active);
+              });
       return id;
     }
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index fe2a8f3..0de5fad 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -319,11 +319,16 @@
       if (email != null) {
         accountManager.link(id, AuthRequest.forEmail(email));
       }
-      Account a = accounts.get(db, id);
-      a.setFullName(fullName);
-      a.setPreferredEmail(email);
-      a.setActive(active);
-      accountsUpdate.create().update(db, a);
+      accountsUpdate
+          .create()
+          .update(
+              db,
+              id,
+              a -> {
+                a.setFullName(fullName);
+                a.setPreferredEmail(email);
+                a.setActive(active);
+              });
       return id;
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 4bb176c..a742c35 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -37,6 +37,7 @@
 import java.io.IOException;
 import java.util.HashSet;
 import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -103,7 +104,7 @@
   @Inject private AddIncludedGroups addIncludedGroups;
 
   @Override
-  protected void run() throws Failure, OrmException, IOException {
+  protected void run() throws Failure, OrmException, IOException, ConfigInvalidException {
     try {
       GroupResource rsrc = createGroup();
 
@@ -119,7 +120,8 @@
     }
   }
 
-  private GroupResource createGroup() throws RestApiException, OrmException, IOException {
+  private GroupResource createGroup()
+      throws RestApiException, OrmException, IOException, ConfigInvalidException {
     GroupInput input = new GroupInput();
     input.description = groupDescription;
     input.visibleToAll = visibleToAll;
@@ -132,7 +134,8 @@
     return groups.parse(TopLevelResource.INSTANCE, IdString.fromUrl(group.id));
   }
 
-  private void addMembers(GroupResource rsrc) throws RestApiException, OrmException, IOException {
+  private void addMembers(GroupResource rsrc)
+      throws RestApiException, OrmException, IOException, ConfigInvalidException {
     AddMembers.Input input =
         AddMembers.Input.fromMembers(
             initialMembers.stream().map(Object::toString).collect(toList()));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index 4d93fe8..bbe736d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -35,6 +35,7 @@
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -79,7 +80,7 @@
     Account userAccount;
     try {
       userAccount = accountResolver.find(db, userName);
-    } catch (OrmException e) {
+    } catch (OrmException | IOException | ConfigInvalidException e) {
       throw die(e);
     }
     if (userAccount == null) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 1923169..033b4c6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -292,7 +292,8 @@
   }
 
   private void putPreferred(String email)
-      throws RestApiException, OrmException, IOException, PermissionBackendException {
+      throws RestApiException, OrmException, IOException, PermissionBackendException,
+          ConfigInvalidException {
     for (EmailInfo e : getEmails.apply(rsrc)) {
       if (e.email.equals(email)) {
         putPreferred.apply(new AccountResource.Email(user, email), null);
diff --git a/lib/codemirror/cm.bzl b/lib/codemirror/cm.bzl
index 9b473c0..bf10043 100644
--- a/lib/codemirror/cm.bzl
+++ b/lib/codemirror/cm.bzl
@@ -214,7 +214,7 @@
     "z80",
 ]
 
-CM_VERSION = "5.26.0"
+CM_VERSION = "5.27.2"
 
 TOP = "META-INF/resources/webjars/codemirror/%s" % CM_VERSION
 
diff --git a/lib/js/bower_components.bzl b/lib/js/bower_components.bzl
index bb047bd70..9c3763e 100644
--- a/lib/js/bower_components.bzl
+++ b/lib/js/bower_components.bzl
@@ -174,6 +174,15 @@
     seed = True,
   )
   bower_component(
+    name = "polymer-resin",
+    license = "//lib:LICENSE-polymer",
+    deps = [
+      ":polymer",
+      ":webcomponentsjs",
+    ],
+    seed = True,
+  )
+  bower_component(
     name = "promise-polyfill",
     license = "//lib:LICENSE-promise-polyfill",
     deps = [ ":polymer" ],
diff --git a/plugins/replication b/plugins/replication
index 6d83f5e..fae5360 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 6d83f5ebfae04717d13cfadda4ca526fd66a5918
+Subproject commit fae5360380023e8351f39be3d4effd4bb2cd8906
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index fd75f5f..d4d2322 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -21,6 +21,7 @@
         "//lib/js:moment",
         "//lib/js:page",
         "//lib/js:polymer",
+        "//lib/js:polymer-resin",
         "//lib/js:promise-polyfill",
     ],
 )
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index f334354..04a1d69 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -2,6 +2,7 @@
     default_visibility = ["//visibility:public"],
 )
 
+load(":rules.bzl", "polygerrit_bundle")
 load("//tools/bzl:genrule2.bzl", "genrule2")
 load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library", "closure_js_binary")
 load(
@@ -12,8 +13,8 @@
     "js_component",
 )
 
-vulcanize(
-    name = "gr-app",
+polygerrit_bundle(
+    name = "polygerrit_ui",
     srcs = glob(
         [
             "**/*.html",
@@ -27,82 +28,7 @@
         ],
     ),
     app = "elements/gr-app.html",
-    deps = ["//polygerrit-ui:polygerrit_components.bower_components"],
-)
-
-closure_js_library(
-    name = "closure_lib",
-    srcs = ["gr-app.js"],
-    convention = "GOOGLE",
-    # TODO(davido): Clean up these issues: http://paste.openstack.org/show/608548
-    # and remove this supression
-    suppress = ["JSC_UNUSED_LOCAL_ASSIGNMENT"],
-    deps = [
-        "//lib/polymer_externs:polymer_closure",
-        "@io_bazel_rules_closure//closure/library",
-    ],
-)
-
-closure_js_binary(
-    name = "closure_bin",
-    # Known issue: Closure compilation not compatible with Polymer behaviors.
-    # See: https://github.com/google/closure-compiler/issues/2042
-    compilation_level = "WHITESPACE_ONLY",
-    defs = [
-        "--polymer_pass",
-        "--jscomp_off=duplicate",
-        "--force_inject_library=es6_runtime",
-    ],
-    language = "ECMASCRIPT5",
-    deps = [":closure_lib"],
-)
-
-filegroup(
-    name = "top_sources",
-    srcs = [
-        "favicon.ico",
-        "index.html",
-    ],
-)
-
-filegroup(
-    name = "css_sources",
-    srcs = glob(["styles/**/*.css"]),
-)
-
-filegroup(
-    name = "app_sources",
-    srcs = [
-        "closure_bin.js",
-        "gr-app.html",
-    ],
-)
-
-genrule2(
-    name = "polygerrit_ui",
-    srcs = [
-        "//lib/fonts:robotomono",
-        "//lib/js:highlightjs_files",
-        ":top_sources",
-        ":css_sources",
-        ":app_sources",
-        # we extract from the zip, but depend on the component for license checking.
-        "@webcomponentsjs//:zipfile",
-        "//lib/js:webcomponentsjs",
-    ],
     outs = ["polygerrit_ui.zip"],
-    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: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",
-        "unzip -qd $$TMP/polygerrit_ui/bower_components $(location @webcomponentsjs//:zipfile) webcomponentsjs/webcomponents-lite.js",
-        "cd $$TMP",
-        "find . -exec touch -t 198001010000 '{}' ';'",
-        "zip -qr $$ROOT/$@ *",
-    ]),
 )
 
 bower_component_bundle(
@@ -198,3 +124,49 @@
         "manual",
     ],
 )
+
+# Embed bundle
+polygerrit_bundle(
+    name = "polygerrit_embed_ui",
+    srcs = glob(
+        [
+            "**/*.html",
+            "**/*.js",
+        ],
+        exclude = [
+            "bower_components/**",
+            "index.html",
+            "test/**",
+            "**/*_test.html",
+        ],
+    ),
+    app = "embed/change-diff-views.html",
+    outs = ["polygerrit_embed_ui.zip"],
+)
+
+filegroup(
+    name = "embed_test_files",
+    srcs = glob(
+        [
+            "embed/**/*_test.html",
+        ],
+    ),
+)
+
+sh_test(
+    name = "embed_test",
+    size = "large",
+    srcs = ["embed_test.sh"],
+    data = [
+        "test/common-test-setup.html",
+        "embed/test.html",
+        ":embed_test_files",
+        ":polygerrit_embed_ui.zip",
+        ":test_components.zip",
+    ],
+    # Should not run sandboxed.
+    tags = [
+        "local",
+        "manual",
+    ],
+)
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
index ab9158d..22a2abc 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
@@ -31,7 +31,7 @@
         items-per-page="[[_groupsPerPage]]"
         loading="[[_loading]]"
         offset="[[_offset]]"
-        path="/admin/groups">
+        path="[[_path]]">
       <table id="list">
         <tr class="headerRow">
           <th class="name topHeader">Group Name</th>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
index 130e4f4..645b89b 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -33,7 +33,7 @@
       _path: {
         type: String,
         readOnly: true,
-        value: '/admin/groups/',
+        value: '/admin/groups',
       },
       _groups: Array,
 
@@ -77,7 +77,7 @@
     },
 
     _computeGroupUrl(id) {
-      return this.getUrl(this._path, id);
+      return this.getUrl(this._path + '/', id);
     },
 
     _getGroups(filter, groupsPerPage, offset) {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
index 05b30e2..45a6e4a 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
@@ -32,7 +32,7 @@
         items="[[_projects]]"
         loading="[[_loading]]"
         offset="[[_offset]]"
-        path="/admin/projects">
+        path="[[_path]]">
       <table id="list">
         <tr class="headerRow">
           <th class="name topHeader">Project Name</th>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
index 41a4737..8cd804f 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
@@ -33,7 +33,7 @@
       _path: {
         type: String,
         readOnly: true,
-        value: '/admin/projects/',
+        value: '/admin/projects',
       },
       _projects: Array,
 
@@ -72,7 +72,7 @@
     },
 
     _computeProjectUrl(name) {
-      return this.getUrl(this._path, name);
+      return this.getUrl(this._path + '/', name);
     },
 
     _getProjects(filter, projectsPerPage, offset) {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
index 8829506..1292bc5 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
@@ -33,7 +33,7 @@
 </test-fixture>
 
 <script>
-  let counter = 0;
+  let counter;
   const projectGenerator = () => {
     return {
       id: `test${++counter}`,
@@ -56,6 +56,7 @@
     setup(() => {
       sandbox = sinon.sandbox.create();
       element = fixture('basic');
+      counter = 0;
     });
 
     teardown(() => {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.html b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.html
index a9dd3df8..e149594 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.html
@@ -97,7 +97,7 @@
                         id="submitTypeSelect"
                         is="gr-select"
                         bind-value="{{_projectConfig.submit_type}}"
-                        disabled$="[[_readOnly]]">>
+                        disabled$="[[_readOnly]]">
                       <template is="dom-repeat" items="[[_submitTypes]]">
                         <option value="[[item.value]]">[[item.label]]</option>
                       </template>
@@ -111,7 +111,7 @@
                         id="contentMergeSelect"
                         is="gr-select"
                         bind-value="{{_projectConfig.use_content_merge.configured_value}}"
-                        disabled$="[[_readOnly]]">>
+                        disabled$="[[_readOnly]]">
                       <template is="dom-repeat"
                           items="[[_formatBooleanSelect(_projectConfig.use_content_merge)]]">
                         <option value="[[item.value]]">[[item.label]]</option>
@@ -128,7 +128,7 @@
                         id="newChangeSelect"
                         is="gr-select"
                         bind-value="{{_projectConfig.create_new_change_for_all_not_in_target.configured_value}}"
-                        disabled$="[[_readOnly]]">>
+                        disabled$="[[_readOnly]]">
                       <template is="dom-repeat"
                           items="[[_formatBooleanSelect(_projectConfig.create_new_change_for_all_not_in_target)]]">
                         <option value="[[item.value]]">[[item.label]]</option>
@@ -143,7 +143,7 @@
                         id="requireChangeIdSelect"
                         is="gr-select"
                         bind-value="{{_projectConfig.require_change_id.configured_value}}"
-                        disabled$="[[_readOnly]]">>
+                        disabled$="[[_readOnly]]">
                       <template is="dom-repeat"
                           items="[[_formatBooleanSelect(_projectConfig.require_change_id)]]">
                         <option value="[[item.value]]">[[item.label]]</option>
@@ -159,7 +159,7 @@
                         id="rejectImplicitMergesSelect"
                         is="gr-select"
                         bind-value="{{_projectConfig.reject_implicit_merges.configured_value}}"
-                        disabled$="[[_readOnly]]">>
+                        disabled$="[[_readOnly]]">
                       <template is="dom-repeat"
                           items="[[_formatBooleanSelect(_projectConfig.reject_implicit_merges)]]">
                         <option value="[[item.value]]">[[item.label]]</option>
@@ -178,6 +178,21 @@
                         disabled$="[[_readOnly]]">
                   </span>
                 </section>
+                <section>
+                  <span class="title">Match authored date with committer date upon submit</span>
+                  <span class="value">
+                    <select
+                        id="matchAuthoredDateWithCommitterDateSelect"
+                        is="gr-select"
+                        bind-value="{{_projectConfig.match_author_to_committer_date.configured_value}}"
+                        disabled$="[[_readOnly]]">
+                      <template is="dom-repeat"
+                          items="[[_formatBooleanSelect(_projectConfig.match_author_to_committer_date)]]">
+                        <option value="[[item.value]]">[[item.label]]</option>
+                      </template>
+                    </select>
+                  </span>
+                </section>
               </fieldset>
               <h3 id="Options">Contributor Agreements</h3>
               <fieldset id="agreements">
@@ -189,7 +204,7 @@
                         id="contributorAgreementSelect"
                         is="gr-select"
                         bind-value="{{_projectConfig.use_contributor_agreements.configured_value}}"
-                        disabled$="[[_readOnly]]">>
+                        disabled$="[[_readOnly]]">
                     <template is="dom-repeat"
                         items="[[_formatBooleanSelect(_projectConfig.use_contributor_agreements)]]">
                       <option value="[[item.value]]">[[item.label]]</option>
@@ -204,7 +219,7 @@
                         id="useSignedOffBySelect"
                         is="gr-select"
                         bind-value="{{_projectConfig.use_signed_off_by.configured_value}}"
-                        disabled$="[[_readOnly]]">>
+                        disabled$="[[_readOnly]]">
                     <template is="dom-repeat"
                         items="[[_formatBooleanSelect(_projectConfig.use_signed_off_by)]]">
                       <option value="[[item.value]]">[[item.label]]</option>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project_test.html b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project_test.html
index 031720a..e3b809d 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project_test.html
@@ -77,6 +77,10 @@
               value: false,
               configured_value: 'FALSE',
             },
+            match_author_to_committer_date: {
+              value: false,
+              configured_value: 'FALSE',
+            },
             max_object_size_limit: {},
             submit_type: 'MERGE_IF_NECESSARY',
           });
@@ -225,6 +229,7 @@
           create_new_change_for_all_not_in_target: 'TRUE',
           require_change_id: 'TRUE',
           reject_implicit_merges: 'TRUE',
+          match_author_to_committer_date: 'TRUE',
           max_object_size_limit: 10,
           submit_type: 'FAST_FORWARD_ONLY',
           state: 'READ_ONLY',
@@ -251,6 +256,8 @@
               configInputObj.require_change_id;
           element.$.rejectImplicitMergesSelect.bindValue =
               configInputObj.reject_implicit_merges;
+          element.$.matchAuthoredDateWithCommitterDateSelect.bindValue =
+              configInputObj.match_author_to_committer_date;
           element.$.maxGitObjSizeInput.bindValue =
               configInputObj.max_object_size_limit;
           element.$.contributorAgreementSelect.bindValue =
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
index 80db48b..91946a6 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -17,6 +17,7 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 
 <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="../../../styles/gr-menu-page-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html">
@@ -27,6 +28,7 @@
 <link rel="import" href="../gr-admin-plugin-list/gr-admin-plugin-list.html">
 <link rel="import" href="../gr-admin-project-list/gr-admin-project-list.html">
 <link rel="import" href="../gr-admin-project/gr-admin-project.html">
+<link rel="import" href="../gr-project-branches/gr-project-branches.html">
 
 <dom-module id="gr-admin-view">
   <template>
@@ -34,17 +36,32 @@
     <style include="gr-menu-page-styles"></style>
     <gr-page-nav class$="[[_computeLoadingClass(_loading)]]">
       <ul class="sectionContent">
-        <template is="dom-repeat" items="[[_filteredLinks]]">
+        <template id="adminNav" is="dom-repeat" items="[[_filteredLinks]]">
           <li class$="sectionTitle [[_computeSelectedClass(item.view, params)]]">
             <a class="title" href="[[_computeLinkURL(item)]]"
-                  rel$="[[_computeLinkRel(item)]]">[[item.name]]</a>
+                  rel="noopener">[[item.name]]</a>
           </li>
-          <template is="dom-repeat" items="[[item.children]]">
-            <li class$="[[_computeSelectedClass(item.view, params)]]">
-              <a href="[[_computeLinkURL(item)]]"
-                  rel$="[[_computeLinkRel(item)]]">[[item.name]]</a>
+          <template is="dom-repeat" items="[[item.children]]" as="child">
+            <li class$="[[_computeSelectedClass(child.view, params)]]">
+              <a href$="[[_computeLinkURL(child)]]"
+                  rel="noopener">[[child.name]]</a>
             </li>
           </template>
+          <template is="dom-if" if="[[item.subsection]]">
+            <!--If a section has a subsection, render that.-->
+            <li class$="[[_computeSelectedClass(item.subsection.view, params)]]">
+              <a class="title" href$="[[_computeLinkURL(item.subsection)]]"
+                  rel="noopener">
+                [[item.subsection.name]]</a>
+            </li>
+            <!--Loop through the links in the sub-section.-->
+            <template is="dom-repeat"
+                items="[[item.subsection.children]]" as="child">
+              <li class$="subsectionItem [[_computeSelectedClass(child.view, params)]]">
+                <a href$="[[_computeLinkURL(child)]]">[[child.name]]</a>
+              </li>
+            </template>
+          </template>
         </template>
       </ul>
     </gr-page-nav>
@@ -77,6 +94,13 @@
             id="createProject"></gr-admin-create-project>
       </main>
     </template>
+    <template is="dom-if" if="[[_showProjectBranches]]" restamp="true">
+      <main class="table">
+        <gr-project-branches
+            params="[[params]]"
+            class="table"></gr-project-branches>
+      </main>
+    </template>
     <template is="dom-if" if="[[params.placeholder]]" restamp="true">
       <gr-placeholder title="Admin" path="[[path]]"></gr-placeholder>
     </template>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index 66105a1..e69d5be 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -55,6 +55,7 @@
       path: String,
       adminView: String,
 
+      _project: String,
       _filteredLinks: Array,
       _showDownload: {
         type: Boolean,
@@ -63,12 +64,14 @@
       _showCreateProject: Boolean,
       _showProjectMain: Boolean,
       _showProjectList: Boolean,
+      _showProjectBranches: Boolean,
       _showGroupList: Boolean,
       _showPluginList: Boolean,
     },
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
+      Gerrit.URLEncodingBehavior,
     ],
 
     observers: [
@@ -94,10 +97,29 @@
 
     _filterLinks(filterFn) {
       const links = ADMIN_LINKS.filter(filterFn);
+      const filteredLinks = [];
       for (const link of links) {
-        link.children = link.children ? link.children.filter(filterFn) : [];
+        const linkCopy = Object.assign({}, link);
+        linkCopy.children = linkCopy.children ?
+            linkCopy.children.filter(filterFn) : [];
+        if (linkCopy.name === 'Projects' && this._project) {
+          linkCopy.subsection = {
+            name: `${this._project}`,
+            view: 'gr-admin-project',
+            url: `/admin/projects/${this.encodeURL(this._project, true)}`,
+            children: [
+              {
+                name: 'Branches',
+                view: 'gr-project-branches',
+                url: `/admin/projects/${this.encodeURL(this._project, true)}` +
+                    ',branches',
+              },
+            ],
+          };
+        }
+        filteredLinks.push(linkCopy);
       }
-      return links;
+      return filteredLinks;
     },
 
     _loadAccountCapabilities() {
@@ -110,37 +132,21 @@
           });
     },
 
-    _computeSideLinks(unformattedLinks) {
-      const topLevelLinks = unformattedLinks.filter(link => {
-        return link.topLevel;
-      });
-
-      const nestedLinks = unformattedLinks.filter(link => {
-        return !link.topLevel;
-      });
-
-      return topLevelLinks.map(item => {
-        const section = {
-          name: item.name,
-          url: item.url,
-          view: item.view,
-        };
-        const newLinks = nestedLinks.filter(group => {
-          return group.section === section.name;
-        });
-        section.links = newLinks;
-        return section;
-      });
-    },
-
     _paramsChanged(params) {
       this.set('_showCreateProject',
           params.adminView === 'gr-admin-create-project');
       this.set('_showProjectMain', params.adminView === 'gr-admin-project');
       this.set('_showProjectList',
           params.adminView === 'gr-admin-project-list');
+      this.set('_showProjectBranches',
+          params.adminView === 'gr-project-branches');
       this.set('_showGroupList', params.adminView === 'gr-admin-group-list');
       this.set('_showPluginList', params.adminView === 'gr-admin-plugin-list');
+      if (params.project !== this._project) {
+        this._project = params.project || '';
+        // Reloads the admin menu.
+        this.reload();
+      }
     },
 
     // TODO (beckysiegel): Update these functions after router abstraction is
@@ -156,19 +162,13 @@
     },
 
     _computeLinkURL(link) {
-      if (typeof link.url === 'undefined') {
-        return '';
-      }
+      if (!link || typeof link.url === 'undefined') { return ''; }
       if (link.target) {
         return link.url;
       }
       return this._computeRelativeURL(link.url);
     },
 
-    _computeLinkRel(link) {
-      return link.target ? 'noopener' : null;
-    },
-
     _computeSelectedClass(itemView, params) {
       return itemView === params.adminView ? 'selected' : '';
     },
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
index 721bcc0..acb3ba4 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -39,6 +39,11 @@
     setup(() => {
       sandbox = sinon.sandbox.create();
       element = fixture('basic');
+      stub('gr-rest-api-interface', {
+        getProjectConfig() {
+          return Promise.resolve({});
+        },
+      });
     });
 
     teardown(() => {
@@ -99,6 +104,7 @@
 
         // Projects
         assert.equal(element._filteredLinks[0].children.length, 1);
+        assert.isNotOk(element._filteredLinks[0].subsection);
 
         // Groups
         assert.equal(element._filteredLinks[1].children.length, 1);
@@ -118,6 +124,7 @@
 
         // Projects
         assert.equal(element._filteredLinks[0].children.length, 0);
+        assert.isNotOk(element._filteredLinks[0].subsection);
 
         // Groups
         assert.equal(element._filteredLinks[1].children.length, 0);
@@ -131,8 +138,53 @@
 
         // Projects
         assert.equal(element._filteredLinks[0].children.length, 0);
+        assert.isNotOk(element._filteredLinks[0].subsection);
         done();
       });
     });
+
+    test('Project shows up in nav', done => {
+      element._project = 'Test Project';
+      sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => {
+        return Promise.resolve({
+          createGroup: true,
+          createProject: true,
+          viewPlugins: true,
+        });
+      });
+      element._loadAccountCapabilities().then(() => {
+        assert.equal(element._filteredLinks.length, 3);
+
+        // Projects
+        assert.equal(element._filteredLinks[0].children.length, 1);
+        assert.equal(element._filteredLinks[0].subsection.name, 'Test Project');
+
+        // Groups
+        assert.equal(element._filteredLinks[1].children.length, 1);
+
+        // Plugins
+        assert.equal(element._filteredLinks[2].children.length, 0);
+        done();
+      });
+    });
+
+    test('Nav is reloaded when project changes', () => {
+      sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => {
+        return Promise.resolve({
+          createGroup: true,
+          createProject: true,
+          viewPlugins: true,
+        });
+      });
+      sandbox.stub(element.$.restAPI, 'getAccount', () => {
+        return Promise.resolve({_id: 1});
+      });
+      sandbox.stub(element, 'reload');
+      element.params = {project: 'Test Project', adminView: 'gr-admin-project'};
+      assert.equal(element.reload.callCount, 1);
+      element.params = {project: 'Test Project 2',
+        adminView: 'gr-admin-project'};
+      assert.equal(element.reload.callCount, 2);
+    });
   });
 </script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.html b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.html
new file mode 100644
index 0000000..063b311
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.html
@@ -0,0 +1,69 @@
+<!--
+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/gr-list-view-behavior/gr-list-view-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
+<link rel="import" href="../../../styles/shared-styles.html">
+
+
+<dom-module id="gr-project-branches">
+  <template>
+    <style include="shared-styles"></style>
+    <gr-list-view
+        filter="[[_filter]]"
+        items-per-page="[[_branchesPerPage]]"
+        items="[[_branches]]"
+        loading="[[_loading]]"
+        offset="[[_offset]]"
+        path="[[_getPath(_project)]]">
+      <table id="list">
+        <tr class="headerRow">
+          <th class="name topHeader">Branch Name</th>
+          <th class="description topHeader">Revision</th>
+          <th class="repositoryBrowser topHeader">Repository Browser</th>
+        </tr>
+        <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
+          <td>Loading...</td>
+        </tr>
+        <template is="dom-repeat" items="[[_shownBranches]]"
+            class$="[[computeLoadingClass(_loading)]]">
+          <tr class="table">
+            <td class="name">[[_stripRefsHeads(item.ref)]]</td>
+            <td class="description">[[item.revision]]</td>
+            <td class="repositoryBrowser">
+              <template is="dom-repeat"
+                  items="[[_computeWeblink(item)]]" as="link">
+                <a href$="[[link.url]]"
+                    class="webLink"
+                    rel="noopener"
+                    target="_blank">
+                  ([[link.name]])
+                </a>
+              </template>
+            </td>
+          </tr>
+        </template>
+      </table>
+    </gr-list-view>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-project-branches.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.js b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.js
new file mode 100644
index 0000000..97e3c4d
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.js
@@ -0,0 +1,96 @@
+// 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-project-branches',
+
+    properties: {
+      /**
+       * URL params passed from the router.
+       */
+      params: {
+        type: Object,
+        observer: '_paramsChanged',
+      },
+
+      /**
+       * Offset of currently visible query results.
+       */
+      _offset: Number,
+      _project: Object,
+      _branches: Array,
+      /**
+       * Because  we request one more than the projectsPerPage, _shownProjects
+       * maybe one less than _projects.
+       * */
+      _shownBranches: {
+        type: Array,
+        computed: 'computeShownItems(_branches)',
+      },
+      _branchesPerPage: {
+        type: Number,
+        value: 25,
+      },
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+      _filter: String,
+    },
+
+    behaviors: [
+      Gerrit.ListViewBehavior,
+      Gerrit.URLEncodingBehavior,
+    ],
+
+    _paramsChanged(params) {
+      this._loading = true;
+      if (!params || !params.project) { return; }
+
+      this._project = params.project;
+
+      this._filter = this.getFilterValue(params);
+      this._offset = this.getOffsetValue(params);
+
+      return this._getBranches(this._filter, this._project,
+          this._branchesPerPage, this._offset);
+    },
+
+    _getBranches(filter, project, projectsPerPage, offset) {
+      this._projectsBranches = [];
+      return this.$.restAPI.getProjectBranches(
+          filter, project, projectsPerPage, offset) .then(branches => {
+            if (!branches) { return; }
+            this._branches = branches;
+            this._loading = false;
+          });
+    },
+
+    _getPath(project) {
+      return '/admin/projects/' + this.encodeURL(project, true) + ',branches';
+    },
+
+    _computeWeblink(project) {
+      if (!project.web_links) { return ''; }
+      const webLinks = project.web_links;
+      return webLinks.length ? webLinks : null;
+    },
+
+    _stripRefsHeads(item) {
+      return item.replace('refs/heads/', '');
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches_test.html b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches_test.html
new file mode 100644
index 0000000..707a536
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches_test.html
@@ -0,0 +1,150 @@
+<!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-project-branches</title>
+
+<script src="../../../bower_components/page/page.js"></script>
+<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-project-branches.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-project-branches></gr-project-branches>
+  </template>
+</test-fixture>
+
+<script>
+  let counter;
+  const branchGenerator = () => {
+    return {
+      ref: `refs/heads/test${++counter}`,
+      revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
+      web_links: [
+        {
+          name: 'diffusion',
+          url: `https://git.example.org/branch/test;refs/heads/test${counter}`,
+        },
+      ],
+    };
+  };
+
+  suite('gr-project-branches tests', () => {
+    let element;
+    let branches;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+      counter = 0;
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    suite('list with project branches', () => {
+      setup(done => {
+        branches = _.times(26, branchGenerator);
+
+        stub('gr-rest-api-interface', {
+          getProjectBranches(num, project, offset) {
+            return Promise.resolve(branches);
+          },
+        });
+
+        const params = {
+          project: 'test',
+        };
+
+        element._paramsChanged(params).then(() => { flush(done); });
+      });
+
+      test('test for test branch in the list', done => {
+        flush(() => {
+          assert.equal(element._branches[1].ref, 'refs/heads/test2');
+          done();
+        });
+      });
+
+      test('test for test web links in the branches list', done => {
+        flush(() => {
+          assert.equal(element._branches[1].web_links[0].url,
+              'https://git.example.org/branch/test;refs/heads/test2');
+          done();
+        });
+      });
+
+      test('test for refs/heads/ being striped from ref', done => {
+        flush(() => {
+          assert.equal(element._stripRefsHeads(element._branches[1].ref),
+              'test2');
+          done();
+        });
+      });
+
+      test('_shownBranches', () => {
+        assert.equal(element._shownBranches.length, 25);
+      });
+    });
+
+    suite('list with less then 25 branches', () => {
+      setup(done => {
+        branches = _.times(25, branchGenerator);
+
+        stub('gr-rest-api-interface', {
+          getProjectBranches(num, project, offset) {
+            return Promise.resolve(branches);
+          },
+        });
+
+        const params = {
+          project: 'test',
+        };
+
+        element._paramsChanged(params).then(() => { flush(done); });
+      });
+
+      test('_shownProjectsBranches', () => {
+        assert.equal(element._shownBranches.length, 25);
+      });
+    });
+
+    suite('filter', () => {
+      test('_paramsChanged', done => {
+        sandbox.stub(element.$.restAPI, 'getProjectBranches', () => {
+          return Promise.resolve(branches);
+        });
+        const params = {
+          project: 'test',
+          filter: 'test',
+          offset: 25,
+        };
+        element._paramsChanged(params).then(() => {
+          assert.isTrue(element.$.restAPI.getProjectBranches.lastCall
+              .calledWithExactly('test', 'test', 25, 25));
+          done();
+        });
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
index 1288094..1dcae68 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
@@ -45,7 +45,7 @@
           data-account-id$="[[account._account_id]]"
           removable="[[_computeRemovable(account)]]"
           on-keydown="_handleChipKeydown"
-          tabindex$="[[index]]">
+          tabindex="-1">
       </gr-account-chip>
     </template>
     <gr-account-entry
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 7a61d56..5b5ece6 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
@@ -252,6 +252,9 @@
           flex-direction: column;
           flex-wrap: nowrap;
         }
+        #commitMessageEditor {
+          min-width: 0;
+        }
       }
       /* NOTE: If you update this breakpoint, also update the
       BREAKPOINT_RELATED_SMALL in the JS */
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 d3ae4b2..f8a1ea2 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
@@ -73,6 +73,7 @@
         type: Object,
         notify: true,
         value() { return {}; },
+        observer: '_viewStateChanged',
       },
       backPage: String,
       hasParent: Boolean,
@@ -87,6 +88,7 @@
       _diffPrefs: Object,
       _numFilesShown: {
         type: Number,
+        value: DEFAULT_NUM_FILES_SHOWN,
         observer: '_numFilesShownChanged',
       },
       _account: {
@@ -221,9 +223,6 @@
         }
       });
 
-      this._numFilesShown = this.viewState.numFilesShown ?
-          this.viewState.numFilesShown : DEFAULT_NUM_FILES_SHOWN;
-
       this.addEventListener('comment-save', this._handleCommentSave.bind(this));
       this.addEventListener('comment-discard',
           this._handleCommentDiscard.bind(this));
@@ -512,6 +511,11 @@
       }
     },
 
+    _viewStateChanged(viewState) {
+      this._numFilesShown = viewState.numFilesShown ?
+          viewState.numFilesShown : DEFAULT_NUM_FILES_SHOWN;
+    },
+
     _numFilesShownChanged(numFilesShown) {
       this.viewState.numFilesShown = numFilesShown;
     },
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index c1640fa..010a01f 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -93,8 +93,14 @@
       this._reviewers = result.filter(reviewer => {
         return reviewer._account_id != owner._account_id;
       });
-      this._displayedReviewers =
+      // If there is one more than the max reviewers, don't show the 'show more'
+      // button, because it takes up just as much space.
+      if (this._reviewers.length <= MAX_REVIEWERS_DISPLAYED + 1) {
+        this._displayedReviewers = this._reviewers;
+      } else {
+        this._displayedReviewers =
           this._reviewers.slice(0, MAX_REVIEWERS_DISPLAYED);
+      }
     },
 
     _computeHiddenCount(reviewers, displayedReviewers) {
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index 4e0541e6..3c1773d 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -188,9 +188,9 @@
           {value: {ccsOnly: true}}));
     });
 
-    test('no show all reviewers button with 5 reviewers', () => {
+    test('no show all reviewers button with 6 reviewers', () => {
       const reviewers = [];
-      for (let i = 0; i < 5; i++) {
+      for (let i = 0; i < 6; i++) {
         reviewers.push(
           {email: i+'reviewer@google.com', name: 'reviewer-' + i});
       }
@@ -206,11 +206,34 @@
       };
       flushAsynchronousOperations();
       assert.equal(element._hiddenReviewerCount, 0);
-      assert.equal(element._displayedReviewers.length, 5);
-      assert.equal(element._reviewers.length, 5);
+      assert.equal(element._displayedReviewers.length, 6);
+      assert.equal(element._reviewers.length, 6);
       assert.isTrue(element.$$('.hiddenReviewers').hidden);
     });
 
+    test('how all reviewers button with 7 reviewers', () => {
+      const reviewers = [];
+      for (let i = 0; i < 7; i++) {
+        reviewers.push(
+          {email: i+'reviewer@google.com', name: 'reviewer-' + i});
+      }
+      element.ccsOnly = true;
+
+      element.change = {
+        owner: {
+          _account_id: 1,
+        },
+        reviewers: {
+          CC: reviewers,
+        },
+      };
+      flushAsynchronousOperations();
+      assert.equal(element._hiddenReviewerCount, 2);
+      assert.equal(element._displayedReviewers.length, 5);
+      assert.equal(element._reviewers.length, 7);
+      assert.isFalse(element.$$('.hiddenReviewers').hidden);
+    });
+
     test('show all reviewers button', () => {
       const reviewers = [];
       for (let i = 0; i < 100; i++) {
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 b8d1f47..1b89783 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -174,6 +174,38 @@
       });
     });
 
+    // Matches /admin/projects/<project/branches[,<offset>].
+    page(/^\/admin\/projects\/(.+),branches(,(.+))?$/, loadUser, data => {
+      app.params = {
+        view: 'gr-admin-view',
+        adminView: 'gr-project-branches',
+        project: data.params[0],
+        offset: data.params[2] || 0,
+        filter: null,
+      };
+    });
+
+    page('/admin/projects/:project,branches/q/filter::filter,:offset',
+        loadUser, data => {
+          app.params = {
+            view: 'gr-admin-view',
+            adminView: 'gr-project-branches',
+            project: data.params.project,
+            offset: data.params.offset,
+            filter: data.params.filter,
+          };
+        });
+
+    page('/admin/projects/:project,branches/q/filter::filter',
+        loadUser, data => {
+          app.params = {
+            view: 'gr-admin-view',
+            adminView: 'gr-project-branches',
+            project: data.params.project,
+            filter: data.params.filter || null,
+          };
+        });
+
     // Matches /admin/projects[,<offset>][/].
     page(/^\/admin\/projects(,(\d+))?(\/)?$/, loadUser, data => {
       app.params = {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index 895f777..330fefe 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -94,6 +94,47 @@
       }
     },
 
+    /**
+     * Get current normalized selection.
+     * Merges multiple ranges, accounts for triple click, accounts for
+     * syntax highligh, convert native DOM Range objects to Gerrit concepts
+     * (line, side, etc).
+     * @return {{
+     *   start: {
+     *     node: Node,
+     *     side: string,
+     *     line: Number,
+     *     column: Number
+     *   },
+     *   end: {
+     *     node: Node,
+     *     side: string,
+     *     line: Number,
+     *     column: Number
+     *   }
+     * }}
+     */
+    _getNormalizedRange() {
+      const selection = window.getSelection();
+      const rangeCount = selection.rangeCount;
+      if (rangeCount === 0) {
+        return null;
+      } else if (rangeCount === 1) {
+        return this._normalizeRange(selection.getRangeAt(0));
+      } else {
+        const startRange = this._normalizeRange(selection.getRangeAt(0));
+        const endRange = this._normalizeRange(
+            selection.getRangeAt(rangeCount - 1));
+        return {
+          start: startRange.start,
+          end: endRange.end,
+        };
+      }
+    },
+
+    /**
+     * Normalize a specific DOM Range.
+     */
     _normalizeRange(domRange) {
       const range = GrRangeNormalizer.normalize(domRange);
       return this._fixTripleClickSelection({
@@ -204,15 +245,11 @@
     },
 
     _handleSelection() {
-      const selection = window.getSelection();
-      if (selection.rangeCount != 1) {
+      const normalizedRange = this._getNormalizedRange();
+      if (!normalizedRange) {
         return;
       }
-      const range = selection.getRangeAt(0);
-      if (range.collapsed) {
-        return;
-      }
-      const normalizedRange = this._normalizeRange(range);
+      const domRange = window.getSelection().getRangeAt(0);
       const start = normalizedRange.start;
       if (!start) {
         return;
@@ -239,7 +276,7 @@
       };
       actionBox.side = start.side;
       if (start.line === end.line) {
-        actionBox.placeAbove(range);
+        actionBox.placeAbove(domRange);
       } else if (start.node instanceof Text) {
         actionBox.placeAbove(start.node.splitText(start.column));
         start.node.parentElement.normalize(); // Undo splitText from above.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index f8e2a77..b63b9a4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -298,6 +298,37 @@
         assert.equal(getActionSide(), 'right');
       });
 
+      test('multiple ranges aka firefox implementation', () => {
+        const startContent = stubContent(119, 'right');
+        const endContent = stubContent(120, 'right');
+
+        const startRange = document.createRange();
+        startRange.setStart(startContent.firstChild, 10);
+        startRange.setEnd(startContent.firstChild, 11);
+
+        const endRange = document.createRange();
+        endRange.setStart(endContent.lastChild, 6);
+        endRange.setEnd(endContent.lastChild, 7);
+
+        const getRangeAtStub = sandbox.stub();
+        getRangeAtStub
+            .onFirstCall().returns(startRange)
+            .onSecondCall().returns(endRange);
+        sandbox.stub(window, 'getSelection').returns({
+          rangeCount: 2,
+          getRangeAt: getRangeAtStub,
+          removeAllRanges: sandbox.stub(),
+        });
+        element._handleSelection();
+        assert.isTrue(element.isRangeSelected());
+        assert.deepEqual(getActionRange(), {
+          startLine: 119,
+          startChar: 10,
+          endLine: 120,
+          endChar: 36,
+        });
+      });
+
       test('multiline grow end highlight over tabs', () => {
         const startContent = stubContent(119, 'right');
         const endContent = stubContent(120, 'right');
@@ -496,6 +527,7 @@
         result = GrRangeNormalizer._getTextOffset(content, child);
         assert.equal(result, 0);
       });
+
       // TODO (viktard): Selection starts in line number.
       // TODO (viktard): Empty lines in selection start.
       // TODO (viktard): Empty lines in selection end.
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 1b918db..c34f00f 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
@@ -57,6 +57,7 @@
         type: Object,
         notify: true,
         value() { return {}; },
+        observer: '_changeViewStatehanged',
       },
 
       _patchRange: Object,
@@ -120,6 +121,7 @@
     observers: [
       '_getProjectConfig(_change.project)',
       '_getFiles(_changeNum, _patchRange.*)',
+      '_setReviewedObserver(_loggedIn, params.*)',
     ],
 
     keyBindings: {
@@ -141,21 +143,7 @@
     attached() {
       this._getLoggedIn().then(loggedIn => {
         this._loggedIn = loggedIn;
-        if (loggedIn) {
-          this._setReviewed(true);
-        }
       });
-      if (this.changeViewState.diffMode === null) {
-        // If screen size is small, always default to unified view.
-        this.$.restAPI.getPreferences().then(prefs => {
-          this.set('changeViewState.diffMode', prefs.default_diff_view);
-        });
-      }
-
-      if (this._path) {
-        this.fire('title-change',
-            {title: this._computeFileDisplayName(this._path)});
-      }
 
       this.$.cursor.push('diffs', this.$.diff);
     },
@@ -476,6 +464,21 @@
       });
     },
 
+    _changeViewStatehanged(changeViewState) {
+      if (changeViewState.diffMode === null) {
+        // If screen size is small, always default to unified view.
+        this.$.restAPI.getPreferences().then(prefs => {
+          this.set('changeViewState.diffMode', prefs.default_diff_view);
+        });
+      }
+    },
+
+    _setReviewedObserver(_loggedIn) {
+      if (_loggedIn) {
+        this._setReviewed(true);
+      }
+    },
+
     /**
      * If the URL hash is a diff address then configure the diff cursor.
      */
@@ -492,14 +495,15 @@
     },
 
     _pathChanged(path) {
+      if (path) {
+        this.fire('title-change',
+            {title: this._computeFileDisplayName(path)});
+      }
+
       if (this._fileList.length == 0) { return; }
 
       this.set('changeViewState.selectedFileIndex',
           this._fileList.indexOf(path));
-
-      if (this._loggedIn) {
-        this._setReviewed(true);
-      }
     },
 
     _getDiffURL(changeNum, patchRange, path) {
@@ -522,6 +526,7 @@
 
     _computeAvailablePatches(revisions) {
       const patchNums = [];
+      if (!revisions) { return patchNums; }
       for (const rev of Object.values(revisions)) {
         patchNums.push(rev._number);
       }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 13d7140..0a7ed28 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -54,6 +54,8 @@
         getDiffChangeDetail() { return Promise.resolve(null); },
         getChangeFiles() { return Promise.resolve({}); },
         saveFileReviewed() { return Promise.resolve(); },
+        getDiffRobotComments() { return Promise.resolve(); },
+        getDiffDrafts() { return Promise.resolve(); },
       });
       element = fixture('basic');
     });
@@ -469,16 +471,21 @@
     });
 
     test('file review status', done => {
-      element._loggedIn = true;
-      element._changeNum = '42';
-      element._patchRange = {
-        basePatchNum: '1',
-        patchNum: '2',
-      };
-      element._fileList = ['/COMMIT_MSG'];
-      element._path = '/COMMIT_MSG';
+      stub('gr-rest-api-interface', {
+        getDiffComments() { return Promise.resolve({}); },
+      });
       const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
           () => Promise.resolve());
+      sandbox.stub(element.$.diff, 'reload');
+
+      element._loggedIn = true;
+      element.params = {
+        view: 'gr-diff-view',
+        changeNum: '42',
+        patchNum: '2',
+        basePatchNum: '1',
+        path: '/COMMIT_MSG',
+      };
 
       flush(() => {
         const commitMsg = Polymer.dom(element.root).querySelector(
@@ -625,7 +632,6 @@
     suite('_loadCommentMap', () => {
       test('empty', done => {
         stub('gr-rest-api-interface', {
-          getDiffRobotComments() { return Promise.resolve({}); },
           getDiffComments() { return Promise.resolve({}); },
         });
         element._loadCommentMap().then(map => {
@@ -636,7 +642,6 @@
 
       test('paths in patch range', done => {
         stub('gr-rest-api-interface', {
-          getDiffRobotComments() { return Promise.resolve({}); },
           getDiffComments() {
             return Promise.resolve({
               'path/to/file/one.cpp': [{patch_set: 3, message: 'lorem'}],
@@ -658,7 +663,6 @@
 
       test('empty for paths outside patch range', done => {
         stub('gr-rest-api-interface', {
-          getDiffRobotComments() { return Promise.resolve({}); },
           getDiffComments() {
             return Promise.resolve({
               'path/to/file/one.cpp': [{patch_set: 'PARENT', message: 'lorem'}],
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 5319ace..6aae8d1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -142,20 +142,6 @@
       .content.remove {
         background-color: var(--light-remove-highlight-color);
       }
-      .dueToRebase .content.add .intraline,
-      .delta.total.dueToRebase .content.add {
-        background-color: var(--dark-rebased-add-highlight-color);
-      }
-      .dueToRebase .content.add {
-        background-color: var(--light-rebased-add-highlight-color);
-      }
-      .dueToRebase .content.remove .intraline,
-      .delta.total.dueToRebase .content.remove {
-        background-color: var(--dark-rebased-remove-highlight-color);
-      }
-      .dueToRebase .content.remove {
-        background-color: var(--light-rebased-remove-highlight-color);
-      }
       .content .contentText:after {
         /* Newline, to ensure all lines are one line-height tall. */
         content: '\A';
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 68f0294..754ca69d 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -15,6 +15,13 @@
 -->
 
 <link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/polymer-resin/standalone/polymer-resin.html">
+<script>
+  security.polymer_resin.install({
+    allowedIdentifierPrefixes: [''],
+    reportHandler: security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER,
+  });
+</script>
 
 <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">
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
index a3c44e2..ad4e2b0 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
@@ -15,6 +15,7 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
 <dom-module id="gr-plugin-host">
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index cd7059c..efad106 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -24,6 +24,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+    ],
+
     _configChanged(config) {
       const jsPlugins = config.js_resource_paths || [];
       const htmlPlugins = config.html_resource_paths || [];
@@ -33,23 +37,32 @@
     },
 
     _importHtmlPlugins(plugins) {
-      for (let url of plugins) {
-        if (!url.startsWith('http')) {
-          url = '/' + url;
-        }
+      for (const url of plugins) {
         this.importHref(
-            url, Gerrit._pluginInstalled, Gerrit._pluginInstalled, true);
+            this._urlFor(url), Gerrit._pluginInstalled, Gerrit._pluginInstalled,
+            true);
       }
     },
 
     _loadJsPlugins(plugins) {
-      for (let i = 0; i < plugins.length; i++) {
-        const scriptEl = document.createElement('script');
-        scriptEl.defer = true;
-        scriptEl.src = '/' + plugins[i];
-        scriptEl.onerror = Gerrit._pluginInstalled;
-        document.body.appendChild(scriptEl);
+      for (const url of plugins) {
+        this._createScriptTag(this._urlFor(url));
       }
     },
+
+    _createScriptTag(url) {
+      const el = document.createElement('script');
+      el.defer = true;
+      el.src = url;
+      el.onerror = Gerrit._pluginInstalled;
+      return document.body.appendChild(el);
+    },
+
+    _urlFor(pathOrUrl) {
+      if (pathOrUrl.startsWith('http')) {
+        return pathOrUrl;
+      }
+      return this.getBaseUrl() + '/' + pathOrUrl;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
index 1e81092..ead0781 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -54,7 +54,7 @@
       assert.isTrue(Gerrit._setPluginsCount.calledWith(3));
     });
 
-    test('imports html plugins from config', () => {
+    test('imports relative html plugins from config', () => {
       element.config = {
         html_resource_paths: ['foo/bar', 'baz'],
       };
@@ -63,5 +63,73 @@
       assert.isTrue(element.importHref.calledWith(
           '/baz', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
     });
+
+    test('imports relative html plugins from config with a base url', () => {
+      sandbox.stub(element, 'getBaseUrl').returns('/the-base');
+      element.config = {
+        html_resource_paths: ['foo/bar', 'baz'],
+      };
+      assert.isTrue(element.importHref.calledWith(
+          '/the-base/foo/bar', Gerrit._pluginInstalled, Gerrit._pluginInstalled,
+          true));
+      assert.isTrue(element.importHref.calledWith(
+          '/the-base/baz', Gerrit._pluginInstalled, Gerrit._pluginInstalled,
+          true));
+    });
+
+    test('imports absolute html plugins from config', () => {
+      element.config = {
+        html_resource_paths: [
+          'http://example.com/foo/bar',
+          'https://example.com/baz',
+        ],
+      };
+      assert.isTrue(element.importHref.calledWith(
+          'http://example.com/foo/bar', Gerrit._pluginInstalled,
+          Gerrit._pluginInstalled, true));
+      assert.isTrue(element.importHref.calledWith(
+          'https://example.com/baz', Gerrit._pluginInstalled,
+          Gerrit._pluginInstalled, true));
+    });
+
+    test('adds js plugins from config to the body', () => {
+      element.config = {
+        js_resource_paths: ['foo/bar', 'baz'],
+      };
+      assert.isTrue(document.body.appendChild.calledTwice);
+    });
+
+    test('imports relative js plugins from config', () => {
+      sandbox.stub(element, '_createScriptTag');
+      element.config = {
+        js_resource_paths: ['foo/bar', 'baz'],
+      };
+      assert.isTrue(element._createScriptTag.calledWith('/foo/bar'));
+      assert.isTrue(element._createScriptTag.calledWith('/baz'));
+    });
+
+    test('imports relative html plugins from config with a base url', () => {
+      sandbox.stub(element, '_createScriptTag');
+      sandbox.stub(element, 'getBaseUrl').returns('/the-base');
+      element.config = {
+        js_resource_paths: ['foo/bar', 'baz'],
+      };
+      assert.isTrue(element._createScriptTag.calledWith('/the-base/foo/bar'));
+      assert.isTrue(element._createScriptTag.calledWith('/the-base/baz'));
+    });
+
+    test('imports absolute html plugins from config', () => {
+      sandbox.stub(element, '_createScriptTag');
+      element.config = {
+        js_resource_paths: [
+          'http://example.com/foo/bar',
+          'https://example.com/baz',
+        ],
+      };
+      assert.isTrue(element._createScriptTag.calledWith(
+          'http://example.com/foo/bar'));
+      assert.isTrue(element._createScriptTag.calledWith(
+          'https://example.com/baz'));
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
index c93c509..e4324de 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
@@ -23,27 +23,30 @@
 <dom-module id="gr-agreements-list">
   <template>
     <style include="shared-styles">
-      .nameHeader {
-        width: 15em;
+      #agreements .nameColumn {
+        min-width: 11em;
+        width: auto;
       }
-      .descriptionHeader {
-        width: 21.5em;
+      #agreements .descriptionColumn {
+        width: auto;
       }
     </style>
     <style include="gr-form-styles"></style>
     <div class="gr-form-styles">
-      <table>
+      <table id="agreements">
         <thead>
           <tr>
-            <th class="nameHeader">Name</th>
-            <th class="descriptionHeader">Description</th>
+            <th class="nameColumn">Name</th>
+            <th class="descriptionColumn">Description</th>
           </tr>
         </thead>
         <tbody>
           <template is="dom-repeat" items="[[_agreements]]">
             <tr>
-              <td><a href$="[[getUrlBase(item.url)]]">[[item.name]]</a></td>
-              <td>[[item.description]]</td>
+              <td class="nameColumn">
+                <a href$="[[getUrlBase(item.url)]]">[[item.name]]</a>
+              </td>
+              <td class="descriptionColumn">[[item.description]]</td>
             </tr>
           </template>
         </tbody>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
index a297aa9..c93ed0c 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
@@ -24,29 +24,28 @@
 
 <dom-module id="gr-change-table-editor">
   <template>
-    <style include="shared-styles">
-      table {
-        margin-top: 1em;
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles">
+      #changeCols {
+        width: auto;
       }
-      th.nameHeader {
-        width: 11em;
+      #changeCols .visibleHeader {
+        text-align: center;
       }
-      td.checkboxContainer {
-        border: 1px solid #fff;
+      .checkboxContainer {
         cursor: pointer;
         text-align: center;
       }
-      td.checkboxContainer:hover {
-        border: 1px solid #ddd;
+      .checkboxContainer:hover {
+        outline: 1px solid #ddd;
       }
     </style>
-    <style include="gr-form-styles"></style>
     <div class="gr-form-styles">
-      <table>
+      <table id="changeCols">
         <thead>
           <tr>
             <th class="nameHeader">Column</th>
-            <th>Visible</th>
+            <th class="visibleHeader">Visible</th>
           </tr>
         </thead>
         <tbody>
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
index 68d9576..99a0392 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
@@ -22,35 +22,37 @@
 
 <dom-module id="gr-email-editor">
   <template>
-    <style include="shared-styles">
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles">
       th {
         color: #666;
         text-align: left;
       }
-      th.emailHeader {
-        width: 32.5em;
+      #emailTable .emailColumn {
+        min-width: 32.5em;
+        width: auto;
       }
-      th.preferredHeader {
+      #emailTable .preferredHeader {
         text-align: center;
         width: 6em;
       }
-      tbody tr:nth-child(even) {
-        background-color: #f4f4f4;
-      }
-      td.preferredControl {
+      #emailTable .preferredControl {
         cursor: pointer;
+        height: auto;
         text-align: center;
       }
-      td.preferredControl:hover {
-        border: 1px solid #ddd;
+      #emailTable .preferredControl .preferredRadio {
+        height: auto;
+      }
+      .preferredControl:hover {
+        outline: 1px solid #d1d2d3;
       }
     </style>
-    <style include="gr-form-styles"></style>
     <div class="gr-form-styles">
-      <table>
+      <table id="emailTable">
         <thead>
           <tr>
-            <th class="emailHeader">Email</th>
+            <th class="emailColumn">Email</th>
             <th class="preferredHeader">Preferred</th>
             <th></th>
           </tr>
@@ -58,10 +60,11 @@
         <tbody>
           <template is="dom-repeat" items="[[_emails]]">
             <tr>
-              <td>[[item.email]]</td>
+              <td class="emailColumn">[[item.email]]</td>
               <td class="preferredControl" on-tap="_handlePreferredControlTap">
                 <input
                     is="iron-input"
+                    class="preferredRadio"
                     type="radio"
                     on-change="_handlePreferredChange"
                     name="preferred"
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
index 23d062e..bcae0bf 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
@@ -21,31 +21,33 @@
 
 <dom-module id="gr-group-list">
   <template>
-    <style include="shared-styles">
-      .nameHeader {
-        width: 15em;
-      }
-      .descriptionHeader {
-        width: 21.5em;
-      }
-      .visibleCell {
-        text-align: center;
-      }
-    </style>
-    <style include="gr-form-styles"></style>
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles">
+        #groups .nameColumn {
+          min-width: 11em;
+          width: auto;
+        }
+        .descriptionHeader {
+          min-width: 21.5em;
+        }
+        .visibleCell {
+          text-align: center;
+          width: 6em;
+        }
+      </style>
     <div class="gr-form-styles">
-      <table>
+      <table id="groups">
         <thead>
           <tr>
             <th class="nameHeader">Name</th>
             <th class="descriptionHeader">Description</th>
-            <th>Visible to all</th>
+            <th class="visibleCell">Visible to all</th>
           </tr>
         </thead>
         <tbody>
           <template is="dom-repeat" items="[[_groups]]">
             <tr>
-              <td>[[item.name]]</td>
+              <td class="nameColumn">[[item.name]]</td>
               <td>[[item.description]]</td>
               <td class="visibleCell">[[_computeVisibleToAll(item)]]</td>
             </tr>
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
index 597aae9..fa3428a 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
@@ -25,21 +25,22 @@
 <dom-module id="gr-menu-editor">
   <template>
     <style include="shared-styles">
-      th.nameHeader {
-        width: 11em;
+      .buttonColumn {
+        width: 2em;
       }
-      tbody tr:first-of-type td .move-up-button,
-      tbody tr:last-of-type td .move-down-button {
+      .moveUpButton,
+      .moveDownButton {
+        width: 100%
+      }
+      tbody tr:first-of-type td .moveUpButton,
+      tbody tr:last-of-type td .moveDownButton {
         display: none;
       }
       td.urlCell {
         word-break: break-word;
       }
-      .newTitleInput {
-        width: 10em;
-      }
       .newUrlInput {
-        width: 23em;
+        min-width: 23em;
       }
     </style>
     <style include="gr-form-styles"></style>
@@ -56,17 +57,17 @@
             <tr>
               <td>[[item.name]]</td>
               <td class="urlCell">[[item.url]]</td>
-              <td>
+              <td class="buttonColumn">
                 <gr-button
                     data-index="[[index]]"
                     on-tap="_handleMoveUpButton"
-                    class="move-up-button">↑</gr-button>
+                    class="moveUpButton">↑</gr-button>
               </td>
-              <td>
+              <td class="buttonColumn">
                 <gr-button
                     data-index="[[index]]"
                     on-tap="_handleMoveDownButton"
-                    class="move-down-button">↓</gr-button>
+                    class="moveDownButton">↓</gr-button>
               </td>
               <td>
                 <gr-button
@@ -81,7 +82,6 @@
           <tr>
             <th>
               <input
-                  class="newTitleInput"
                   is="iron-input"
                   placeholder="New Title"
                   on-keydown="_handleInputKeydown"
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
index 7e12483..f16ba6c 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
@@ -48,7 +48,7 @@
     // The index of the first row is 0, corresponding to the array.
     function move(element, index, direction) {
       const selector =
-          'tr:nth-child(' + (index + 1) + ') .move-' + direction + '-button';
+          'tr:nth-child(' + (index + 1) + ') .move' + direction + 'Button';
       const button = element.$$('tbody').querySelector(selector);
       MockInteractions.tap(button);
     }
@@ -110,12 +110,12 @@
           ['first name', 'second name', 'third name']);
 
       // Move the middle item down
-      move(element, 1, 'down');
+      move(element, 1, 'Down');
       assertMenuNamesEqual(element,
           ['first name', 'third name', 'second name']);
 
       // Moving the bottom item down is a no-op.
-      move(element, 2, 'down');
+      move(element, 2, 'Down');
       assertMenuNamesEqual(element,
           ['first name', 'third name', 'second name']);
     });
@@ -125,13 +125,13 @@
           ['first name', 'second name', 'third name']);
 
       // Move the last item up twice to be the first.
-      move(element, 2, 'up');
-      move(element, 1, 'up');
+      move(element, 2, 'Up');
+      move(element, 1, 'Up');
       assertMenuNamesEqual(element,
           ['third name', 'first name', 'second name']);
 
       // Moving the top item up is a no-op.
-      move(element, 0, 'up');
+      move(element, 0, 'Up');
       assertMenuNamesEqual(element,
           ['third name', 'first name', 'second name']);
     });
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 2cf3c94..ca2feb1 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
@@ -40,6 +40,9 @@
       #newEmailInput {
         width: 20em;
       }
+      #email {
+        margin-bottom: 1em;
+      }
     </style>
     <style include="gr-form-styles"></style>
     <style include="gr-menu-page-styles"></style>
@@ -59,9 +62,11 @@
             SSH Keys
           </a></li>
           <li><a href="#Groups">Groups</a></li>
-          <li hidden$="[[!_serverConfig.auth.contributor_agreements]]">
-            <a href="#Agreements">Agreements</a>
-          </li>
+          <template is="dom-if" if="[[_serverConfig.auth.use_contributor_agreements]]">
+            <li>
+              <a href="#Agreements">Agreements</a>
+            </li>
+          </template>
         </ul>
       </gr-page-nav>
       <main class="gr-form-styles">
@@ -364,12 +369,12 @@
         <fieldset>
           <gr-group-list id="groupList"></gr-group-list>
         </fieldset>
-        <div hidden$="[[!_serverConfig.auth.contributor_agreements]]">
+        <template is="dom-if" if="[[_serverConfig.auth.use_contributor_agreements]]">
           <h2 id="Agreements">Agreements</h2>
           <fieldset>
             <gr-agreements-list id="agreementsList"></gr-agreements-list>
           </fieldset>
-        </div>
+        </template>
       </main>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
index 82d8ebe..1afd255 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
@@ -24,10 +24,8 @@
 
 <dom-module id="gr-ssh-editor">
   <template>
-    <style include="shared-styles">
-      .commentHeader {
-        width: 27em;
-      }
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles">
       .statusHeader {
         width: 4em;
       }
@@ -49,22 +47,29 @@
         position: absolute;
         right: 2em;
       }
+      #existing {
+        margin-bottom: 1em;
+      }
+      #existing .commentColumn {
+        min-width: 27em;
+        width: auto;
+      }
     </style>
-    <style include="gr-form-styles"></style>
     <div class="gr-form-styles">
-      <fieldset>
+      <fieldset id="existing">
         <table>
           <thead>
             <tr>
-              <th class="commentHeader">Comment</th>
+              <th class="commentColumn">Comment</th>
               <th class="statusHeader">Status</th>
               <th class="keyHeader">Public key</th>
+              <th></th>
             </tr>
           </thead>
           <tbody>
             <template is="dom-repeat" items="[[_keys]]" as="key">
               <tr>
-                <td>[[key.comment]]</td>
+                <td class="commentColumn">[[key.comment]]</td>
                 <td>[[_getStatusLabel(key.valid)]]</td>
                 <td>
                   <gr-button
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
index e82c4b0c..a834ea2 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
@@ -22,42 +22,30 @@
 
 <dom-module id="gr-watched-projects-editor">
   <template>
-    <style include="shared-styles">
-      th.projectHeader {
-        width: 11em;
-      }
-      th.notificationHeader {
-        text-align: center;
-      }
-      th.notifType {
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles">
+      #watchedProjects .notifType {
         text-align: center;
         padding: 0 0.4em;
       }
-      td.notifControl {
+      .notifControl {
         cursor: pointer;
         text-align: center;
       }
-      td.notifControl:hover {
-        border: 1px solid #ddd;
+      .notifControl:hover {
+        outline: 1px solid #ddd;
       }
       .projectFilter {
         color: #777;
         font-style: italic;
         margin-left: 1em;
       }
-      input {
-        font-size: 1em;
-      }
-      .newProjectInput {
-        width: 10em;
-      }
       .newFilterInput {
-        width: 26em;
+        width: 100%;
       }
     </style>
-    <style include="gr-form-styles"></style>
     <div class="gr-form-styles">
-      <table>
+      <table id="watchedProjects">
         <thead>
           <tr>
             <th class="projectHeader">Project</th>
@@ -93,7 +81,7 @@
                       checked$="[[_computeCheckboxChecked(project, type.key)]]">
                 </td>
               </template>
-              <td class="delete-column">
+              <td>
                 <gr-button
                     data-index$="[[projectIndex]]"
                     on-tap="_handleRemoveProject">Delete</gr-button>
@@ -106,7 +94,6 @@
             <th>
               <gr-autocomplete
                   id="newProject"
-                  class="newProjectInput"
                   query="[[_query]]"
                   threshold="3"
                   placeholder="Project"></gr-autocomplete>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
index f2c3bdc..a517e6a 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
@@ -76,6 +76,7 @@
           id="remove"
           hidden$="[[!removable]]"
           hidden
+          tabindex="-1"
           aria-label="Remove"
           class$="remove [[_getBackgroundClass(transparentBackground)]]"
           on-tap="_handleRemoveTap">×</gr-button>
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 42a6f6b..79747ba 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
@@ -37,7 +37,7 @@
       }
     </style>
     <span>
-      <a href$="[[_computeOwnerLink(account)]]">
+      <a href$="[[_computeOwnerLink(account)]]" tabindex="-1">
         <gr-account-label account="[[account]]"
             avatar-image-size="[[avatarImageSize]]"
             show-email="[[_computeShowEmail(account)]]"></gr-account-label>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index 94e17ea..2a86a0c 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -27,6 +27,7 @@
         font-size: 1em;
         height: 100%;
         width: 100%;
+        @apply --gr-autocomplete;
       }
       input.borderless,
       input.borderless:focus {
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
index 32b6f67..4095d3e 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
@@ -16,6 +16,7 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-avatar">
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index 166204bf..5fca577 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -28,6 +28,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+    ],
+
     created() {
       this.hidden = true;
     },
@@ -64,7 +68,8 @@
           return avatars[i].url;
         }
       }
-      return '/accounts/' + account._account_id + '/avatar?s=' + this.imageSize;
+      return this.getBaseUrl() + '/accounts/' +
+        account._account_id + '/avatar?s=' + this.imageSize;
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
index c28679c..4a45352 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -55,10 +55,10 @@
       }, REQUEST_DEBOUNCE_INTERVAL_MS);
     },
 
-    _computeNavLink(offset, direction, projectsPerPage, filter) {
+    _computeNavLink(offset, direction, itemsPerPage, filter) {
       // Offset could be a string when passed from the router.
       offset = +(offset || 0);
-      const newOffset = Math.max(0, offset + (projectsPerPage * direction));
+      const newOffset = Math.max(0, offset + (itemsPerPage * direction));
       let href = this.getBaseUrl() + this.path;
       if (filter) {
         href += '/q/filter:' + filter;
@@ -73,12 +73,12 @@
       return offset === 0;
     },
 
-    _hideNextArrow(loading, projects) {
+    _hideNextArrow(loading, items) {
       let lastPage = false;
-      if (projects.length < this.itemsPerPage + 1) {
+      if (items.length < this.itemsPerPage + 1) {
         lastPage = true;
       }
-      return loading || lastPage || !projects || !projects.length;
+      return loading || lastPage || !items || !items.length;
     },
   });
 })();
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 b7090da..3f954b4 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
@@ -40,6 +40,9 @@
         border-top: 1px solid transparent;
         padding: 0 2em;
       }
+      #nav ::content li  a {
+        word-break: break-all;
+      }
       #nav ::content .subsectionItem {
         padding-left: 3em;
       }
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 eb2b8c6..4f83086 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
@@ -523,6 +523,16 @@
       );
     },
 
+    getProjectBranches(filter, project, projectsBranchesPerPage, opt_offset) {
+      const offset = opt_offset || 0;
+      filter = filter ? '&m=' + filter : '';
+
+      return this._fetchSharedCacheURL(
+          `/projects/${project}/branches?n=${projectsBranchesPerPage + 1}&s=` +
+          `${offset}${filter}`
+      );
+    },
+
     getPlugins() {
       return this._fetchSharedCacheURL('/plugins/?all');
     },
diff --git a/polygerrit-ui/app/embed/change-diff-views.html b/polygerrit-ui/app/embed/change-diff-views.html
index b4f521b..8426585 100644
--- a/polygerrit-ui/app/embed/change-diff-views.html
+++ b/polygerrit-ui/app/embed/change-diff-views.html
@@ -16,3 +16,4 @@
 <link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../elements/change/gr-change-view/gr-change-view.html">
 <link rel="import" href="../elements/diff/gr-diff-view/gr-diff-view.html">
+<link rel="import" href="../styles/app-theme.html">
diff --git a/polygerrit-ui/app/embed/embed_test.html b/polygerrit-ui/app/embed/embed_test.html
new file mode 100644
index 0000000..26ea895
--- /dev/null
+++ b/polygerrit-ui/app/embed/embed_test.html
@@ -0,0 +1,51 @@
+<!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>change-diff-views-embed_test</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="../polygerrit_ui/elements/change-diff-views.html"/>
+
+<script>void(0);</script>
+
+<test-fixture id="change-view">
+  <template>
+    <gr-change-view></gr-change-view>
+  </template>
+</test-fixture>
+
+<test-fixture id="diff-view">
+  <template>
+    <gr-diff-view></gr-diff-view>
+  </template>
+</test-fixture>
+
+<script>
+  suite('embed test', () => {
+    test('gr-change-view is embedded', () => {
+      const element = fixture('change-view');
+      assert.equal(element.is, 'gr-change-view');
+    });
+
+    test('diff-view is embedded', () => {
+      const element = fixture('diff-view');
+      assert.equal(element.is, 'gr-diff-view');
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/embed/test.html b/polygerrit-ui/app/embed/test.html
new file mode 100644
index 0000000..0587562
--- /dev/null
+++ b/polygerrit-ui/app/embed/test.html
@@ -0,0 +1,25 @@
+<!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>Embed Test Runner</title>
+<meta charset="utf-8">
+<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../bower_components/web-component-tester/browser.js"></script>
+<script>
+  WCT.loadSuites(['embed_test.html']);
+</script>
diff --git a/polygerrit-ui/app/embed_test.sh b/polygerrit-ui/app/embed_test.sh
new file mode 100755
index 0000000..adcc653
--- /dev/null
+++ b/polygerrit-ui/app/embed_test.sh
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+set -ex
+
+t=$(mktemp -d || mktemp -d -t wct-XXXXXXXXXX)
+components=$TEST_SRCDIR/gerrit/polygerrit-ui/app/test_components.zip
+code=$TEST_SRCDIR/gerrit/polygerrit-ui/app/polygerrit_embed_ui.zip
+index=$TEST_SRCDIR/gerrit/polygerrit-ui/app/embed/test.html
+tests=$TEST_SRCDIR/gerrit/polygerrit-ui/app/embed/*_test.html
+
+unzip -qd $t $components
+unzip -qd $t $code
+mkdir -p $t/test
+cp $index $t/test/
+cp $tests $t/test/
+
+# For some reason wct tries to install selenium into its node_modules
+# directory on first run. If you've installed into /usr/local and
+# aren't running wct as root, you're screwed. Turning this option off
+# through skipSeleniumInstall seems to still work, so there's that.
+
+# Sauce tests are disabled by default in order to run local tests
+# only.  Run it with (saucelabs.com account required; free for open
+# source): WCT_ARGS='--plugin sauce' ./polygerrit-ui/app/embed_test.sh
+
+cat <<EOF > $t/wct.conf.js
+module.exports = {
+      'suites': ['test'],
+      'webserver': {
+        'pathMappings': [
+          {'/components/bower_components': 'bower_components'}
+        ]
+      },
+      'plugins': {
+        'local': {
+          'skipSeleniumInstall': true
+        },
+        'sauce': {
+          'disabled': true,
+          'browsers': [
+            'OS X 10.12/chrome',
+            'Windows 10/chrome',
+            'Linux/firefox',
+            'OS X 10.12/safari',
+            'Windows 10/microsoftedge'
+          ]
+        }
+      }
+    };
+EOF
+
+export PATH="$(dirname $WCT):$(dirname $NPM):$PATH"
+
+cd $t
+test -n "${WCT}"
+
+$(basename ${WCT}) ${WCT_ARGS}
diff --git a/polygerrit-ui/app/index.html b/polygerrit-ui/app/index.html
index 5e9cde8..46fc46b 100644
--- a/polygerrit-ui/app/index.html
+++ b/polygerrit-ui/app/index.html
@@ -29,6 +29,14 @@
 <link rel="stylesheet" href="/styles/fonts.css">
 <link rel="stylesheet" href="/styles/main.css">
 <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<!--
+  - Content between webcomponents-lite and the load of the main app element
+  - run before polymer-resin is installed so may have security consequences.
+  - Contact your local security engineer if you have any questions, and
+  - CC them on any changes that load content before gr-app.html.
+  -
+  - github.com/Polymer/polymer-resin/blob/master/getting-started.md#integrating
+  -->
 <link rel="preload" href="/elements/gr-app.js" as="script" crossorigin="anonymous">
 <link rel="import" href="/elements/gr-app.html">
 
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
new file mode 100644
index 0000000..be80c13
--- /dev/null
+++ b/polygerrit-ui/app/rules.bzl
@@ -0,0 +1,94 @@
+load("//tools/bzl:genrule2.bzl", "genrule2")
+load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library", "closure_js_binary")
+load(
+    "//tools/bzl:js.bzl",
+    "bower_component_bundle",
+    "vulcanize",
+    "bower_component",
+    "js_component",
+)
+
+def polygerrit_bundle(name, srcs, outs, app):
+  appName = app.split(".html")[0].split("/").pop() # eg: gr-app
+
+  closure_js_binary(
+    name = name + "_closure_bin",
+    # Known issue: Closure compilation not compatible with Polymer behaviors.
+    # See: https://github.com/google/closure-compiler/issues/2042
+    compilation_level = "WHITESPACE_ONLY",
+    defs = [
+      "--polymer_pass",
+      "--jscomp_off=duplicate",
+      "--force_inject_library=es6_runtime",
+    ],
+    language = "ECMASCRIPT5",
+    deps = [name + "_closure_lib"],
+  )
+
+  closure_js_library(
+    name = name + "_closure_lib",
+    srcs = [appName + ".js"],
+    convention = "GOOGLE",
+    # TODO(davido): Clean up these issues: http://paste.openstack.org/show/608548
+    # and remove this supression
+    suppress = ["JSC_UNUSED_LOCAL_ASSIGNMENT"],
+    deps = [
+      "//lib/polymer_externs:polymer_closure",
+      "@io_bazel_rules_closure//closure/library",
+    ],
+  )
+
+  vulcanize(
+    name = appName,
+    srcs = srcs,
+    app = app,
+    deps = ["//polygerrit-ui:polygerrit_components.bower_components"],
+  )
+
+  native.filegroup(
+    name = name + "_app_sources",
+    srcs = [
+      name + "_closure_bin.js",
+      appName + ".html",
+    ],
+  )
+
+  native.filegroup(
+    name = name + "_css_sources",
+    srcs = native.glob(["styles/**/*.css"]),
+  )
+
+  native.filegroup(
+    name = name + "_top_sources",
+    srcs = [
+        "favicon.ico",
+        "index.html",
+    ],
+  )
+
+  genrule2(
+    name = name,
+    srcs = [
+      name + "_app_sources",
+      name + "_css_sources",
+      name + "_top_sources",
+      "//lib/fonts:robotomono",
+      "//lib/js:highlightjs_files",
+      # we extract from the zip, but depend on the component for license checking.
+      "@webcomponentsjs//:zipfile",
+      "//lib/js:webcomponentsjs"
+    ],
+    outs = outs,
+    cmd = " && ".join([
+      "mkdir -p $$TMP/polygerrit_ui/{styles,fonts,bower_components/{highlightjs,webcomponentsjs},elements}",
+      "for f in $(locations " + name + "_app_sources); do ext=$${f##*.}; cp -p $$f $$TMP/polygerrit_ui/elements/"  + appName + ".$$ext; done",
+      "cp $(locations //lib/fonts:robotomono) $$TMP/polygerrit_ui/fonts/",
+      "for f in $(locations " + name + "_top_sources); do cp $$f $$TMP/polygerrit_ui/; done",
+      "for f in $(locations "+ name + "_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",
+      "unzip -qd $$TMP/polygerrit_ui/bower_components $(location @webcomponentsjs//:zipfile) webcomponentsjs/webcomponents-lite.js",
+      "cd $$TMP",
+      "find . -exec touch -t 198001010000 '{}' ';'",
+      "zip -qr $$ROOT/$@ *",
+    ]),
+  )
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh
index c2421ad..0edf41c 100755
--- a/polygerrit-ui/app/run_test.sh
+++ b/polygerrit-ui/app/run_test.sh
@@ -21,4 +21,5 @@
       --test_env="NPM=${npm_bin}" \
       --test_env="DISPLAY=${DISPLAY}" \
       "$@" \
+      //polygerrit-ui/app:embed_test \
       //polygerrit-ui/app:wct_test
diff --git a/polygerrit-ui/app/styles/gr-form-styles.html b/polygerrit-ui/app/styles/gr-form-styles.html
index d8b799b..9fefc0e 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.html
+++ b/polygerrit-ui/app/styles/gr-form-styles.html
@@ -19,17 +19,23 @@
       .gr-form-styles h1 {
         margin-bottom: .1em;
       }
+      .gr-form-styles h2 {
+        margin-bottom: .3em;
+      }
       .gr-form-styles fieldset {
         border: none;
         margin: 0 0 2em 2em;
       }
       .gr-form-styles section {
-        margin-bottom: .5em;
+        margin: .15em 0;
+        min-height: 2em;
+      }
+      .gr-form-styles section * {
+        vertical-align: middle;
       }
       .gr-form-styles .title,
       .gr-form-styles .value {
         display: inline-block;
-        vertical-align: top;
       }
       .gr-form-styles .title {
         color: #666;
@@ -37,19 +43,77 @@
         padding-right: .5em;
         width: 11em;
       }
-      .gr-form-styles input {
-        font-size: 1em;
-      }
       .gr-form-styles iron-autogrow-textarea {
         font-size: 1em;
       }
       .gr-form-styles th {
         color: #666;
         text-align: left;
+        vertical-align: bottom;
+      }
+      .gr-form-styles td,
+      .gr-form-styles tfoot th {
+        height: 2em;
+        vertical-align: middle;
+      }
+      .gr-form-styles .emptyHeader {
+        text-align: right;
       }
       .gr-form-styles tbody tr:nth-child(even) {
         background-color: #f4f4f4;
       }
+      .gr-form-styles table {
+        width: 50em;
+      }
+      .gr-form-styles th:first-child,
+      .gr-form-styles td:first-child {
+        width: 11em;
+      }
+      .gr-form-styles th:first-child input,
+      .gr-form-styles td:first-child input {
+        width: 10em;
+      }
+      .gr-form-styles input:not([type="checkbox"]),
+      .gr-form-styles select,
+      .gr-form-styles textarea {
+        border: 1px solid #d1d2d3;
+        border-radius: 2px;
+        font-size: 1em;
+        height: 2em;
+        padding: 0 .15em;
+      }
+      .gr-form-styles gr-button:not([link]) {
+        height: 2.2em;
+      }
+      .gr-form-styles td:last-child {
+        width: 5em;
+      }
+      .gr-form-styles th:last-child gr-button,
+      .gr-form-styles td:last-child gr-button {
+        width: 100%;
+      }
+      .gr-form-styles iron-autogrow-textarea {
+        border: none;
+        height: auto;
+        min-height: 2em;
+        --iron-autogrow-textarea: {
+          border: 1px solid #d1d2d3;
+          border-radius: 2px;
+          font-size: 1em;
+          padding: .25em .15em 0 .15em;
+        }
+      }
+      .gr-form-styles gr-autocomplete {
+        border: none;
+        --gr-autocomplete: {
+          border: 1px solid #d1d2d3;
+          border-radius: 2px;
+          font-size: 1em;
+          height: 2em;
+          padding: 0 .15em;
+          width: 10em;
+        }
+      }
       @media only screen and (max-width: 40em) {
         .gr-form-styles section {
           margin-bottom: 1em;
@@ -58,6 +122,9 @@
         .gr-form-styles .value {
           display: block;
         }
+        .gr-form-styles table {
+          width: 100%;
+        }
       }
     </style>
   </template>
diff --git a/polygerrit-ui/app/test/common-test-setup.html b/polygerrit-ui/app/test/common-test-setup.html
index fb24af2d..7c894b6 100644
--- a/polygerrit-ui/app/test/common-test-setup.html
+++ b/polygerrit-ui/app/test/common-test-setup.html
@@ -15,5 +15,24 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html" />
+<link rel="import"
+    href="../bower_components/polymer-resin/standalone/polymer-resin.html" />
+<script>
+  security.polymer_resin.install({
+    allowedIdentifierPrefixes: [''],
+    reportHandler(isViolation, fmt, ...args) {
+      const log = security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER;
+      log(isViolation, fmt, ...args);
+      if (isViolation) {
+        // This will cause the test to fail if there is a data binding
+        // violation.
+        throw new Error(
+            'polymer-resin violation: ' + fmt
+            + JSON.stringify(args));
+      }
+    },
+  });
+</script>
+<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 7d29db5..f31e9be 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -36,6 +36,7 @@
     'admin/gr-admin-project/gr-admin-project_test.html',
     'admin/gr-admin-project-list/gr-admin-project-list_test.html',
     'admin/gr-admin-view/gr-admin-view_test.html',
+    'admin/gr-project-branches/gr-project-branches_test.html',
     'change-list/gr-change-list-item/gr-change-list-item_test.html',
     'change-list/gr-change-list-view/gr-change-list-view_test.html',
     'change-list/gr-change-list/gr-change-list_test.html',
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index f2a97bf..a42acc3 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -122,6 +122,9 @@
 }
 
 func injectLocalPlugins(r io.Reader) io.Reader {
+	if len(*plugins) == 0 {
+		return r
+	}
 	// Skip escape prefix
 	io.CopyN(ioutil.Discard, r, 5)
 	dec := json.NewDecoder(r)