Merge "Add a reviewer-deleted event"
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index 715d589..22f9109 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -1,7 +1,7 @@
 = init
 
 == NAME
-init - Initialize a new Gerrit server installation
+init - Initialize/Upgrade a Gerrit server installation
 
 == SYNOPSIS
 --
@@ -25,13 +25,14 @@
 into a newly created `$site_path`.
 
 If run in an existing `$site_path`, init will upgrade some resources
-as necessary.
+(e.g. DB schema, plugins) as necessary.
 
 == OPTIONS
 --batch::
-	Run in batch mode, skipping interactive prompts.  Reasonable
-	configuration defaults are chosen based on the whims of
-	the Gerrit developers.
+	Run in batch mode, skipping interactive prompts. For a fresh
+	install, reasonable configuration defaults are chosen based
+	on the whims of the Gerrit developers. On upgrades, the settings
+	in gerrit.config are respected.
 +
 	If during a schema migration unused objects (e.g. tables, columns)
 	are detected they are *not* automatically dropped, but only a list of
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt
index 6d7c6d0..2b0db70b 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -53,7 +53,9 @@
 
 The method Gerrit uses to submit a change to a project can be
 modified by any project owner through the project console, `Projects` >
-`List` > my/project.  The following methods are supported:
+`List` > my/project. In general, a submitted change is only merged if all
+its dependencies are also submitted, with exceptions documented below.
+The following submit types are supported:
 
 [[fast_forward_only]]
 * Fast Forward Only
@@ -96,10 +98,12 @@
 is also set to the submitter, while the author header retains the
 original patch set author.
 +
-Note that Gerrit ignores patch set dependencies when operating in
-cherry-pick mode. Submitters must remember to submit changes in
-the right order since inter-change dependencies will not be
-enforced for them.
+Note that Gerrit ignores dependencies between changes when using this
+submit type unless
+link:config-gerrit.html#change.submitWholeTopic[`change.submitWholeTopic`]
+is enabled and depending changes share the same topic. So generally
+submitters must remember to submit changes in the right order when using this
+submit type.
 
 [[rebase_if_necessary]]
 * Rebase If Necessary
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 7bbf2be..ae42477 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -641,6 +641,9 @@
 
 The SSH public key must be provided as raw content in the request body.
 
+Trying to add an SSH key that already exists succeeds, but no new SSH
+key is persisted.
+
 .Request
 ----
   POST /accounts/self/sshkeys HTTP/1.0
@@ -1105,11 +1108,13 @@
     "changes_per_page": 25,
     "show_site_header": true,
     "use_flash_clipboard": true,
+    "download_command": "CHECKOUT",
     "date_format": "STD",
     "time_format": "HHMM_12",
+    "diff_view": "SIDE_BY_SIDE",
     "size_bar_in_change_table": true,
     "review_category_strategy": "ABBREV",
-    "diff_view": "SIDE_BY_SIDE",
+    "mute_common_path_prefixes": true,
     "my": [
       {
         "url": "#/dashboard/self",
@@ -1159,11 +1164,13 @@
     "changes_per_page": 50,
     "show_site_header": true,
     "use_flash_clipboard": true,
+    "download_command": "CHECKOUT",
     "date_format": "STD",
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
     "review_category_strategy": "NAME",
     "diff_view": "SIDE_BY_SIDE",
+    "mute_common_path_prefixes": true,
     "my": [
       {
         "url": "#/dashboard/self",
@@ -1207,11 +1214,13 @@
     "changes_per_page": 50,
     "show_site_header": true,
     "use_flash_clipboard": true,
+    "download_command": "CHECKOUT",
     "date_format": "STD",
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
     "review_category_strategy": "NAME",
     "diff_view": "SIDE_BY_SIDE",
+    "mute_common_path_prefixes": true,
     "my": [
       {
         "url": "#/dashboard/self",
@@ -1944,7 +1953,7 @@
 Whether the site header should be shown.
 |`use_flash_clipboard`          |not set if `false`|
 Whether to use the flash clipboard widget.
-|`download_scheme`              ||
+|`download_scheme`              |optional|
 The type of download URL the user prefers to use. May be any key from
 the `schemes` map in
 link:rest-api-config.html#download-info[DownloadInfo].
@@ -1981,7 +1990,7 @@
 |`url_aliases`                  |optional|
 A map of URL path pairs, where the first URL path is an alias for the
 second URL path.
-|`email_notifications`          ||
+|`email_strategy`               ||
 The type of email strategy to use. On `ENABLED`, the user will receive emails
 from Gerrit. On `CC_ON_OWN_COMMENTS` the user will also receive emails for
 their own comments. On `DISABLED` the user will not receive any email
@@ -2039,6 +2048,12 @@
 |`url_aliases`                  |optional|
 A map of URL path pairs, where the first URL path is an alias for the
 second URL path.
+|`email_strategy`               |optional|
+The type of email strategy to use. On `ENABLED`, the user will receive emails
+from Gerrit. On `CC_ON_OWN_COMMENTS` the user will also receive emails for
+their own comments. On `DISABLED` the user will not receive any email
+notifications from Gerrit.
+Allowed values are `ENABLED`, `CC_ON_OWN_COMMENTS`, `DISABLED`.
 |============================================
 
 [[query-limit-info]]
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 ee0688b..3632502 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
@@ -22,13 +22,12 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -47,18 +46,23 @@
 public class AccountCreator {
   private final Map<String, TestAccount> accounts;
 
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-  private GroupCache groupCache;
-  private SshKeyCache sshKeyCache;
-  private AccountCache accountCache;
-  private AccountByEmailCache byEmailCache;
+  private final SchemaFactory<ReviewDb> reviewDbProvider;
+  private final VersionedAuthorizedKeys.Accessor authorizedKeys;
+  private final GroupCache groupCache;
+  private final SshKeyCache sshKeyCache;
+  private final AccountCache accountCache;
+  private final AccountByEmailCache byEmailCache;
 
   @Inject
-  AccountCreator(SchemaFactory<ReviewDb> schema, GroupCache groupCache,
-      SshKeyCache sshKeyCache, AccountCache accountCache,
+  AccountCreator(SchemaFactory<ReviewDb> schema,
+      VersionedAuthorizedKeys.Accessor authorizedKeys,
+      GroupCache groupCache,
+      SshKeyCache sshKeyCache,
+      AccountCache accountCache,
       AccountByEmailCache byEmailCache) {
     accounts = new HashMap<>();
     reviewDbProvider = schema;
+    this.authorizedKeys = authorizedKeys;
     this.groupCache = groupCache;
     this.sshKeyCache = sshKeyCache;
     this.accountCache = accountCache;
@@ -66,17 +70,14 @@
   }
 
   public synchronized TestAccount create(String username, String email,
-      String fullName, String... groups)
-      throws OrmException, UnsupportedEncodingException, JSchException {
+      String fullName, String... groups) throws Exception {
     TestAccount account = accounts.get(username);
     if (account != null) {
       return account;
     }
     try (ReviewDb db = reviewDbProvider.open()) {
       Account.Id id = new Account.Id(db.nextAccountId());
-      KeyPair sshKey = genSshKey();
-      AccountSshKey key =
-          new AccountSshKey(new AccountSshKey.Id(id, 1), publicKey(sshKey, email));
+
       AccountExternalId extUser =
           new AccountExternalId(id, new AccountExternalId.Key(
               AccountExternalId.SCHEME_USERNAME, username));
@@ -95,8 +96,6 @@
       a.setPreferredEmail(email);
       db.accounts().insert(Collections.singleton(a));
 
-      db.accountSshKeys().insert(Collections.singleton(key));
-
       if (groups != null) {
         for (String n : groups) {
           AccountGroup.NameKey k = new AccountGroup.NameKey(n);
@@ -107,7 +106,10 @@
         }
       }
 
+      KeyPair sshKey = genSshKey();
+      authorizedKeys.addKey(id, publicKey(sshKey, email));
       sshKeyCache.evict(username);
+
       accountCache.evictByUsername(username);
       byEmailCache.evict(email);
 
@@ -118,35 +120,29 @@
     }
   }
 
-  public TestAccount create(String username, String group)
-      throws OrmException, UnsupportedEncodingException, JSchException {
+  public TestAccount create(String username, String group) throws Exception {
     return create(username, null, username, group);
   }
 
-  public TestAccount create(String username)
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount create(String username) throws Exception {
     return create(username, null, username, (String[]) null);
   }
 
-  public TestAccount admin()
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount admin() throws Exception {
     return create("admin", "admin@example.com", "Administrator",
       "Administrators");
   }
 
-  public TestAccount admin2()
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount admin2() throws Exception {
     return create("admin2", "admin2@example.com", "Administrator2",
       "Administrators");
   }
 
-  public TestAccount user()
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount user() throws Exception {
     return create("user", "user@example.com", "User");
   }
 
-  public TestAccount user2()
-      throws UnsupportedEncodingException, OrmException, JSchException {
+  public TestAccount user2() throws Exception {
     return create("user2", "user2@example.com", "User2");
   }
 
@@ -169,6 +165,6 @@
       throws UnsupportedEncodingException {
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     sshKey.writePublicKey(out, comment);
-    return out.toString(US_ASCII.name());
+    return out.toString(US_ASCII.name()).trim();
   }
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
index c16eed7..14188bd 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -259,11 +259,8 @@
         UploadPack up = new UploadPack(repo);
         up.setPackConfig(transferConfig.getPackConfig());
         up.setTimeout(transferConfig.getTimeout());
-
-        if (!ctl.allRefsAreVisible()) {
-          up.setAdvertiseRefsHook(new VisibleRefFilter(
-              tagCache, changeCache, repo, ctl, dbProvider.get(), true));
-        }
+        up.setAdvertiseRefsHook(new VisibleRefFilter(
+            tagCache, changeCache, repo, ctl, dbProvider.get(), true));
         List<PreUploadHook> hooks = Lists.newArrayList(preUploadHooks);
         hooks.add(uploadValidatorsFactory.create(
             ctl.getProject(), repo, "localhost-test"));
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 59f0150..0c2e0c6 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
@@ -340,6 +340,7 @@
     // The test account should initially have exactly one ssh key
     List<SshKeyInfo> info = gApi.accounts().self().listSshKeys();
     assertThat(info).hasSize(1);
+    assertSequenceNumbers(info);
     SshKeyInfo key = info.get(0);
     String inital = AccountCreator.publicKey(admin.sshKey, admin.email);
     assertThat(key.sshPublicKey).isEqualTo(inital);
@@ -350,11 +351,35 @@
     gApi.accounts().self().addSshKey(newKey);
     info = gApi.accounts().self().listSshKeys();
     assertThat(info).hasSize(2);
+    assertSequenceNumbers(info);
 
-    // Add an existing key again
+    // Add an existing key (the request succeeds, but the key isn't added again)
     gApi.accounts().self().addSshKey(inital);
     info = gApi.accounts().self().listSshKeys();
+    assertThat(info).hasSize(2);
+    assertSequenceNumbers(info);
+
+    // Add another new key
+    String newKey2 = AccountCreator.publicKey(
+        AccountCreator.genSshKey(), admin.email);
+    gApi.accounts().self().addSshKey(newKey2);
+    info = gApi.accounts().self().listSshKeys();
     assertThat(info).hasSize(3);
+    assertSequenceNumbers(info);
+
+    // Delete second key
+    gApi.accounts().self().deleteSshKey(2);
+    info = gApi.accounts().self().listSshKeys();
+    assertThat(info).hasSize(2);
+    assertThat(info.get(0).seq).isEqualTo(1);
+    assertThat(info.get(1).seq).isEqualTo(3);
+  }
+
+  private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) {
+    int seq = 1;
+    for (SshKeyInfo key : sshKeys) {
+      assertThat(key.seq).isEqualTo(seq++);
+    }
   }
 
   private PGPPublicKey getOnlyKeyFromStore(TestKey key) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index bc206b4..5f2af6e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
-import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -47,14 +46,12 @@
 import com.google.gerrit.testutil.TestTimeUtil;
 
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.transport.PushResult;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
 import java.util.Collection;
-import java.util.List;
 import java.util.Set;
 
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
@@ -493,35 +490,21 @@
   }
 
   @Test
-  public void testPushSameCommitTwiceUsingMagicBranchBaseOption()
-      throws Exception {
-    grant(Permission.PUSH, project, "refs/heads/master");
-    PushOneCommit.Result rBase = pushTo("refs/heads/master");
-    rBase.assertOkStatus();
-
-    gApi.projects()
-        .name(project.get())
-        .branch("foo")
-        .create(new BranchInput());
+  public void testCreateNewChangeForAllNotInTarget() throws Exception {
+    ProjectConfig config = projectCache.checkedGet(project).getConfig();
+    config.getProject().setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
+    saveProjectConfig(project, config);
 
     PushOneCommit push =
         pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
-            "b.txt", "anotherContent");
-
+            "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
 
-    PushResult pr = GitUtil.pushHead(
-        testRepo, "refs/for/foo%base=" + rBase.getCommit().name(), false, false);
-    assertThat(pr.getMessages()).contains("changes: new: 1, refs: 1, done");
-
-    List<ChangeInfo> changes = query(r.getCommit().name());
-    assertThat(changes).hasSize(2);
-    ChangeInfo c1 = get(changes.get(0).id);
-    ChangeInfo c2 = get(changes.get(1).id);
-    assertThat(c1.project).isEqualTo(c2.project);
-    assertThat(c1.branch).isNotEqualTo(c2.branch);
-    assertThat(c1.changeId).isEqualTo(c2.changeId);
-    assertThat(c1.currentRevision).isEqualTo(c2.currentRevision);
+    push =
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent");
+    r = push.to("refs/for/master");
+    r.assertOkStatus();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 8228a277..40ea296 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -692,6 +692,7 @@
           PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
           psUtil.setGroups(ctx.getDb(), ctx.getUpdate(psId), ps,
               ImmutableList.<String> of());
+          ctx.bumpLastUpdatedOn(false);
           return true;
         }
       });
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
index 75872f4..55073d5 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -29,11 +28,6 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface AccountService extends RemoteJsonService {
-  @Audit
-  @SignInRequired
-  void changeDiffPreferences(DiffPreferencesInfo diffPref,
-      AsyncCallback<VoidResult> callback);
-
   @SignInRequired
   void myProjectWatch(AsyncCallback<List<AccountProjectWatchInfo>> callback);
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index a45d8d3..a6e54b2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -49,6 +49,7 @@
 
   List<SshKeyInfo> listSshKeys() throws RestApiException;
   SshKeyInfo addSshKey(String key) throws RestApiException;
+  void deleteSshKey(int seq) throws RestApiException;
 
   Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException;
   Map<String, GpgKeyInfo> putGpgKeys(List<String> add, List<String> remove)
@@ -129,6 +130,11 @@
     }
 
     @Override
+    public void deleteSshKey(int seq) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public Map<String, GpgKeyInfo> putGpgKeys(List<String> add,
         List<String> remove) throws RestApiException {
       throw new NotImplementedException();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
deleted file mode 100644
index 0c31f41..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (C) 2010 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.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.Util;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gwtjsonrpc.common.VoidResult;
-
-public class ListenableAccountDiffPreference
-    extends ListenableOldValue<DiffPreferencesInfo> {
-
-  public ListenableAccountDiffPreference() {
-    reset();
-  }
-
-  public void save(final GerritCallback<VoidResult> cb) {
-    if (Gerrit.isSignedIn()) {
-      Util.ACCOUNT_SVC.changeDiffPreferences(get(),
-          new GerritCallback<VoidResult>() {
-        @Override
-        public void onSuccess(VoidResult result) {
-          Gerrit.setDiffPreferences(get());
-          cb.onSuccess(result);
-        }
-
-        @Override
-        public void onFailure(Throwable caught) {
-          cb.onFailure(caught);
-        }
-      });
-    }
-  }
-
-  public void reset() {
-    if (Gerrit.isSignedIn() && Gerrit.getDiffPreferences() != null) {
-      set(Gerrit.getDiffPreferences());
-    } else {
-      set(DiffPreferencesInfo.defaults());
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index bf8fe01..063f22b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -256,10 +256,8 @@
           uploadValidatorsFactory.create(pc.getProject(), repo, request.getRemoteHost());
       up.setPreUploadHook(PreUploadHookChain.newChain(
           Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
-      if (!pc.allRefsAreVisible()) {
-        up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache,
-            repo, pc, db.get(), true));
-      }
+      up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache,
+          repo, pc, db.get(), true));
 
       next.doFilter(request, response);
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index 8d3947f..ce47161 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
@@ -19,17 +19,12 @@
 import com.google.gerrit.common.data.AgreementInfo;
 import com.google.gerrit.common.errors.InvalidQueryException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.account.SetDiffPreferences;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.query.QueryParseException;
@@ -41,9 +36,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.errors.ConfigInvalidException;
-
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -55,41 +47,17 @@
   private final ProjectControl.Factory projectControlFactory;
   private final AgreementInfoFactory.Factory agreementInfoFactory;
   private final Provider<ChangeQueryBuilder> queryBuilder;
-  private final SetDiffPreferences setDiff;
 
   @Inject
   AccountServiceImpl(final Provider<ReviewDb> schema,
       final Provider<IdentifiedUser> identifiedUser,
       final ProjectControl.Factory projectControlFactory,
       final AgreementInfoFactory.Factory agreementInfoFactory,
-      final Provider<ChangeQueryBuilder> queryBuilder,
-      SetDiffPreferences setDiff) {
+      final Provider<ChangeQueryBuilder> queryBuilder) {
     super(schema, identifiedUser);
     this.projectControlFactory = projectControlFactory;
     this.agreementInfoFactory = agreementInfoFactory;
     this.queryBuilder = queryBuilder;
-    this.setDiff = setDiff;
-  }
-
-  @Override
-  public void changeDiffPreferences(final DiffPreferencesInfo diffPref,
-      AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      @Override
-      public VoidResult run(ReviewDb db) throws OrmException {
-        if (!getUser().isIdentifiedUser()) {
-          throw new IllegalArgumentException("Not authenticated");
-        }
-        IdentifiedUser me = getUser().asIdentifiedUser();
-        try {
-          setDiff.apply(new AccountResource(me), diffPref);
-        } catch (AuthException | BadRequestException | ConfigInvalidException
-            | IOException e) {
-          throw new OrmException("Cannot save diff preferences", e);
-        }
-        return VoidResult.INSTANCE;
-      }
-    });
   }
 
   @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 40a07b4..6cbd40d 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -43,14 +43,17 @@
 public class InitAdminUser implements InitStep {
   private final ConsoleUI ui;
   private final InitFlags flags;
+  private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory;
   private SchemaFactory<ReviewDb> dbFactory;
 
   @Inject
   InitAdminUser(
       InitFlags flags,
-      ConsoleUI ui) {
+      ConsoleUI ui,
+      VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory) {
     this.flags = flags;
     this.ui = ui;
+    this.authorizedKeysFactory = authorizedKeysFactory;
   }
 
   @Override
@@ -110,7 +113,10 @@
           db.accountGroupMembers().insert(Collections.singleton(m));
 
           if (sshKey != null) {
-            db.accountSshKeys().insert(Collections.singleton(sshKey));
+            VersionedAuthorizedKeysOnInit authorizedKeys =
+                authorizedKeysFactory.create(id).load();
+            authorizedKeys.addKey(sshKey.getSshPublicKey());
+            authorizedKeys.save("Added SSH key for initial admin user\n");
           }
         }
       }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
index 57a1a30..b5aa625 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
@@ -42,6 +42,7 @@
     bind(Libraries.class);
     bind(LibraryDownloader.class);
     factory(Section.Factory.class);
+    factory(VersionedAuthorizedKeysOnInit.Factory.class);
 
     // Steps are executed in the order listed here.
     //
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
new file mode 100644
index 0000000..9ef6956
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
@@ -0,0 +1,198 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.account.AuthorizedKeys;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+
+public class VersionedAuthorizedKeysOnInit extends VersionedMetaData {
+  public static interface Factory {
+    VersionedAuthorizedKeysOnInit create(Account.Id accountId);
+  }
+
+  private final Account.Id accountId;
+  private final String ref;
+  private final String project;
+  private final SitePaths site;
+  private final InitFlags flags;
+
+  private List<Optional<AccountSshKey>> keys;
+  private ObjectId revision;
+
+  @Inject
+  public VersionedAuthorizedKeysOnInit(
+      AllUsersNameOnInitProvider allUsers,
+      SitePaths site,
+      InitFlags flags,
+      @Assisted Account.Id accountId) {
+
+    this.project = allUsers.get();
+    this.site = site;
+    this.flags = flags;
+    this.accountId = accountId;
+    this.ref = RefNames.refsUsers(accountId);
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  public VersionedAuthorizedKeysOnInit load()
+      throws IOException, ConfigInvalidException {
+    File path = getPath();
+    if (path != null) {
+      try (Repository repo = new FileRepository(path)) {
+        load(repo);
+      }
+    }
+    return this;
+  }
+
+  private File getPath() {
+    Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
+    if (basePath == null) {
+      throw new IllegalStateException("gerrit.basePath must be configured");
+    }
+    return FileKey.resolve(basePath.resolve(project).toFile(), FS.DETECTED);
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    revision = getRevision();
+    keys = AuthorizedKeys.parse(accountId, readUTF8(AuthorizedKeys.FILE_NAME));
+  }
+
+  public AccountSshKey addKey(String pub) {
+    checkState(keys != null, "SSH keys not loaded yet");
+    int seq = keys.isEmpty() ? 1 : keys.size() + 1;
+    AccountSshKey.Id keyId = new AccountSshKey.Id(accountId, seq);
+    AccountSshKey key =
+        new VersionedAuthorizedKeys.SimpleSshKeyCreator().create(keyId, pub);
+    keys.add(Optional.of(key));
+    return key;
+  }
+
+  public void save(String message) throws IOException {
+    save(new PersonIdent("Gerrit Initialization", "init@gerrit"), message);
+  }
+
+  private void save(PersonIdent ident, String msg) throws IOException {
+    File path = getPath();
+    if (path == null) {
+      throw new IOException(project + " does not exist.");
+    }
+
+    try (Repository repo = new FileRepository(path);
+        ObjectInserter i = repo.newObjectInserter();
+        ObjectReader r = repo.newObjectReader();
+        RevWalk rw = new RevWalk(reader)) {
+      inserter = i;
+      reader = r;
+
+      RevTree srcTree = revision != null ? rw.parseTree(revision) : null;
+      newTree = readTree(srcTree);
+
+      CommitBuilder commit = new CommitBuilder();
+      commit.setAuthor(ident);
+      commit.setCommitter(ident);
+      commit.setMessage(msg);
+
+      onSave(commit);
+      ObjectId res = newTree.writeTree(inserter);
+      if (res.equals(srcTree)) {
+        return;
+      }
+
+      commit.setTreeId(res);
+      if (revision != null) {
+        commit.addParentId(revision);
+      }
+      ObjectId newRevision = inserter.insert(commit);
+      updateRef(repo, ident, newRevision, "commit: " + msg);
+      revision = newRevision;
+    } finally {
+      inserter = null;
+      reader = null;
+    }
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException {
+    if (Strings.isNullOrEmpty(commit.getMessage())) {
+      commit.setMessage("Updated SSH keys\n");
+    }
+
+    saveUTF8(AuthorizedKeys.FILE_NAME, AuthorizedKeys.serialize(keys));
+    return true;
+  }
+
+  private void updateRef(Repository repo, PersonIdent ident,
+      ObjectId newRevision, String refLogMsg) throws IOException {
+    RefUpdate ru = repo.updateRef(getRefName());
+    ru.setRefLogIdent(ident);
+    ru.setNewObjectId(newRevision);
+    ru.setExpectedOldObjectId(revision);
+    ru.setRefLogMessage(refLogMsg, false);
+    RefUpdate.Result r = ru.update();
+    switch(r) {
+      case FAST_FORWARD:
+      case NEW:
+      case NO_CHANGE:
+        break;
+      case FORCED:
+      case IO_FAILURE:
+      case LOCK_FAILURE:
+      case NOT_ATTEMPTED:
+      case REJECTED:
+      case REJECTED_CURRENT_BRANCH:
+      case RENAMED:
+      default:
+        throw new IOException("Failed to update " + getRefName() + " of "
+            + project + ": " + r.name());
+    }
+  }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java
index 8c4ac82..7b1e874 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java
@@ -17,6 +17,8 @@
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
 
+import java.util.Objects;
+
 /** An SSH key approved for use by an {@link Account}. */
 public final class AccountSshKey {
   public static class Id extends IntKey<Account.Id> {
@@ -129,4 +131,20 @@
   public void setInvalid() {
     valid = false;
   }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o instanceof AccountSshKey) {
+      AccountSshKey other = (AccountSshKey) o;
+      return Objects.equals(id, other.id)
+          && Objects.equals(sshPublicKey, other.sshPublicKey)
+          && Objects.equals(valid, other.valid);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(id, sshPublicKey, valid);
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
deleted file mode 100644
index 6f71ba4..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountSshKeyAccess extends
-    Access<AccountSshKey, AccountSshKey.Id> {
-  @Override
-  @PrimaryKey("id")
-  AccountSshKey get(AccountSshKey.Id id) throws OrmException;
-
-  @Query("WHERE id.accountId = ?")
-  ResultSet<AccountSshKey> byAccount(Account.Id id) throws OrmException;
-
-  @Query("WHERE id.accountId = ? ORDER BY id.seq DESC LIMIT 1")
-  ResultSet<AccountSshKey> byAccountLast(Account.Id id) throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 7b28f1f..9f62dc2 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -53,8 +53,7 @@
   @Relation(id = 7)
   AccountExternalIdAccess accountExternalIds();
 
-  @Relation(id = 8)
-  AccountSshKeyAccess accountSshKeys();
+  // Deleted @Relation(id = 8)
 
   @Relation(id = 10)
   AccountGroupAccess accountGroups();
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
index 05793b5..aa37974 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -89,11 +89,6 @@
   }
 
   @Override
-  public AccountSshKeyAccess accountSshKeys() {
-    return delegate.accountSshKeys();
-  }
-
-  @Override
   public AccountGroupAccess accountGroups() {
     return delegate.accountGroups();
   }
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index 9242fe0..2110295 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -48,11 +48,6 @@
 
 
 -- *********************************************************************
--- AccountSshKeyAccess
---    @PrimaryKey covers: byAccount, valid
-
-
--- *********************************************************************
 -- ApprovalCategoryAccess
 --    too small to bother indexing
 
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
index 6934eb2..334b6c4 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
@@ -55,11 +55,6 @@
 #
 
 -- *********************************************************************
--- AccountSshKeyAccess
---    @PrimaryKey covers: byAccount, valid
-
-
--- *********************************************************************
 -- ApprovalCategoryAccess
 --    too small to bother indexing
 
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index f146785..d723667 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -96,11 +96,6 @@
 
 
 -- *********************************************************************
--- AccountSshKeyAccess
---    @PrimaryKey covers: byAccount, valid
-
-
--- *********************************************************************
 -- ApprovalCategoryAccess
 --    too small to bother indexing
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
index e91998e..216672c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
@@ -16,7 +16,6 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.collect.Iterables;
 import com.google.common.io.ByteSource;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
@@ -27,24 +26,22 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AddSshKey.Input;
 import com.google.gerrit.server.mail.AddKeySender;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Collections;
 
 @Singleton
 public class AddSshKey implements RestModifyView<AccountResource, Input> {
@@ -55,22 +52,25 @@
   }
 
   private final Provider<CurrentUser> self;
-  private final Provider<ReviewDb> dbProvider;
+  private final VersionedAuthorizedKeys.Accessor authorizedKeys;
   private final SshKeyCache sshKeyCache;
   private final AddKeySender.Factory addKeyFactory;
 
   @Inject
-  AddSshKey(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider,
-      SshKeyCache sshKeyCache, AddKeySender.Factory addKeyFactory) {
+  AddSshKey(Provider<CurrentUser> self,
+      VersionedAuthorizedKeys.Accessor authorizedKeys,
+      SshKeyCache sshKeyCache,
+      AddKeySender.Factory addKeyFactory) {
     this.self = self;
-    this.dbProvider = dbProvider;
+    this.authorizedKeys = authorizedKeys;
     this.sshKeyCache = sshKeyCache;
     this.addKeyFactory = addKeyFactory;
   }
 
   @Override
   public Response<SshKeyInfo> apply(AccountResource rsrc, Input input)
-      throws AuthException, BadRequestException, OrmException, IOException {
+      throws AuthException, BadRequestException, OrmException, IOException,
+      ConfigInvalidException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("not allowed to add SSH keys");
@@ -79,7 +79,8 @@
   }
 
   public Response<SshKeyInfo> apply(IdentifiedUser user, Input input)
-      throws BadRequestException, OrmException, IOException {
+      throws BadRequestException, IOException,
+      ConfigInvalidException {
     if (input == null) {
       input = new Input();
     }
@@ -87,11 +88,6 @@
       throw new BadRequestException("SSH public key missing");
     }
 
-    ResultSet<AccountSshKey> byAccountLast =
-        dbProvider.get().accountSshKeys().byAccountLast(user.getAccountId());
-    AccountSshKey last = Iterables.getOnlyElement(byAccountLast, null);
-    int max = last == null ? 0 : last.getKey().get();
-
     final RawInput rawKey = input.raw;
     String sshPublicKey = new ByteSource() {
       @Override
@@ -102,15 +98,15 @@
 
     try {
       AccountSshKey sshKey =
-          sshKeyCache.create(new AccountSshKey.Id(
-              user.getAccountId(), max + 1), sshPublicKey);
-      dbProvider.get().accountSshKeys().insert(Collections.singleton(sshKey));
+          authorizedKeys.addKey(user.getAccountId(), sshPublicKey);
+
       try {
         addKeyFactory.create(user, sshKey).send();
       } catch (EmailException e) {
         log.error("Cannot send SSH key added message to "
             + user.getAccount().getPreferredEmail(), e);
       }
+
       sshKeyCache.evict(user.getUserName());
       return Response.<SshKeyInfo>created(GetSshKeys.newSshKeyInfo(sshKey));
     } catch (InvalidSshKeyException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java
new file mode 100644
index 0000000..0e8c051
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class AuthorizedKeys {
+  public static final String FILE_NAME = "authorized_keys";
+
+  @VisibleForTesting
+  public static final String INVALID_KEY_COMMENT_PREFIX = "# INVALID ";
+
+  @VisibleForTesting
+  public static final String DELETED_KEY_COMMENT = "# DELETED";
+
+  public static List<Optional<AccountSshKey>> parse(
+      Account.Id accountId, String s) {
+    List<Optional<AccountSshKey>> keys = new ArrayList<>();
+    int seq = 1;
+    for (String line : s.split("\\r?\\n")) {
+      line = line.trim();
+      if (line.isEmpty()) {
+        continue;
+      } else if (line.startsWith(INVALID_KEY_COMMENT_PREFIX)) {
+        String pub = line.substring(INVALID_KEY_COMMENT_PREFIX.length());
+        AccountSshKey key =
+            new AccountSshKey(new AccountSshKey.Id(accountId, seq++), pub);
+        key.setInvalid();
+        keys.add(Optional.of(key));
+      } else if (line.startsWith(DELETED_KEY_COMMENT)) {
+        keys.add(Optional.<AccountSshKey> absent());
+        seq++;
+      } else if (line.startsWith("#")) {
+        continue;
+      } else {
+        AccountSshKey key =
+            new AccountSshKey(new AccountSshKey.Id(accountId, seq++), line);
+        keys.add(Optional.of(key));
+      }
+    }
+    return keys;
+  }
+
+  public static String serialize(Collection<Optional<AccountSshKey>> keys) {
+    StringBuilder b = new StringBuilder();
+    for (Optional<AccountSshKey> key : keys) {
+      if (key.isPresent()) {
+        if (!key.get().isValid()) {
+          b.append(INVALID_KEY_COMMENT_PREFIX);
+        }
+        b.append(key.get().getSshPublicKey().trim());
+      } else {
+        b.append(DELETED_KEY_COMMENT);
+      }
+      b.append("\n");
+    }
+    return b.toString();
+  }
+}
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 190db5f..bcdc071 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
@@ -34,7 +34,6 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.CreateAccount.Input;
@@ -48,7 +47,9 @@
 import com.google.inject.assistedinject.Assisted;
 
 import org.apache.commons.validator.routines.EmailValidator;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
+import java.io.IOException;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
@@ -73,37 +74,45 @@
   private final ReviewDb db;
   private final Provider<IdentifiedUser> currentUser;
   private final GroupsCollection groupsCollection;
+  private final VersionedAuthorizedKeys.Accessor authorizedKeys;
   private final SshKeyCache sshKeyCache;
   private final AccountCache accountCache;
   private final AccountByEmailCache byEmailCache;
   private final AccountLoader.Factory infoLoader;
   private final DynamicSet<AccountExternalIdCreator> externalIdCreators;
-  private final String username;
   private final AuditService auditService;
+  private final String username;
 
   @Inject
-  CreateAccount(ReviewDb db, Provider<IdentifiedUser> currentUser,
-      GroupsCollection groupsCollection, SshKeyCache sshKeyCache,
-      AccountCache accountCache, AccountByEmailCache byEmailCache,
+  CreateAccount(ReviewDb db,
+      Provider<IdentifiedUser> currentUser,
+      GroupsCollection groupsCollection,
+      VersionedAuthorizedKeys.Accessor authorizedKeys,
+      SshKeyCache sshKeyCache,
+      AccountCache accountCache,
+      AccountByEmailCache byEmailCache,
       AccountLoader.Factory infoLoader,
       DynamicSet<AccountExternalIdCreator> externalIdCreators,
-      @Assisted String username, AuditService auditService) {
+      AuditService auditService,
+      @Assisted String username) {
     this.db = db;
     this.currentUser = currentUser;
     this.groupsCollection = groupsCollection;
+    this.authorizedKeys = authorizedKeys;
     this.sshKeyCache = sshKeyCache;
     this.accountCache = accountCache;
     this.byEmailCache = byEmailCache;
     this.infoLoader = infoLoader;
     this.externalIdCreators = externalIdCreators;
-    this.username = username;
     this.auditService = auditService;
+    this.username = username;
   }
 
   @Override
   public Response<AccountInfo> apply(TopLevelResource rsrc, Input input)
       throws BadRequestException, ResourceConflictException,
-      UnprocessableEntityException, OrmException {
+      UnprocessableEntityException, OrmException, IOException,
+      ConfigInvalidException {
     if (input == null) {
       input = new Input();
     }
@@ -119,7 +128,6 @@
     Set<AccountGroup.Id> groups = parseGroups(input.groups);
 
     Account.Id id = new Account.Id(db.nextAccountId());
-    AccountSshKey key = createSshKey(id, input.sshKey);
 
     AccountExternalId extUser =
         new AccountExternalId(id, new AccountExternalId.Key(
@@ -178,10 +186,6 @@
     a.setPreferredEmail(input.email);
     db.accounts().insert(Collections.singleton(a));
 
-    if (key != null) {
-      db.accountSshKeys().insert(Collections.singleton(key));
-    }
-
     for (AccountGroup.Id groupId : groups) {
       AccountGroupMember m =
           new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
@@ -190,7 +194,15 @@
       db.accountGroupMembers().insert(Collections.singleton(m));
     }
 
-    sshKeyCache.evict(username);
+    if (input.sshKey != null) {
+      try {
+        authorizedKeys.addKey(id, input.sshKey);
+        sshKeyCache.evict(username);
+      } catch (InvalidSshKeyException e) {
+        throw new BadRequestException(e.getMessage());
+      }
+    }
+
     accountCache.evictByUsername(username);
     byEmailCache.evict(input.email);
 
@@ -212,18 +224,6 @@
     return groupIds;
   }
 
-  private AccountSshKey createSshKey(Account.Id id, String sshKey)
-      throws BadRequestException {
-    if (sshKey == null) {
-      return null;
-    }
-    try {
-      return sshKeyCache.create(new AccountSshKey.Id(id, 1), sshKey.trim());
-    } catch (InvalidSshKeyException e) {
-      throw new BadRequestException(e.getMessage());
-    }
-  }
-
   private AccountExternalId.Key getEmailKey(String email) {
     return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
index 9066858..9212002 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.DeleteSshKey.Input;
 import com.google.gerrit.server.ssh.SshKeyCache;
@@ -26,7 +25,10 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import java.util.Collections;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
 
 @Singleton
 public class DeleteSshKey implements
@@ -35,28 +37,31 @@
   }
 
   private final Provider<CurrentUser> self;
-  private final Provider<ReviewDb> dbProvider;
+  private final VersionedAuthorizedKeys.Accessor authorizedKeys;
   private final SshKeyCache sshKeyCache;
 
   @Inject
-  DeleteSshKey(Provider<ReviewDb> dbProvider,
-      Provider<CurrentUser> self,
+  DeleteSshKey(Provider<CurrentUser> self,
+      VersionedAuthorizedKeys.Accessor authorizedKeys,
       SshKeyCache sshKeyCache) {
     this.self = self;
-    this.dbProvider = dbProvider;
+    this.authorizedKeys = authorizedKeys;
     this.sshKeyCache = sshKeyCache;
   }
 
   @Override
   public Response<?> apply(AccountResource.SshKey rsrc, Input input)
-      throws AuthException, OrmException {
+      throws AuthException, OrmException, RepositoryNotFoundException,
+      IOException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("not allowed to delete SSH keys");
     }
-    dbProvider.get().accountSshKeys()
-        .deleteKeys(Collections.singleton(rsrc.getSshKey().getKey()));
+
+    authorizedKeys.deleteKey(rsrc.getUser().getAccountId(),
+        rsrc.getSshKey().getKey().get());
     sshKeyCache.evict(rsrc.getUser().getUserName());
+
     return Response.none();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
index e1668f2..bf1a3af 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.base.Function;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.common.SshKeyInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gwtorm.server.OrmException;
@@ -28,23 +28,29 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
 import java.util.List;
 
 @Singleton
 public class GetSshKeys implements RestReadView<AccountResource> {
 
   private final Provider<CurrentUser> self;
-  private final Provider<ReviewDb> dbProvider;
+  private final VersionedAuthorizedKeys.Accessor authorizedKeys;
 
   @Inject
-  GetSshKeys(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider) {
+  GetSshKeys(Provider<CurrentUser> self,
+      VersionedAuthorizedKeys.Accessor authorizedKeys) {
     this.self = self;
-    this.dbProvider = dbProvider;
+    this.authorizedKeys = authorizedKeys;
   }
 
   @Override
-  public List<SshKeyInfo> apply(AccountResource rsrc) throws AuthException,
-      OrmException {
+  public List<SshKeyInfo> apply(AccountResource rsrc)
+      throws AuthException, OrmException, RepositoryNotFoundException,
+      IOException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to get SSH keys");
@@ -52,13 +58,15 @@
     return apply(rsrc.getUser());
   }
 
-  public List<SshKeyInfo> apply(IdentifiedUser user) throws OrmException {
-    List<SshKeyInfo> sshKeys = Lists.newArrayList();
-    for (AccountSshKey sshKey : dbProvider.get().accountSshKeys()
-        .byAccount(user.getAccountId()).toList()) {
-      sshKeys.add(newSshKeyInfo(sshKey));
-    }
-    return sshKeys;
+  public List<SshKeyInfo> apply(IdentifiedUser user)
+      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+    return Lists.transform(authorizedKeys.getKeys(user.getAccountId()),
+        new Function<AccountSshKey, SshKeyInfo>() {
+          @Override
+          public SshKeyInfo apply(AccountSshKey key) {
+            return newSshKeyInfo(key);
+          }
+        });
   }
 
   public static SshKeyInfo newSshKeyInfo(AccountSshKey sshKey) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index b70cabd..eb32e5a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -140,7 +140,7 @@
     }
   }
 
-  public static void storeUrlAliases(VersionedAccountPreferences prefs,
+  private static void storeUrlAliases(VersionedAccountPreferences prefs,
       Map<String, String> urlAliases) {
     if (urlAliases != null) {
       Config cfg = prefs.getConfig();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
index b35c03e..44a3192 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gwtorm.server.OrmException;
@@ -28,22 +27,26 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
+
 @Singleton
 public class SshKeys implements
     ChildCollection<AccountResource, AccountResource.SshKey> {
   private final DynamicMap<RestView<AccountResource.SshKey>> views;
   private final GetSshKeys list;
   private final Provider<CurrentUser> self;
-  private final Provider<ReviewDb> dbProvider;
+  private final VersionedAuthorizedKeys.Accessor authorizedKeys;
 
   @Inject
   SshKeys(DynamicMap<RestView<AccountResource.SshKey>> views,
       GetSshKeys list, Provider<CurrentUser> self,
-      Provider<ReviewDb> dbProvider) {
+      VersionedAuthorizedKeys.Accessor authorizedKeys) {
     this.views = views;
     this.list = list;
     this.self = self;
-    this.dbProvider = dbProvider;
+    this.authorizedKeys = authorizedKeys;
   }
 
   @Override
@@ -53,7 +56,8 @@
 
   @Override
   public AccountResource.SshKey parse(AccountResource rsrc, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, OrmException, IOException,
+      ConfigInvalidException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canModifyAccount()) {
       throw new ResourceNotFoundException();
@@ -62,12 +66,10 @@
   }
 
   public AccountResource.SshKey parse(IdentifiedUser user, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, IOException, ConfigInvalidException {
     try {
       int seq = Integer.parseInt(id.get(), 10);
-      AccountSshKey sshKey =
-          dbProvider.get().accountSshKeys()
-              .get(new AccountSshKey.Id(user.getAccountId(), seq));
+      AccountSshKey sshKey = authorizedKeys.getKey(user.getAccountId(), seq);
       if (sshKey == null) {
         throw new ResourceNotFoundException(id);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
new file mode 100644
index 0000000..4662f87
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -0,0 +1,327 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.client.AccountSshKey.Id;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.gerrit.server.ssh.SshKeyCreator;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 'authorized_keys' file in the refs/users/CD/ABCD branches of the All-Users
+ * repository.
+ *
+ * The `authorized_keys' files stores the public SSH keys of the user. The file
+ * format matches the standard SSH file format, which means that each key is
+ * stored on a separate line (see
+ * https://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#.7E.2F.ssh.2Fauthorized_keys).
+ *
+ * The order of the keys in the file determines the sequence numbers of the
+ * keys. The first line corresponds to sequence number 1.
+ *
+ * Invalid keys are marked with the prefix <code># INVALID</code>.
+ *
+ * To keep the sequence numbers intact when a key is deleted, a
+ * <code># DELETED</code> line is inserted at the position where the key was
+ * deleted.
+ *
+ * Other comment lines are ignored on read, and are not written back when the
+ * file is modified.
+ */
+public class VersionedAuthorizedKeys extends VersionedMetaData
+    implements AutoCloseable {
+  @Singleton
+  public static class Accessor {
+    private final GitRepositoryManager repoManager;
+    private final AllUsersName allUsersName;
+    private final VersionedAuthorizedKeys.Factory authorizedKeysFactory;
+    private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
+    private final IdentifiedUser.GenericFactory userFactory;
+
+    @Inject
+    Accessor(
+        GitRepositoryManager repoManager,
+        AllUsersName allUsersName,
+        VersionedAuthorizedKeys.Factory authorizedKeysFactory,
+        Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+        IdentifiedUser.GenericFactory userFactory) {
+      this.repoManager = repoManager;
+      this.allUsersName = allUsersName;
+      this.authorizedKeysFactory = authorizedKeysFactory;
+      this.metaDataUpdateFactory = metaDataUpdateFactory;
+      this.userFactory = userFactory;
+    }
+
+    public List<AccountSshKey> getKeys(Account.Id accountId)
+        throws IOException, ConfigInvalidException {
+      return read(accountId).getKeys();
+    }
+
+    public AccountSshKey getKey(Account.Id accountId, int seq)
+        throws IOException, ConfigInvalidException {
+      return read(accountId).getKey(seq);
+    }
+
+    public AccountSshKey addKey(Account.Id accountId, String pub)
+        throws IOException, ConfigInvalidException, InvalidSshKeyException {
+      try (VersionedAuthorizedKeys authorizedKeys = open(accountId)) {
+        AccountSshKey key = authorizedKeys.addKey(pub);
+        commit(authorizedKeys);
+        return key;
+      }
+    }
+
+    public void deleteKey(Account.Id accountId, int seq)
+        throws IOException, ConfigInvalidException {
+      try (VersionedAuthorizedKeys authorizedKeys = open(accountId)) {
+        if (authorizedKeys.deleteKey(seq)) {
+          commit(authorizedKeys);
+        }
+      }
+    }
+
+    public void markKeyInvalid(Account.Id accountId, int seq)
+        throws IOException, ConfigInvalidException {
+      try (VersionedAuthorizedKeys authorizedKeys = open(accountId)) {
+        if (authorizedKeys.markKeyInvalid(seq)) {
+          commit(authorizedKeys);
+        }
+      }
+    }
+
+    private VersionedAuthorizedKeys read(Account.Id accountId)
+        throws IOException, ConfigInvalidException {
+      try (Repository git = repoManager.openRepository(allUsersName)) {
+        VersionedAuthorizedKeys authorizedKeys =
+            authorizedKeysFactory.create(accountId);
+        authorizedKeys.load(git);
+        return authorizedKeys;
+      }
+    }
+
+    private VersionedAuthorizedKeys open(Account.Id accountId)
+        throws IOException, ConfigInvalidException {
+      Repository git = repoManager.openRepository(allUsersName);
+      VersionedAuthorizedKeys authorizedKeys =
+          authorizedKeysFactory.create(accountId);
+      authorizedKeys.load(git);
+      return authorizedKeys;
+    }
+
+    private void commit(VersionedAuthorizedKeys authorizedKeys)
+        throws IOException {
+      try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName,
+          userFactory.create(authorizedKeys.accountId))) {
+        authorizedKeys.commit(md);
+      }
+    }
+  }
+
+  public static class SimpleSshKeyCreator implements SshKeyCreator {
+    @Override
+    public AccountSshKey create(Id id, String encoded) {
+      return new AccountSshKey(id, encoded);
+    }
+  }
+
+  public static interface Factory {
+    VersionedAuthorizedKeys create(Account.Id accountId);
+  }
+
+  private final SshKeyCreator sshKeyCreator;
+  private final Account.Id accountId;
+  private final String ref;
+  private Repository git;
+  private List<Optional<AccountSshKey>> keys;
+
+  @Inject
+  public VersionedAuthorizedKeys(
+      SshKeyCreator sshKeyCreator,
+      @Assisted Account.Id accountId) {
+    this.sshKeyCreator = sshKeyCreator;
+    this.accountId = accountId;
+    this.ref = RefNames.refsUsers(accountId);
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  @Override
+  public void load(Repository git) throws IOException, ConfigInvalidException {
+    checkState(this.git == null);
+    this.git = git;
+    super.load(git);
+  }
+
+  @Override
+  protected void onLoad() throws IOException {
+    keys = AuthorizedKeys.parse(accountId, readUTF8(AuthorizedKeys.FILE_NAME));
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException {
+    if (Strings.isNullOrEmpty(commit.getMessage())) {
+      commit.setMessage("Updated SSH keys\n");
+    }
+
+    saveUTF8(AuthorizedKeys.FILE_NAME, AuthorizedKeys.serialize(keys));
+    return true;
+  }
+
+  /** Returns all SSH keys. */
+  private List<AccountSshKey> getKeys() {
+    checkLoaded();
+    return Lists.newArrayList(Optional.presentInstances(keys));
+  }
+
+  /**
+   * Returns the SSH key with the given sequence number.
+   *
+   * @param seq sequence number
+   * @return the SSH key, <code>null</code> if there is no SSH key with this
+   *         sequence number, or if the SSH key with this sequence number has
+   *         been deleted
+   */
+  private AccountSshKey getKey(int seq) {
+    checkLoaded();
+    Optional<AccountSshKey> key = keys.get(seq - 1);
+    return key.orNull();
+  }
+
+  /**
+   * Adds a new public SSH key.
+   *
+   * If the specified public key exists already, the existing key is returned.
+   *
+   * @param pub the public SSH key to be added
+   * @return the new SSH key
+   * @throws InvalidSshKeyException
+   */
+  private AccountSshKey addKey(String pub) throws InvalidSshKeyException {
+    checkLoaded();
+
+    for (Optional<AccountSshKey> key : keys) {
+      if (key.isPresent()
+          && key.get().getSshPublicKey().trim().equals(pub.trim())) {
+        return key.get();
+      }
+    }
+
+    int seq = keys.size() + 1;
+    AccountSshKey.Id keyId = new AccountSshKey.Id(accountId, seq);
+    AccountSshKey key = sshKeyCreator.create(keyId, pub);
+    keys.add(Optional.of(key));
+    return key;
+  }
+
+  /**
+   * Deletes the SSH key with the given sequence number.
+   *
+   * @param seq the sequence number
+   * @return <code>true</code> if a key with this sequence number was found and
+   *         deleted, <code>false</code> if no key with the given sequence
+   *         number exists
+   */
+  private boolean deleteKey(int seq) {
+    checkLoaded();
+    if (seq <= keys.size() && keys.get(seq - 1).isPresent()) {
+      keys.set(seq - 1, Optional.<AccountSshKey> absent());
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Marks the SSH key with the given sequence number as invalid.
+   *
+   * @param seq the sequence number
+   * @return <code>true</code> if a key with this sequence number was found and
+   *         marked as invalid, <code>false</code> if no key with the given
+   *         sequence number exists or if the key was already marked as invalid
+   */
+  private boolean markKeyInvalid(int seq) {
+    checkLoaded();
+    AccountSshKey key = getKey(seq);
+    if (key != null && key.isValid()) {
+      key.setInvalid();
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Sets new SSH keys.
+   *
+   * The existing SSH keys are overwritten.
+   *
+   * @param newKeys the new public SSH keys
+   */
+  public void setKeys(Collection<AccountSshKey> newKeys) {
+    Ordering<AccountSshKey> o =
+        Ordering.natural().onResultOf(new Function<AccountSshKey, Integer>() {
+          @Override
+          public Integer apply(AccountSshKey sshKey) {
+            return sshKey.getKey().get();
+          }
+        });
+    keys = new ArrayList<>(Collections.nCopies(o.max(newKeys).getKey().get(),
+        Optional.<AccountSshKey> absent()));
+    for (AccountSshKey key : newKeys) {
+      keys.set(key.getKey().get() - 1, Optional.of(key));
+    }
+  }
+
+  @Override
+  public void close() {
+    if (git != null) {
+      git.close();
+    }
+  }
+
+  private void checkLoaded() {
+    checkNotNull(keys, "SSH keys not loaded yet");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 6c8e599..709020f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AddSshKey;
 import com.google.gerrit.server.account.CreateEmail;
+import com.google.gerrit.server.account.DeleteSshKey;
 import com.google.gerrit.server.account.GetAvatar;
 import com.google.gerrit.server.account.GetDiffPreferences;
 import com.google.gerrit.server.account.GetEditPreferences;
@@ -41,6 +42,7 @@
 import com.google.gerrit.server.account.SetDiffPreferences;
 import com.google.gerrit.server.account.SetEditPreferences;
 import com.google.gerrit.server.account.SetPreferences;
+import com.google.gerrit.server.account.SshKeys;
 import com.google.gerrit.server.account.StarredChanges;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
@@ -75,6 +77,8 @@
   private final GpgApiAdapter gpgApiAdapter;
   private final GetSshKeys getSshKeys;
   private final AddSshKey addSshKey;
+  private final DeleteSshKey deleteSshKey;
+  private final SshKeys sshKeys;
 
   @Inject
   AccountApiImpl(AccountLoader.Factory ailf,
@@ -92,6 +96,8 @@
       GpgApiAdapter gpgApiAdapter,
       GetSshKeys getSshKeys,
       AddSshKey addSshKey,
+      DeleteSshKey deleteSshKey,
+      SshKeys sshKeys,
       @Assisted AccountResource account) {
     this.account = account;
     this.accountLoaderFactory = ailf;
@@ -108,6 +114,8 @@
     this.createEmailFactory = createEmailFactory;
     this.getSshKeys = getSshKeys;
     this.addSshKey = addSshKey;
+    this.deleteSshKey = deleteSshKey;
+    this.sshKeys = sshKeys;
     this.gpgApiAdapter = gpgApiAdapter;
   }
 
@@ -225,7 +233,7 @@
   public List<SshKeyInfo> listSshKeys() throws RestApiException {
     try {
       return getSshKeys.apply(account);
-    } catch (OrmException e) {
+    } catch (OrmException | IOException | ConfigInvalidException e) {
       throw new RestApiException("Cannot list SSH keys", e);
     }
   }
@@ -236,12 +244,23 @@
     in.raw = RawInputUtil.create(key);
     try {
       return addSshKey.apply(account, in).value();
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | IOException | ConfigInvalidException e) {
       throw new RestApiException("Cannot add SSH key", e);
     }
   }
 
   @Override
+  public void deleteSshKey(int seq) throws RestApiException {
+    try {
+      AccountResource.SshKey sshKeyRes =
+          sshKeys.parse(account, IdString.fromDecoded(Integer.toString(seq)));
+      deleteSshKey.apply(sshKeyRes, null);
+    } catch (OrmException | IOException | ConfigInvalidException e) {
+      throw new RestApiException("Cannot delete SSH key", e);
+    }
+  }
+
+  @Override
   public Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException {
     try {
       return gpgApiAdapter.listGpgKeys(account);
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 d8fbd3b..288dd83 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
@@ -132,7 +132,6 @@
       patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
       change.setStatus(Change.Status.ABANDONED);
       change.setLastUpdatedOn(ctx.getWhen());
-      ctx.saveChange();
 
       update.setStatus(change.getStatus());
       message = newMessage(ctx);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 6af3e79..44e55c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -332,7 +332,6 @@
     }
     patchSet = psUtil.insert(ctx.getDb(), ctx.getRevWalk(), update, psId,
         commit, draft, newGroups, null);
-    ctx.saveChange();
 
     /* TODO: fixStatus is used here because the tests
      * (byStatusClosed() in AbstractQueryChangesTest)
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 513a34f..cefbf8a 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
@@ -286,7 +286,6 @@
       changeMessage.setMessage(sb.toString());
 
       cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), changeMessage);
-      ctx.saveChange(); // Bump lastUpdatedOn to match message.
       return true;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
index b991303..bfbc828 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
@@ -127,6 +127,7 @@
           comment, patchListCache, ctx.getChange(), ps);
       plcUtil.putComments(
           ctx.getDb(), ctx.getUpdate(psId), Collections.singleton(comment));
+      ctx.bumpLastUpdatedOn(false);
       return true;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
index 2edff4b..c9f5aa3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -160,7 +160,6 @@
       if (c.currentPatchSetId().equals(psId)) {
         c.setCurrentPatchSet(previousPatchSetInfo(ctx));
       }
-      ctx.saveChange();
     }
 
     private boolean deletedOnlyPatchSet() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
index 0ded3a2..8f597ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -168,7 +168,6 @@
       if (psa == null) {
         throw new ResourceNotFoundException();
       }
-      ctx.saveChange();
       ctx.getDb().patchSetApprovals().update(Collections.singleton(psa));
 
       if (msg.length() > 0) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
index db5cc47..2139ec4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
@@ -186,7 +186,6 @@
       cmsg.setMessage(msgBuf.toString());
       cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
 
-      ctx.saveChange();
       return true;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index a70716a..3defdd7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -246,7 +246,6 @@
       change.setStatus(Change.Status.NEW);
     }
     change.setCurrentPatchSet(patchSetInfo);
-    ctx.saveChange();
     if (copyApprovals) {
       approvalCopier.copy(db, ctl, patchSet);
     }
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 961b0bf..dc84e49 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
@@ -370,9 +370,6 @@
       dirty |= insertComments(ctx);
       dirty |= updateLabels(ctx);
       dirty |= insertMessage(ctx);
-      if (dirty) {
-        ctx.saveChange();
-      }
       return dirty;
     }
 
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 d6e80f4..e3a8572 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
@@ -275,10 +275,12 @@
               rsrc.getChange(),
               reviewers.keySet());
 
-      if (!added.isEmpty()) {
-        patchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
+      if (added.isEmpty()) {
+        return false;
       }
-      return !added.isEmpty();
+      patchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
+      ctx.bumpLastUpdatedOn(false);
+      return true;
     }
 
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index 74cbd5f..ff5185e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -192,7 +192,6 @@
       if (wasDraftChange) {
         change.setStatus(Change.Status.NEW);
         update.setStatus(change.getStatus());
-        ctx.saveChange();
       }
     }
 
@@ -202,10 +201,6 @@
         throw new ResourceConflictException("Patch set is not a draft");
       }
       psUtil.publish(ctx.getDb(), ctx.getUpdate(psId), patchSet);
-      // Force ETag invalidation if not done already
-      if (!wasDraftChange) {
-        ctx.saveChange();
-      }
     }
 
     private void addReviewers(ChangeContext ctx)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
index 082798a..1d756f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -155,6 +155,7 @@
         plcUtil.putComments(ctx.getDb(), update,
             Collections.singleton(update(comment, in, ctx.getWhen())));
       }
+      ctx.bumpLastUpdatedOn(false);
       return true;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
index 61718ae..f0db81d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -114,7 +114,6 @@
       }
       change.setTopic(Strings.emptyToNull(newTopicName));
       update.setTopic(change.getTopic());
-      ctx.saveChange();
 
       ChangeMessage cmsg = new ChangeMessage(
           new ChangeMessage.Key(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index 8696ed6..b03194e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -117,7 +117,6 @@
       patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
       change.setStatus(Status.NEW);
       change.setLastUpdatedOn(ctx.getWhen());
-      ctx.saveChange();
       update.setStatus(change.getStatus());
 
       message = newMessage(ctx);
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 f1ff452..b5b3b6a 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
@@ -70,6 +70,7 @@
 import com.google.gerrit.server.account.GroupDetailFactory;
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
 import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.api.accounts.AccountExternalIdCreator;
 import com.google.gerrit.server.auth.AuthBackend;
 import com.google.gerrit.server.auth.UniversalAuthBackend;
@@ -332,6 +333,7 @@
     factory(SubmoduleSectionParser.Factory.class);
     factory(ReplaceOp.Factory.class);
     factory(GitModules.Factory.class);
+    factory(VersionedAuthorizedKeys.Factory.class);
 
     bind(AccountManager.class);
     factory(ChangeUserName.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
index 4c5ab65..bbf2299 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
@@ -60,6 +60,8 @@
         || i.legacycidInChangeTable != null
         || i.muteCommonPathPrefixes != null
         || i.reviewCategoryStrategy != null
+        || i.signedOffBy != null
+        || i.urlAliases != null
         || i.emailStrategy != null) {
       throw new BadRequestException("unsupported option");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index db1e97e..86b269b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -179,7 +179,7 @@
     private final ReviewDbWrapper dbWrapper;
 
     private boolean deleted;
-    private boolean saved;
+    private boolean bumpLastUpdatedOn = true;
 
     private ChangeContext(ChangeControl ctl, ReviewDbWrapper dbWrapper) {
       this.ctl = ctl;
@@ -223,13 +223,11 @@
       return c;
     }
 
-    public void saveChange() {
-      checkState(!deleted, "cannot both save and delete change");
-      saved = true;
+    public void bumpLastUpdatedOn(boolean bump) {
+      bumpLastUpdatedOn = bump;
     }
 
     public void deleteChange() {
-      checkState(!saved, "cannot both save and delete change");
       deleted = true;
     }
   }
@@ -581,14 +579,14 @@
           }
 
           // Bump lastUpdatedOn or rowVersion and commit.
+          Iterable<Change> cs = changesToUpdate(ctx);
           if (newChanges.containsKey(id)) {
-            db.changes().insert(bumpLastUpdatedOn(ctx));
-          } else if (ctx.saved) {
-            db.changes().update(bumpLastUpdatedOn(ctx));
+            // Insert rather than upsert in case of a race on change IDs.
+            db.changes().insert(cs);
           } else if (ctx.deleted) {
-            db.changes().delete(bumpLastUpdatedOn(ctx));
+            db.changes().delete(cs);
           } else {
-            db.changes().update(bumpRowVersionNotLastUpdatedOn(ctx));
+            db.changes().update(cs);
           }
           db.commit();
         } finally {
@@ -618,19 +616,14 @@
     }
   }
 
-  private static Iterable<Change> bumpLastUpdatedOn(ChangeContext ctx) {
+  private static Iterable<Change> changesToUpdate(ChangeContext ctx) {
     Change c = ctx.getChange();
-    if (c.getLastUpdatedOn().before(ctx.getWhen())) {
+    if (ctx.bumpLastUpdatedOn && c.getLastUpdatedOn().before(ctx.getWhen())) {
       c.setLastUpdatedOn(ctx.getWhen());
     }
     return Collections.singleton(c);
   }
 
-  private static Iterable<Change> bumpRowVersionNotLastUpdatedOn(
-      ChangeContext ctx) {
-    return Collections.singleton(ctx.getChange());
-  }
-
   private ChangeContext newChangeContext(Change.Id id) throws Exception {
     Change c = newChanges.get(id);
     if (c == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
index 9042955..eb359e6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
@@ -19,6 +19,8 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.util.SubmoduleSectionParser;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -27,9 +29,7 @@
 import org.eclipse.jgit.lib.BlobBasedConfig;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -49,28 +49,29 @@
   private static final Logger log = LoggerFactory.getLogger(GitModules.class);
 
   public interface Factory {
-    GitModules create(Branch.NameKey project, String submissionId);
+    GitModules create(Branch.NameKey project, String submissionId,
+        MergeOpRepoManager m);
   }
 
   private static final String GIT_MODULES = ".gitmodules";
 
   private final String thisServer;
-  private final GitRepositoryManager repoManager;
   private final SubmoduleSectionParser.Factory subSecParserFactory;
   private final Branch.NameKey branch;
   private final String submissionId;
+  private final MergeOpRepoManager orm;
 
   Set<SubmoduleSubscription> subscriptions;
 
   @AssistedInject
   GitModules(
       @CanonicalWebUrl @Nullable String canonicalWebUrl,
-      GitRepositoryManager repoManager,
       SubmoduleSectionParser.Factory subSecParserFactory,
       @Assisted Branch.NameKey branch,
-      @Assisted String submissionId) throws SubmoduleException {
-    this.repoManager = repoManager;
+      @Assisted String submissionId,
+      @Assisted MergeOpRepoManager orm) throws SubmoduleException {
     this.subSecParserFactory = subSecParserFactory;
+    this.orm = orm;
     this.branch = branch;
     this.submissionId = submissionId;
     try {
@@ -84,24 +85,27 @@
   void load() throws IOException {
     Project.NameKey project = branch.getParentKey();
     logDebug("Loading .gitmodules of {} for project {}", branch, project);
-    try (Repository repo = repoManager.openRepository(project);
-        RevWalk rw = new RevWalk(repo)) {
+    try {
+      orm.openRepo(project, false);
+    } catch (NoSuchProjectException e) {
+      throw new IOException(e);
+    }
+    OpenRepo or = orm.getRepo(project);
 
-      ObjectId id = repo.resolve(branch.get());
-      if (id == null) {
-        throw new IOException("Cannot open branch " + branch.get());
-      }
-      RevCommit commit = rw.parseCommit(id);
+    ObjectId id = or.repo.resolve(branch.get());
+    if (id == null) {
+      throw new IOException("Cannot open branch " + branch.get());
+    }
+    RevCommit commit = or.rw.parseCommit(id);
 
-      TreeWalk tw = TreeWalk.forPath(repo, GIT_MODULES, commit.getTree());
-      if (tw == null
-          || (tw.getRawMode(0) & FileMode.TYPE_MASK) != FileMode.TYPE_FILE) {
-        return;
-      }
-
+    TreeWalk tw = TreeWalk.forPath(or.repo, GIT_MODULES, commit.getTree());
+    if (tw == null
+        || (tw.getRawMode(0) & FileMode.TYPE_MASK) != FileMode.TYPE_FILE) {
+      return;
+    }
+    try {
       BlobBasedConfig bbc =
-          new BlobBasedConfig(null, repo, commit, GIT_MODULES);
-
+          new BlobBasedConfig(null, or.repo, commit, GIT_MODULES);
       subscriptions = subSecParserFactory.create(bbc, thisServer,
           branch).parseAllSections();
     } catch (ConfigInvalidException e) {
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 0047260..26db045 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
@@ -28,7 +28,6 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Ordering;
@@ -54,7 +53,8 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
-import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.MergeOpRepoManager.OpenBranch;
+import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.git.strategy.SubmitStrategy;
 import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
 import com.google.gerrit.server.git.strategy.SubmitStrategyListener;
@@ -62,8 +62,6 @@
 import com.google.gerrit.server.git.validators.MergeValidators;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -72,17 +70,10 @@
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevSort;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -97,7 +88,6 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -117,88 +107,6 @@
 public class MergeOp implements AutoCloseable {
   private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
 
-  private class OpenRepo {
-    final Repository repo;
-    final CodeReviewRevWalk rw;
-    final RevFlag canMergeFlag;
-    final ObjectInserter ins;
-
-    ProjectState project;
-    BatchUpdate update;
-
-    private final ObjectReader reader;
-    private final Map<Branch.NameKey, OpenBranch> branches;
-
-    OpenRepo(Repository repo, ProjectState project) {
-      this.repo = repo;
-      this.project = project;
-      ins = repo.newObjectInserter();
-      reader = ins.newReader();
-      rw = CodeReviewCommit.newRevWalk(reader);
-      rw.sort(RevSort.TOPO);
-      rw.sort(RevSort.COMMIT_TIME_DESC, true);
-      rw.setRetainBody(false);
-      canMergeFlag = rw.newFlag("CAN_MERGE");
-      rw.retainOnReset(canMergeFlag);
-
-      branches = Maps.newHashMapWithExpectedSize(1);
-    }
-
-    OpenBranch getBranch(Branch.NameKey branch) throws IntegrationException {
-      OpenBranch ob = branches.get(branch);
-      if (ob == null) {
-        ob = new OpenBranch(this, branch);
-        branches.put(branch, ob);
-      }
-      return ob;
-    }
-
-    Project.NameKey getProjectName() {
-      return project.getProject().getNameKey();
-    }
-
-    BatchUpdate getUpdate() {
-      if (update == null) {
-        update = batchUpdateFactory.create(db, getProjectName(), caller, ts);
-        update.setRepository(repo, rw, ins);
-      }
-      return update;
-    }
-
-    void close() {
-      if (update != null) {
-        update.close();
-      }
-      rw.close();
-      reader.close();
-      ins.close();
-      repo.close();
-    }
-  }
-
-  private static class OpenBranch {
-    final RefUpdate update;
-    final CodeReviewCommit oldTip;
-    MergeTip mergeTip;
-
-    OpenBranch(OpenRepo or, Branch.NameKey name) throws IntegrationException {
-      try {
-        update = or.repo.updateRef(name.get());
-        if (update.getOldObjectId() != null) {
-          oldTip = or.rw.parseCommit(update.getOldObjectId());
-        } else if (Objects.equals(or.repo.getFullBranch(), name.get())) {
-          oldTip = null;
-          update.setExpectedOldObjectId(ObjectId.zeroId());
-        } else {
-          throw new IntegrationException("The destination branch "
-              + name + " does not exist anymore.");
-        }
-      } catch (IOException e) {
-        throw new IntegrationException("Cannot open branch " + name, e);
-      }
-    }
-  }
-
   public static class CommitStatus {
     private final ImmutableMap<Change.Id, ChangeData> changes;
     private final ImmutableSetMultimap<Branch.NameKey, Change.Id> byBranch;
@@ -307,16 +215,13 @@
 
   private final ChangeMessagesUtil cmUtil;
   private final BatchUpdate.Factory batchUpdateFactory;
-  private final GitRepositoryManager repoManager;
   private final InternalUser.Factory internalUserFactory;
   private final MergeSuperSet mergeSuperSet;
   private final MergeValidators.Factory mergeValidatorsFactory;
-  private final ProjectCache projectCache;
   private final InternalChangeQuery internalChangeQuery;
   private final SubmitStrategyFactory submitStrategyFactory;
   private final Provider<SubmoduleOp> subOpProvider;
-
-  private final Map<Project.NameKey, OpenRepo> openRepos;
+  private final MergeOpRepoManager orm;
 
   private static final String MACHINE_ID;
   static {
@@ -339,56 +244,27 @@
   @Inject
   MergeOp(ChangeMessagesUtil cmUtil,
       BatchUpdate.Factory batchUpdateFactory,
-      GitRepositoryManager repoManager,
       InternalUser.Factory internalUserFactory,
       MergeSuperSet mergeSuperSet,
       MergeValidators.Factory mergeValidatorsFactory,
-      ProjectCache projectCache,
       InternalChangeQuery internalChangeQuery,
       SubmitStrategyFactory submitStrategyFactory,
-      Provider<SubmoduleOp> subOpProvider) {
+      Provider<SubmoduleOp> subOpProvider,
+      MergeOpRepoManager orm) {
     this.cmUtil = cmUtil;
     this.batchUpdateFactory = batchUpdateFactory;
-    this.repoManager = repoManager;
     this.internalUserFactory = internalUserFactory;
     this.mergeSuperSet = mergeSuperSet;
     this.mergeValidatorsFactory = mergeValidatorsFactory;
-    this.projectCache = projectCache;
     this.internalChangeQuery = internalChangeQuery;
     this.submitStrategyFactory = submitStrategyFactory;
     this.subOpProvider = subOpProvider;
-
-    openRepos = new HashMap<>();
-  }
-
-  private OpenRepo getRepo(Project.NameKey project) {
-    OpenRepo or = openRepos.get(project);
-    checkState(or != null, "repo not yet opened: %s", project);
-    return or;
-  }
-
-  private void openRepo(Project.NameKey project)
-      throws NoSuchProjectException, IOException {
-    checkState(!openRepos.containsKey(project),
-        "repo already opened: %s", project);
-    ProjectState projectState = projectCache.get(project);
-    if (projectState == null) {
-      throw new NoSuchProjectException(project);
-    }
-    try {
-      OpenRepo or =
-          new OpenRepo(repoManager.openRepository(project), projectState);
-      openRepos.put(project, or);
-    } catch (RepositoryNotFoundException e) {
-      throw new NoSuchProjectException(project);
-    }
+    this.orm = orm;
   }
 
   @Override
   public void close() {
-    for (OpenRepo repo : openRepos.values()) {
-      repo.close();
-    }
+    orm.close();
   }
 
   private static Optional<SubmitRecord> findOkRecord(
@@ -544,6 +420,8 @@
     this.caller = caller;
     updateSubmissionId(change);
     this.db = db;
+    orm.setContext(db, ts, caller);
+
     logDebug("Beginning integration of {}", change);
     try {
       ChangeSet cs = mergeSuperSet.completeChangeSet(db, change, caller);
@@ -605,14 +483,14 @@
     openRepos(projects);
 
     for (Branch.NameKey branch : branches) {
-      OpenRepo or = getRepo(branch.getParentKey());
+      OpenRepo or = orm.getRepo(branch.getParentKey());
       toSubmit.put(branch, validateChangeList(or, cbb.get(branch)));
     }
     failFast(cs); // Done checks that don't involve running submit strategies.
 
     List<SubmitStrategy> strategies = new ArrayList<>(branches.size());
     for (Branch.NameKey branch : branches) {
-      OpenRepo or = getRepo(branch.getParentKey());
+      OpenRepo or = orm.getRepo(branch.getParentKey());
       OpenBranch ob = or.getBranch(branch);
       BranchBatch submitting = toSubmit.get(branch);
       checkNotNull(submitting.submitType(),
@@ -647,14 +525,13 @@
       throw new IntegrationException(msg, e);
     }
 
-    SubmoduleOp subOp = subOpProvider.get();
-    updateSuperProjects(subOp, br.values());
+    updateSuperProjects(br.values());
   }
 
   private List<BatchUpdate> batchUpdates(Collection<Project.NameKey> projects) {
     List<BatchUpdate> updates = new ArrayList<>(projects.size());
     for (Project.NameKey project : projects) {
-      updates.add(getRepo(project).getUpdate());
+      updates.add(orm.getRepo(project).getUpdate());
     }
     return updates;
   }
@@ -848,11 +725,11 @@
     }
   }
 
-  private void updateSuperProjects(SubmoduleOp subOp,
-      Collection<Branch.NameKey> branches) {
+  private void updateSuperProjects(Collection<Branch.NameKey> branches) {
     logDebug("Updating superprojects");
+    SubmoduleOp subOp = subOpProvider.get();
     try {
-      subOp.updateSuperProjects(db, branches, submissionId);
+      subOp.updateSuperProjects(db, branches, submissionId, orm);
       logDebug("Updating superprojects done");
     } catch (SubmoduleException e) {
       logError("The gitlinks were not updated according to the "
@@ -864,7 +741,7 @@
       throws IntegrationException {
     for (Project.NameKey project : projects) {
       try {
-        openRepo(project);
+        orm.openRepo(project, true);
       } catch (NoSuchProjectException noProject) {
         logWarn("Project " + noProject.project() + " no longer exists, "
             + "abandoning open changes");
@@ -899,7 +776,6 @@
               cmUtil.addChangeMessage(ctx.getDb(),
                   ctx.getUpdate(change.currentPatchSetId()), msg);
 
-              ctx.saveChange();
               return true;
             }
           });
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
new file mode 100644
index 0000000..0482034
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -0,0 +1,199 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevSort;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * This is a helper class for MergeOp and not intended for general use.
+ *
+ * Some database backends require to open a repository just once within
+ * a transaction of a submission, this caches open repositories to satisfy
+ * that requirement.
+ */
+class MergeOpRepoManager implements AutoCloseable {
+  public class OpenRepo {
+    final Repository repo;
+    final CodeReviewRevWalk rw;
+    final RevFlag canMergeFlag;
+    final ObjectInserter ins;
+
+    final ProjectState project;
+    BatchUpdate update;
+
+    private final ObjectReader reader;
+    private final Map<Branch.NameKey, OpenBranch> branches;
+
+    private OpenRepo(Repository repo, ProjectState project) {
+      this.repo = repo;
+      this.project = project;
+      ins = repo.newObjectInserter();
+      reader = ins.newReader();
+      rw = CodeReviewCommit.newRevWalk(reader);
+      rw.sort(RevSort.TOPO);
+      rw.sort(RevSort.COMMIT_TIME_DESC, true);
+      rw.setRetainBody(false);
+      canMergeFlag = rw.newFlag("CAN_MERGE");
+      rw.retainOnReset(canMergeFlag);
+
+      branches = Maps.newHashMapWithExpectedSize(1);
+    }
+
+    OpenBranch getBranch(Branch.NameKey branch) throws IntegrationException {
+      OpenBranch ob = branches.get(branch);
+      if (ob == null) {
+        ob = new OpenBranch(this, branch);
+        branches.put(branch, ob);
+      }
+      return ob;
+    }
+
+    Project.NameKey getProjectName() {
+      return project.getProject().getNameKey();
+    }
+
+    BatchUpdate getUpdate() {
+      checkState(db != null, "call setContext before getUpdate");
+      if (update == null) {
+        update = batchUpdateFactory.create(db, getProjectName(), caller, ts);
+        update.setRepository(repo, rw, ins);
+      }
+      return update;
+    }
+
+    void close() {
+      if (update != null) {
+        update.close();
+      }
+      rw.close();
+      reader.close();
+      ins.close();
+      repo.close();
+    }
+  }
+
+  public static class OpenBranch {
+    final RefUpdate update;
+    final CodeReviewCommit oldTip;
+    MergeTip mergeTip;
+
+    OpenBranch(OpenRepo or, Branch.NameKey name) throws IntegrationException {
+      try {
+        update = or.repo.updateRef(name.get());
+        if (update.getOldObjectId() != null) {
+          oldTip = or.rw.parseCommit(update.getOldObjectId());
+        } else if (Objects.equals(or.repo.getFullBranch(), name.get())) {
+          oldTip = null;
+          update.setExpectedOldObjectId(ObjectId.zeroId());
+        } else {
+          throw new IntegrationException("The destination branch "
+              + name + " does not exist anymore.");
+        }
+      } catch (IOException e) {
+        throw new IntegrationException("Cannot open branch " + name, e);
+      }
+    }
+  }
+
+
+  private final Map<Project.NameKey, OpenRepo> openRepos;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final GitRepositoryManager repoManager;
+  private final ProjectCache projectCache;
+
+  private ReviewDb db;
+  private Timestamp ts;
+  private IdentifiedUser caller;
+
+  @Inject
+  MergeOpRepoManager(
+      GitRepositoryManager repoManager,
+      ProjectCache projectCache,
+      BatchUpdate.Factory batchUpdateFactory) {
+    this.repoManager = repoManager;
+    this.projectCache = projectCache;
+    this.batchUpdateFactory = batchUpdateFactory;
+
+    openRepos = new HashMap<>();
+  }
+
+  void setContext(ReviewDb db, Timestamp ts, IdentifiedUser caller) {
+    this.db = db;
+    this.ts = ts;
+    this.caller = caller;
+  }
+
+  public OpenRepo getRepo(Project.NameKey project) {
+    OpenRepo or = openRepos.get(project);
+    checkState(or != null, "repo not yet opened: %s", project);
+    return or;
+  }
+
+  public void openRepo(Project.NameKey project, boolean abortIfOpen)
+      throws NoSuchProjectException, IOException {
+    if (abortIfOpen) {
+      checkState(!openRepos.containsKey(project),
+          "repo already opened: %s", project);
+    } else {
+      if (openRepos.containsKey(project)) {
+        return;
+      }
+    }
+    ProjectState projectState = projectCache.get(project);
+    if (projectState == null) {
+      throw new NoSuchProjectException(project);
+    }
+    try {
+      OpenRepo or =
+          new OpenRepo(repoManager.openRepository(project), projectState);
+      openRepos.put(project, or);
+    } catch (RepositoryNotFoundException e) {
+      throw new NoSuchProjectException(project);
+    }
+  }
+
+  @Override
+  public void close() {
+    for (OpenRepo repo : openRepos.values()) {
+      repo.close();
+    }
+  }
+}
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 dc3ff6e..83585c1 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
@@ -341,6 +341,7 @@
   private final Provider<SubmoduleOp> subOpProvider;
   private final Provider<Submit> submitProvider;
   private final Provider<MergeOp> mergeOpProvider;
+  private final Provider<MergeOpRepoManager> ormProvider;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final NotesMigration notesMigration;
   private final ChangeEditUtil editUtil;
@@ -391,6 +392,7 @@
       Provider<SubmoduleOp> subOpProvider,
       Provider<Submit> submitProvider,
       Provider<MergeOp> mergeOpProvider,
+      Provider<MergeOpRepoManager> ormProvider,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       NotesMigration notesMigration,
       ChangeEditUtil editUtil,
@@ -440,6 +442,7 @@
     this.subOpProvider = subOpProvider;
     this.submitProvider = submitProvider;
     this.mergeOpProvider = mergeOpProvider;
+    this.ormProvider = ormProvider;
     this.pluginConfigEntries = pluginConfigEntries;
     this.notesMigration = notesMigration;
 
@@ -474,9 +477,11 @@
     });
 
     if (!projectControl.allRefsAreVisible()) {
-      rp.setCheckReferencedObjectsAreReachable(receiveConfig.checkReferencedObjectsAreReachable);
-      rp.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo, projectControl, db, false));
+      rp.setCheckReferencedObjectsAreReachable(
+          receiveConfig.checkReferencedObjectsAreReachable);
     }
+    rp.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo,
+        projectControl, db, false));
     List<AdvertiseRefsHook> advHooks = new ArrayList<>(3);
     advHooks.add(new AdvertiseRefsHook() {
       @Override
@@ -673,8 +678,9 @@
     }
     // Update superproject gitlinks if required.
     SubmoduleOp op = subOpProvider.get();
-    try {
-      op.updateSuperProjects(db, branches, "receiveID");
+    try (MergeOpRepoManager orm = ormProvider.get()) {
+      orm.setContext(db, TimeUtil.nowTs(), user);
+      op.updateSuperProjects(db, branches, "receiveID", orm);
     } catch (SubmoduleException e) {
       log.error("Can't update the superprojects", e);
     }
@@ -1500,8 +1506,8 @@
   private void selectNewAndReplacedChangesFromMagicBranch() {
     newChanges = Lists.newArrayList();
 
-    SetMultimap<ObjectId, Ref> existing = HashMultimap.create();
-    GroupCollector groupCollector = GroupCollector.create(changeRefsById(), db, psUtil,
+    SetMultimap<ObjectId, Ref> existing = changeRefsById();
+    GroupCollector groupCollector = GroupCollector.create(refsById, db, psUtil,
         notesFactory, project.getNameKey());
 
     rp.getRevWalk().reset();
@@ -1522,7 +1528,6 @@
       } else {
         markHeadsAsUninteresting(
             rp.getRevWalk(),
-            existing,
             magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
       }
 
@@ -1679,23 +1684,15 @@
     }
   }
 
-  private void markHeadsAsUninteresting(
-      final RevWalk walk,
-      SetMultimap<ObjectId, Ref> existing,
-      @Nullable String forRef) {
+  private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) {
     for (Ref ref : allRefs.values()) {
-      if (ref.getObjectId() == null) {
-        continue;
-      } else if (ref.getName().startsWith(REFS_CHANGES)) {
-        existing.put(ref.getObjectId(), ref);
-      } else if (ref.getName().startsWith(R_HEADS)
-          || (forRef != null && forRef.equals(ref.getName()))) {
+      if ((ref.getName().startsWith(R_HEADS) || ref.getName().equals(forRef))
+          && ref.getObjectId() != null) {
         try {
-          walk.markUninteresting(walk.parseCommit(ref.getObjectId()));
+          rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
         } catch (IOException e) {
           log.warn(String.format("Invalid ref %s in %s",
               ref.getName(), project.getName()), e);
-          continue;
         }
       }
     }
@@ -2338,11 +2335,11 @@
       if (!(parsedObject instanceof RevCommit)) {
         return;
       }
-      SetMultimap<ObjectId, Ref> existing = HashMultimap.create();
       walk.markStart((RevCommit)parsedObject);
-      markHeadsAsUninteresting(walk, existing, cmd.getRefName());
+      markHeadsAsUninteresting(walk, cmd.getRefName());
+      Set<ObjectId> existing = changeRefsById().keySet();
       for (RevCommit c; (c = walk.next()) != null;) {
-        if (existing.keySet().contains(c)) {
+        if (existing.contains(c)) {
           continue;
         } else if (!validCommit(walk, ctl, cmd, c)) {
           break;
@@ -2521,7 +2518,6 @@
           if (change.getStatus().isOpen()) {
             change.setCurrentPatchSet(info);
             change.setStatus(Change.Status.MERGED);
-            ctx.saveChange();
 
             // we cannot reconstruct the submit records for when this change was
             // submitted, this is why we must fix the status
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
index 9e51b3f..3469298 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -263,7 +263,6 @@
     if (mergedIntoRef == null) {
       resetChange(ctx, msg);
     }
-    ctx.saveChange();
 
     return true;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index e2ec146..63c71a1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -29,6 +29,8 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 
@@ -67,7 +69,6 @@
 
   private final GitModules.Factory gitmodulesFactory;
   private final PersonIdent myIdent;
-  private final GitRepositoryManager repoManager;
   private final GitReferenceUpdated gitRefUpdated;
   private final ProjectCache projectCache;
   private final Set<Branch.NameKey> updatedSubscribers;
@@ -82,14 +83,12 @@
       GitModules.Factory gitmodulesFactory,
       @GerritPersonIdent PersonIdent myIdent,
       @GerritServerConfig Config cfg,
-      GitRepositoryManager repoManager,
       GitReferenceUpdated gitRefUpdated,
       ProjectCache projectCache,
       @Nullable Account account,
       ChangeHooks changeHooks) {
     this.gitmodulesFactory = gitmodulesFactory;
     this.myIdent = myIdent;
-    this.repoManager = repoManager;
     this.gitRefUpdated = gitRefUpdated;
     this.projectCache = projectCache;
     this.account = account;
@@ -99,11 +98,10 @@
     this.enableSuperProjectSubscriptions = cfg.getBoolean("submodule",
         "enableSuperProjectSubscriptions", true);
     updatedSubscribers = new HashSet<>();
-
   }
 
   public Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src,
-      SubscribeSection s) throws IOException {
+      SubscribeSection s, MergeOpRepoManager orm) throws IOException {
     Collection<Branch.NameKey> ret = new ArrayList<>();
     logDebug("Inspecting SubscribeSection " + s);
     for (RefSpec r : s.getRefSpecs()) {
@@ -111,11 +109,17 @@
       if (r.matchSource(src.get())) {
         if (r.getDestination() == null) {
           // no need to care for wildcard, as we matched already
-          try (Repository repo = repoManager.openRepository(s.getProject())) {
-            for (Ref ref : repo.getRefDatabase().getRefs(
-                RefNames.REFS_HEADS).values()) {
-              ret.add(new Branch.NameKey(s.getProject(), ref.getName()));
-            }
+          try {
+            orm.openRepo(s.getProject(), false);
+          } catch (NoSuchProjectException e) {
+            // A project listed a non existent project to be allowed
+            // to subscribe to it. Allow this for now.
+            continue;
+          }
+          OpenRepo or = orm.getRepo(s.getProject());
+          for (Ref ref : or.repo.getRefDatabase().getRefs(
+              RefNames.REFS_HEADS).values()) {
+            ret.add(new Branch.NameKey(s.getProject(), ref.getName()));
           }
         } else if (r.isWildcard()) {
           // refs/heads/*:refs/heads/*
@@ -134,15 +138,16 @@
 
   public Collection<SubmoduleSubscription>
       superProjectSubscriptionsForSubmoduleBranch(
-      Branch.NameKey branch) throws IOException {
+      Branch.NameKey branch, MergeOpRepoManager orm) throws IOException {
     logDebug("Calculating possible superprojects for " + branch);
     Collection<SubmoduleSubscription> ret = new ArrayList<>();
     Project.NameKey project = branch.getParentKey();
     ProjectConfig cfg = projectCache.get(project).getConfig();
     for (SubscribeSection s : cfg.getSubscribeSections(branch)) {
-      Collection<Branch.NameKey> branches = getDestinationBranches(branch, s);
+      Collection<Branch.NameKey> branches =
+          getDestinationBranches(branch, s, orm);
       for (Branch.NameKey targetBranch : branches) {
-        GitModules m = gitmodulesFactory.create(targetBranch, updateId);
+        GitModules m = gitmodulesFactory.create(targetBranch, updateId, orm);
         m.load();
         ret.addAll(m.subscribedTo(branch));
       }
@@ -152,8 +157,8 @@
   }
 
   protected void updateSuperProjects(ReviewDb db,
-      Collection<Branch.NameKey> updatedBranches, String updateId)
-          throws SubmoduleException {
+      Collection<Branch.NameKey> updatedBranches, String updateId,
+      MergeOpRepoManager orm) throws SubmoduleException {
     if (!enableSuperProjectSubscriptions) {
       logDebug("Updating superprojects disabled");
       return;
@@ -168,7 +173,7 @@
     try {
       for (Branch.NameKey updatedBranch : updatedBranches) {
         for (SubmoduleSubscription sub :
-          superProjectSubscriptionsForSubmoduleBranch(updatedBranch)) {
+          superProjectSubscriptionsForSubmoduleBranch(updatedBranch, orm)) {
           targets.put(sub.getSuperProject(), sub);
         }
       }
@@ -182,7 +187,7 @@
         if (!updatedSubscribers.add(dest)) {
           log.error("Possible circular subscription involving " + dest);
         } else {
-          updateGitlinks(db, dest, targets.get(dest));
+          updateGitlinks(db, dest, targets.get(dest), orm);
         }
       } catch (SubmoduleException e) {
         log.warn("Cannot update gitlinks for " + dest, e);
@@ -198,78 +203,92 @@
    * @throws SubmoduleException
    */
   private void updateGitlinks(ReviewDb db, Branch.NameKey subscriber,
-      Collection<SubmoduleSubscription> updates) throws SubmoduleException {
+      Collection<SubmoduleSubscription> updates, MergeOpRepoManager orm)
+          throws SubmoduleException {
     PersonIdent author = null;
     StringBuilder msgbuf = new StringBuilder("Update git submodules\n\n");
     boolean sameAuthorForAll = true;
 
-    try (Repository pdb = repoManager.openRepository(subscriber.getParentKey())) {
-      if (pdb.exactRef(subscriber.get()) == null) {
+    try {
+      orm.openRepo(subscriber.getParentKey(), false);
+    } catch (NoSuchProjectException | IOException e) {
+      throw new SubmoduleException("Cannot access superproject", e);
+    }
+    OpenRepo or = orm.getRepo(subscriber.getParentKey());
+    try {
+      Ref r = or.repo.exactRef(subscriber.get());
+      if (r == null) {
         throw new SubmoduleException(
             "The branch was probably deleted from the subscriber repository");
       }
 
-      DirCache dc = readTree(pdb, pdb.exactRef(subscriber.get()));
+      DirCache dc = readTree(r, or.rw);
       DirCacheEditor ed = dc.editor();
 
       for (SubmoduleSubscription s : updates) {
-        try (Repository subrepo = repoManager.openRepository(
-            s.getSubmodule().getParentKey());
-            RevWalk rw = CodeReviewCommit.newRevWalk(subrepo)) {
-          Ref ref = subrepo.getRefDatabase().exactRef(s.getSubmodule().get());
-          if (ref == null) {
-            ed.add(new DeletePath(s.getPath()));
+        try {
+          orm.openRepo(s.getSubmodule().getParentKey(), false);
+        } catch (NoSuchProjectException | IOException e) {
+          throw new SubmoduleException("Cannot access submodule", e);
+        }
+        OpenRepo subOr = orm.getRepo(s.getSubmodule().getParentKey());
+        Repository subrepo = subOr.repo;
+
+        Ref ref = subrepo.getRefDatabase().exactRef(s.getSubmodule().get());
+        if (ref == null) {
+          ed.add(new DeletePath(s.getPath()));
+          continue;
+        }
+
+        final ObjectId updateTo = ref.getObjectId();
+        RevCommit newCommit = subOr.rw.parseCommit(updateTo);
+
+        subOr.rw.parseBody(newCommit);
+        if (author == null) {
+          author = newCommit.getAuthorIdent();
+        } else if (!author.equals(newCommit.getAuthorIdent())) {
+          sameAuthorForAll = false;
+        }
+
+        DirCacheEntry dce = dc.getEntry(s.getPath());
+        ObjectId oldId;
+        if (dce != null) {
+          if (!dce.getFileMode().equals(FileMode.GITLINK)) {
+            log.error("Requested to update gitlink " + s.getPath() + " in "
+                + s.getSubmodule().getParentKey().get() + " but entry "
+                + "doesn't have gitlink file mode.");
             continue;
           }
+          oldId = dce.getObjectId();
+        } else {
+          // This submodule did not exist before. We do not want to add
+          // the full submodule history to the commit message, so omit it.
+          oldId = updateTo;
+        }
 
-          final ObjectId updateTo = ref.getObjectId();
-          RevCommit newCommit = rw.parseCommit(updateTo);
-
-          if (author == null) {
-            author = newCommit.getAuthorIdent();
-          } else if (!author.equals(newCommit.getAuthorIdent())) {
-            sameAuthorForAll = false;
+        ed.add(new PathEdit(s.getPath()) {
+          @Override
+          public void apply(DirCacheEntry ent) {
+            ent.setFileMode(FileMode.GITLINK);
+            ent.setObjectId(updateTo);
           }
+        });
+        if (verboseSuperProject) {
+          msgbuf.append("Project: " + s.getSubmodule().getParentKey().get());
+          msgbuf.append(" " + s.getSubmodule().getShortName());
+          msgbuf.append(" " + updateTo.getName());
+          msgbuf.append("\n\n");
 
-          DirCacheEntry dce = dc.getEntry(s.getPath());
-          ObjectId oldId;
-          if (dce != null) {
-            if (!dce.getFileMode().equals(FileMode.GITLINK)) {
-              log.error("Requested to update gitlink " + s.getPath() + " in "
-                  + s.getSubmodule().getParentKey().get() + " but entry "
-                  + "doesn't have gitlink file mode.");
-              continue;
+          try {
+            subOr.rw.markStart(newCommit);
+            subOr.rw.markUninteresting(subOr.rw.parseCommit(oldId));
+            for (RevCommit c : subOr.rw) {
+              subOr.rw.parseBody(c);
+              msgbuf.append(c.getFullMessage() + "\n\n");
             }
-            oldId = dce.getObjectId();
-          } else {
-            // This submodule did not exist before. We do not want to add
-            // the full submodule history to the commit message, so omit it.
-            oldId = updateTo;
-          }
-
-          ed.add(new PathEdit(s.getPath()) {
-            @Override
-            public void apply(DirCacheEntry ent) {
-              ent.setFileMode(FileMode.GITLINK);
-              ent.setObjectId(updateTo);
-            }
-          });
-          if (verboseSuperProject) {
-            msgbuf.append("Project: " + s.getSubmodule().getParentKey().get());
-            msgbuf.append(" " + s.getSubmodule().getShortName());
-            msgbuf.append(" " + updateTo.getName());
-            msgbuf.append("\n\n");
-
-            try {
-              rw.markStart(newCommit);
-              rw.markUninteresting(rw.parseCommit(oldId));
-              for (RevCommit c : rw) {
-                msgbuf.append(c.getFullMessage() + "\n\n");
-              }
-            } catch (IOException e) {
-              throw new SubmoduleException("Could not perform a revwalk to "
-                  + "create superproject commit message", e);
-            }
+          } catch (IOException e) {
+            throw new SubmoduleException("Could not perform a revwalk to "
+                + "create superproject commit message", e);
           }
         }
       }
@@ -279,11 +298,11 @@
         author = myIdent;
       }
 
-      ObjectInserter oi = pdb.newObjectInserter();
+      ObjectInserter oi = or.repo.newObjectInserter();
       ObjectId tree = dc.writeTree(oi);
 
       ObjectId currentCommitId =
-          pdb.exactRef(subscriber.get()).getObjectId();
+          or.repo.exactRef(subscriber.get()).getObjectId();
 
       CommitBuilder commit = new CommitBuilder();
       commit.setTreeId(tree);
@@ -296,7 +315,7 @@
 
       ObjectId commitId = oi.idFor(Constants.OBJ_COMMIT, commit.build());
 
-      final RefUpdate rfu = pdb.updateRef(subscriber.get());
+      final RefUpdate rfu = or.repo.updateRef(subscriber.get());
       rfu.setForceUpdate(false);
       rfu.setNewObjectId(commitId);
       rfu.setExpectedOldObjectId(currentCommitId);
@@ -322,24 +341,23 @@
           throw new IOException(rfu.getResult().name());
       }
       // Recursive call: update subscribers of the subscriber
-      updateSuperProjects(db, Sets.newHashSet(subscriber), updateId);
+      updateSuperProjects(db, Sets.newHashSet(subscriber), updateId, orm);
     } catch (IOException e) {
       throw new SubmoduleException("Cannot update gitlinks for "
           + subscriber.get(), e);
     }
   }
 
-  private static DirCache readTree(final Repository pdb, final Ref branch)
-      throws MissingObjectException, IncorrectObjectTypeException, IOException {
-    try (RevWalk rw = new RevWalk(pdb)) {
-      final DirCache dc = DirCache.newInCore();
-      final DirCacheBuilder b = dc.builder();
-      b.addTree(new byte[0], // no prefix path
-          DirCacheEntry.STAGE_0, // standard stage
-          pdb.newObjectReader(), rw.parseTree(branch.getObjectId()));
-      b.finish();
-      return dc;
-    }
+  private static DirCache readTree(final Ref branch, RevWalk rw)
+      throws MissingObjectException, IncorrectObjectTypeException,
+      IOException {
+    final DirCache dc = DirCache.newInCore();
+    final DirCacheBuilder b = dc.builder();
+    b.addTree(new byte[0], // no prefix path
+        DirCacheEntry.STAGE_0, // standard stage
+        rw.getObjectReader(), rw.parseTree(branch.getObjectId()));
+    b.finish();
+    return dc;
   }
 
   private void logDebug(String msg, Object... args) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 9a9e0b9..91d2cc7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -160,7 +160,6 @@
           prevPs != null ? prevPs.getGroups() : ImmutableList.<String> of(),
           null);
       ctx.getChange().setCurrentPatchSet(patchSetInfo);
-      ctx.saveChange();
 
       // Don't copy approvals, as this is already taken care of by
       // SubmitStrategyOp.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
index f886e49..f0dc239 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -484,7 +484,6 @@
     logDebug("Setting change {} merged", c.getId());
     c.setStatus(Change.Status.MERGED);
     c.setSubmissionId(args.submissionId);
-    ctx.saveChange();
 
     // TODO(dborowitz): We need to be able to change the author of the message,
     // which is not the user from the update context. addMergedMessage was able
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 5ca31d0..e67fba2 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
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_123> C = Schema_123.class;
+  public static final Class<Schema_124> C = Schema_124.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_124.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_124.java
new file mode 100644
index 0000000..957d683
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_124.java
@@ -0,0 +1,119 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys.SimpleSshKeyCreator;
+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.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Collection;
+import java.util.Map;
+
+public class Schema_124 extends SchemaVersion {
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsersName;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_124(Provider<Schema_123> prior,
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.allUsersName = allUsersName;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws OrmException, SQLException {
+    Multimap<Account.Id, AccountSshKey> imports = ArrayListMultimap.create();
+    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+      ResultSet rs = stmt.executeQuery(
+          "SELECT "
+          + "account_id, "
+          + "seq, "
+          + "ssh_public_key, "
+          + "valid "
+          + "FROM account_ssh_keys")) {
+      while (rs.next()) {
+        Account.Id accountId = new Account.Id(rs.getInt(1));
+        int seq = rs.getInt(2);
+        String sshPublicKey = rs.getString(3);
+        AccountSshKey key = new AccountSshKey(
+            new AccountSshKey.Id(accountId, seq), sshPublicKey);
+        boolean valid = rs.getBoolean(4);
+        if (!valid) {
+          key.setInvalid();
+        }
+        imports.put(accountId, key);
+      }
+    }
+
+    if (imports.isEmpty()) {
+      return;
+    }
+
+    try (Repository git = repoManager.openRepository(allUsersName);
+        RevWalk rw = new RevWalk(git)) {
+      BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
+
+      for (Map.Entry<Account.Id, Collection<AccountSshKey>> e : imports.asMap()
+          .entrySet()) {
+        try (MetaDataUpdate md = new MetaDataUpdate(
+                 GitReferenceUpdated.DISABLED, allUsersName, git, bru);
+             VersionedAuthorizedKeys authorizedKeys =
+                 new VersionedAuthorizedKeys(
+                     new SimpleSshKeyCreator(), e.getKey())) {
+          md.getCommitBuilder().setAuthor(serverUser);
+          md.getCommitBuilder().setCommitter(serverUser);
+
+          authorizedKeys.load(md);
+          authorizedKeys.setKeys(e.getValue());
+          authorizedKeys.commit(md);
+        }
+      }
+
+      bru.execute(rw, NullProgressMonitor.INSTANCE);
+    } catch (ConfigInvalidException | IOException ex) {
+      throw new OrmException(ex);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
index 0957594..ed9b87c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
@@ -20,13 +20,14 @@
 import com.google.inject.Module;
 
 
-public class NoSshKeyCache implements SshKeyCache {
+public class NoSshKeyCache implements SshKeyCache, SshKeyCreator {
 
   public static Module module() {
     return new AbstractModule() {
       @Override
       protected void configure() {
         bind(SshKeyCache.class).to(NoSshKeyCache.class);
+        bind(SshKeyCreator.class).to(NoSshKeyCache.class);
       }
     };
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
index a3c0a5a..fd8e1b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
@@ -14,13 +14,7 @@
 
 package com.google.gerrit.server.ssh;
 
-import com.google.gerrit.common.errors.InvalidSshKeyException;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
-
 /** Permits controlling the contents of the SSH key cache area. */
 public interface SshKeyCache {
   void evict(String username);
-
-  AccountSshKey create(AccountSshKey.Id id, String encoded)
-      throws InvalidSshKeyException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCreator.java
new file mode 100644
index 0000000..fd0c69c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCreator.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.ssh;
+
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+
+public interface SshKeyCreator {
+  AccountSshKey create(AccountSshKey.Id id, String encoded)
+      throws InvalidSshKeyException;
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java
new file mode 100644
index 0000000..f5849c1
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java
@@ -0,0 +1,174 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Optional;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AuthorizedKeysTest {
+  private static final String KEY1 =
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCgug5VyMXQGnem2H1KVC4/HcRcD4zzBqS"
+      + "uJBRWVonSSoz3RoAZ7bWXCVVGwchtXwUURD689wFYdiPecOrWOUgeeyRq754YWRhU+W28"
+      + "vf8IZixgjCmiBhaL2gt3wff6pP+NXJpTSA4aeWE5DfNK5tZlxlSxqkKOS8JRSUeNQov5T"
+      + "w== john.doe@example.com";
+  private static final String KEY2 =
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDm5yP7FmEoqzQRDyskX+9+N0q9GrvZeh5"
+      + "RG52EUpE4ms/Ujm3ewV1LoGzc/lYKJAIbdcZQNJ9+06EfWZaIRA3oOwAPe1eCnX+aLr8E"
+      + "6Tw2gDMQOGc5e9HfyXpC2pDvzauoZNYqLALOG3y/1xjo7IH8GYRS2B7zO/Mf9DdCcCKSf"
+      + "w== john.doe@example.com";
+  private static final String KEY3 =
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCaS7RHEcZ/zjl9hkWkqnm29RNr2OQ/TZ5"
+      + "jk2qBVMH3BgzPsTsEs+7ag9tfD8OCj+vOcwm626mQBZoR2e3niHa/9gnHBHFtOrGfzKbp"
+      + "RjTWtiOZbB9HF+rqMVD+Dawo/oicX/dDg7VAgOFSPothe6RMhbgWf84UcK5aQd5eP5y+t"
+      + "Q== john.doe@example.com";
+  private static final String KEY4 =
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDIJzW9BaAeO+upFletwwEBnGS15lJmS5i"
+      + "08/NiFef0jXtNNKcLtnd13bq8jOi5VA2is0bwof1c8YbwcvUkdFa8RL5aXoyZBpfYZsWs"
+      + "/YBLZGiHy5rjooMZQMnH37A50cBPnXr0AQz0WRBxLDBDyOZho+O/DfYAKv4rzPSQ3yw4+"
+      + "w== john.doe@example.com";
+  private static final String KEY5 =
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCgBRKGhiXvY6D9sM+Vbth5Kate57YF7kD"
+      + "rqIyUiYIMJK93/AXc8qR/J/p3OIFQAxvLz1qozAur3j5HaiwvxVU19IiSA0vafdhaDLRi"
+      + "zRuEL5e/QOu9yGq9xkWApCmg6edpWAHG+Bx4AldU78MiZvzoB7gMMdxc9RmZ1gYj/DjxV"
+      + "w== john.doe@example.com";
+
+  @Test
+  public void test() throws Exception {
+    List<Optional<AccountSshKey>> keys = new ArrayList<>();
+    StringBuilder expected = new StringBuilder();
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+
+    expected.append(addKey(keys, KEY1));
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+
+    expected.append(addKey(keys, KEY2));
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+
+    expected.append(addInvalidKey(keys, KEY3));
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+
+    expected.append(addKey(keys, KEY4));
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+
+    expected.append(addDeletedKey(keys));
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+
+    expected.append(addKey(keys, KEY5));
+    assertSerialization(keys, expected);
+    assertParse(expected, keys);
+  }
+
+  @Test
+  public void testParseWindowsLineEndings() throws Exception {
+    List<Optional<AccountSshKey>> keys = new ArrayList<>();
+    StringBuilder authorizedKeys = new StringBuilder();
+    authorizedKeys.append(toWindowsLineEndings(addKey(keys, KEY1)));
+    assertParse(authorizedKeys, keys);
+
+    authorizedKeys.append(toWindowsLineEndings(addKey(keys, KEY2)));
+    assertParse(authorizedKeys, keys);
+
+    authorizedKeys.append(toWindowsLineEndings(addInvalidKey(keys, KEY3)));
+    assertParse(authorizedKeys, keys);
+
+    authorizedKeys.append(toWindowsLineEndings(addKey(keys, KEY4)));
+    assertParse(authorizedKeys, keys);
+
+    authorizedKeys.append(toWindowsLineEndings(addDeletedKey(keys)));
+    assertParse(authorizedKeys, keys);
+
+    authorizedKeys.append(toWindowsLineEndings(addKey(keys, KEY5)));
+    assertParse(authorizedKeys, keys);
+
+  }
+
+  private static String toWindowsLineEndings(String s) {
+    return s.replaceAll("\n", "\r\n");
+  }
+
+  private static void assertSerialization(List<Optional<AccountSshKey>> keys,
+      StringBuilder expected) {
+    assertThat(AuthorizedKeys.serialize(keys)).isEqualTo(expected.toString());
+  }
+
+  private static void assertParse(StringBuilder authorizedKeys,
+      List<Optional<AccountSshKey>> expectedKeys) {
+    Account.Id accountId = new Account.Id(1);
+    List<Optional<AccountSshKey>> parsedKeys =
+        AuthorizedKeys.parse(accountId, authorizedKeys.toString());
+    assertThat(parsedKeys).containsExactlyElementsIn(expectedKeys);
+    int seq = 1;
+    for(Optional<AccountSshKey> sshKey : parsedKeys) {
+      if (sshKey.isPresent()) {
+        assertThat(sshKey.get().getAccount()).isEqualTo(accountId);
+        assertThat(sshKey.get().getKey().get()).isEqualTo(seq);
+      }
+      seq++;
+    }
+  }
+
+  /**
+   * Adds the given public key as new SSH key to the given list.
+   *
+   * @return the expected line for this key in the authorized_keys file
+   */
+  private static String addKey(List<Optional<AccountSshKey>> keys, String pub) {
+    AccountSshKey.Id keyId =
+        new AccountSshKey.Id(new Account.Id(1), keys.size() + 1);
+    AccountSshKey key = new AccountSshKey(keyId, pub);
+    keys.add(Optional.of(key));
+    return key.getSshPublicKey() + "\n";
+  }
+
+  /**
+   * Adds the given public key as invalid SSH key to the given list.
+   *
+   * @return the expected line for this key in the authorized_keys file
+   */
+  private static String addInvalidKey(List<Optional<AccountSshKey>> keys,
+      String pub) {
+    AccountSshKey.Id keyId =
+        new AccountSshKey.Id(new Account.Id(1), keys.size() + 1);
+    AccountSshKey key = new AccountSshKey(keyId, pub);
+    key.setInvalid();
+    keys.add(Optional.of(key));
+    return AuthorizedKeys.INVALID_KEY_COMMENT_PREFIX
+        + key.getSshPublicKey() + "\n";
+  }
+
+  /**
+   * Adds a deleted SSH key to the given list.
+   *
+   * @return the expected line for this key in the authorized_keys file
+   */
+  private static String addDeletedKey(List<Optional<AccountSshKey>> keys) {
+    keys.add(Optional.<AccountSshKey> absent());
+    return AuthorizedKeys.DELETED_KEY_COMMENT + "\n";
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
index 01465a2..ad9a46a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.reviewdb.server.AccountGroupNameAccess;
 import com.google.gerrit.reviewdb.server.AccountPatchReviewAccess;
 import com.google.gerrit.reviewdb.server.AccountProjectWatchAccess;
-import com.google.gerrit.reviewdb.server.AccountSshKeyAccess;
 import com.google.gerrit.reviewdb.server.ChangeAccess;
 import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
 import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
@@ -97,11 +96,6 @@
   }
 
   @Override
-  public AccountSshKeyAccess accountSshKeys() {
-    throw new Disabled();
-  }
-
-  @Override
   public AccountGroupAccess accountGroups() {
     throw new Disabled();
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 4654069..bf3e6bc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -18,13 +18,13 @@
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.ssh.SshKeyCreator;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -32,12 +32,11 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.spec.InvalidKeySpecException;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -64,6 +63,7 @@
           .loader(Loader.class);
         bind(SshKeyCacheImpl.class);
         bind(SshKeyCache.class).to(SshKeyCacheImpl.class);
+        bind(SshKeyCreator.class).to(SshKeyCreatorImpl.class);
       }
     };
   }
@@ -97,48 +97,34 @@
     }
   }
 
-  @Override
-  public AccountSshKey create(AccountSshKey.Id id, String encoded)
-      throws InvalidSshKeyException {
-    try {
-      final AccountSshKey key =
-          new AccountSshKey(id, SshUtil.toOpenSshPublicKey(encoded));
-      SshUtil.parse(key);
-      return key;
-    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
-      throw new InvalidSshKeyException();
-
-    } catch (NoSuchProviderException e) {
-      log.error("Cannot parse SSH key", e);
-      throw new InvalidSshKeyException();
-    }
-  }
-
   static class Loader extends CacheLoader<String, Iterable<SshKeyCacheEntry>> {
     private final SchemaFactory<ReviewDb> schema;
+    private final VersionedAuthorizedKeys.Accessor authorizedKeys;
 
     @Inject
-    Loader(SchemaFactory<ReviewDb> schema) {
+    Loader(SchemaFactory<ReviewDb> schema,
+        VersionedAuthorizedKeys.Accessor authorizedKeys) {
       this.schema = schema;
+      this.authorizedKeys = authorizedKeys;
     }
 
     @Override
     public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
       try (ReviewDb db = schema.open()) {
-        final AccountExternalId.Key key =
+        AccountExternalId.Key key =
             new AccountExternalId.Key(SCHEME_USERNAME, username);
-        final AccountExternalId user = db.accountExternalIds().get(key);
+        AccountExternalId user = db.accountExternalIds().get(key);
         if (user == null) {
           return NO_SUCH_USER;
         }
 
-        final List<SshKeyCacheEntry> kl = new ArrayList<>(4);
-        for (AccountSshKey k : db.accountSshKeys().byAccount(
-            user.getAccountId())) {
+        List<SshKeyCacheEntry> kl = new ArrayList<>(4);
+        for (AccountSshKey k : authorizedKeys.getKeys(user.getAccountId())) {
           if (k.isValid()) {
-            add(db, kl, k);
+            add(kl, k);
           }
         }
+
         if (kl.isEmpty()) {
           return NO_KEYS;
         }
@@ -146,7 +132,7 @@
       }
     }
 
-    private void add(ReviewDb db, List<SshKeyCacheEntry> kl, AccountSshKey k) {
+    private void add(List<SshKeyCacheEntry> kl, AccountSshKey k) {
       try {
         kl.add(new SshKeyCacheEntry(k.getKey(), SshUtil.parse(k)));
       } catch (OutOfMemoryError e) {
@@ -155,16 +141,16 @@
         //
         throw e;
       } catch (Throwable e) {
-        markInvalid(db, k);
+        markInvalid(k);
       }
     }
 
-    private void markInvalid(final ReviewDb db, final AccountSshKey k) {
+    private void markInvalid(AccountSshKey k) {
       try {
         log.info("Flagging SSH key " + k.getKey() + " invalid");
+        authorizedKeys.markKeyInvalid(k.getAccount(), k.getKey().get());
         k.setInvalid();
-        db.accountSshKeys().update(Collections.singleton(k));
-      } catch (OrmException e) {
+      } catch (IOException | ConfigInvalidException e) {
         log.error("Failed to mark SSH key" + k.getKey() + " invalid", e);
       }
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
new file mode 100644
index 0000000..0fd6db4
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.server.ssh.SshKeyCreator;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.spec.InvalidKeySpecException;
+
+public class SshKeyCreatorImpl implements SshKeyCreator {
+  private static final Logger log =
+      LoggerFactory.getLogger(SshKeyCreatorImpl.class);
+
+  @Override
+  public AccountSshKey create(AccountSshKey.Id id, String encoded)
+      throws InvalidSshKeyException {
+    try {
+      AccountSshKey key =
+          new AccountSshKey(id, SshUtil.toOpenSshPublicKey(encoded));
+      SshUtil.parse(key);
+      return key;
+    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+      throw new InvalidSshKeyException();
+
+    } catch (NoSuchProviderException e) {
+      log.error("Cannot parse SSH key", e);
+      throw new InvalidSshKeyException();
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index acbc50e..a84fe04 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -29,6 +29,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -64,7 +65,8 @@
   private CreateAccount.Factory createAccountFactory;
 
   @Override
-  protected void run() throws OrmException, IOException, UnloggedFailure {
+  protected void run() throws OrmException, IOException, ConfigInvalidException,
+      UnloggedFailure {
     CreateAccount.Input input = new CreateAccount.Input();
     input.username = username;
     input.email = email;
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 6963df1..6e03ac1 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
@@ -47,6 +47,8 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -167,7 +169,8 @@
     }
   }
 
-  private void setAccount() throws OrmException, IOException, UnloggedFailure {
+  private void setAccount() throws OrmException, IOException, UnloggedFailure,
+      ConfigInvalidException {
     user = genericUserFactory.create(id);
     rsrc = new AccountResource(user);
     try {
@@ -220,7 +223,7 @@
   }
 
   private void addSshKeys(List<String> sshKeys) throws RestApiException,
-      OrmException, IOException {
+      OrmException, IOException, ConfigInvalidException {
     for (final String sshKey : sshKeys) {
       AddSshKey.Input in = new AddSshKey.Input();
       in.raw = RawInputUtil.create(sshKey.getBytes(), "plain/text");
@@ -228,8 +231,9 @@
     }
   }
 
-  private void deleteSshKeys(List<String> sshKeys) throws RestApiException,
-      OrmException {
+  private void deleteSshKeys(List<String> sshKeys)
+      throws RestApiException, OrmException, RepositoryNotFoundException,
+      IOException, ConfigInvalidException {
     List<SshKeyInfo> infos = getSshKeys.apply(rsrc);
     if (sshKeys.contains("ALL")) {
       for (SshKeyInfo i : infos) {
@@ -247,7 +251,8 @@
     }
   }
 
-  private void deleteSshKey(SshKeyInfo i) throws AuthException, OrmException {
+  private void deleteSshKey(SshKeyInfo i) throws AuthException, OrmException,
+      RepositoryNotFoundException, IOException, ConfigInvalidException {
     AccountSshKey sshKey = new AccountSshKey(
         new AccountSshKey.Id(user.getAccountId(), i.seq), i.sshPublicKey);
     deleteSshKey.apply(
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
index 39efb26..b420a5f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
@@ -68,10 +68,8 @@
     }
 
     final UploadPack up = new UploadPack(repo);
-    if (!projectControl.allRefsAreVisible()) {
-      up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo,
-          projectControl, db, true));
-    }
+    up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo,
+        projectControl, db, true));
     up.setPackConfig(config.getPackConfig());
     up.setTimeout(config.getTimeout());
     up.setPostUploadHook(uploadMetrics);
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java
index 667f274..8e18feb1 100644
--- a/lib/asciidoctor/java/AsciiDoctor.java
+++ b/lib/asciidoctor/java/AsciiDoctor.java
@@ -151,6 +151,7 @@
       }
 
       File[] cssFiles = tmpdir.listFiles(new FilenameFilter() {
+        @Override
         public boolean accept(File dir, String name) {
           return name.endsWith(".css");
         }
diff --git a/plugins/replication b/plugins/replication
index b80cd81..8db7117 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit b80cd8168ae8ba065c0186b1ddfec366a6368cb6
+Subproject commit 8db7117d509498c7d88979034afbc67220d75f6c
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index 1d88b28..24f915a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -68,9 +68,12 @@
     <span class="cell keyboard">
       <span class="positionIndicator">&#x25b6;</span>
     </span>
-    <span class="cell star" hidden$="[[!showStar]]">
+    <span class="cell star" hidden$="[[!showStar]]" hidden>
       <gr-change-star change="{{change}}"></gr-change-star>
     </span>
+    <a class="cell number" href$="[[changeURL]]" hidden$="[[!showNumber]]" hidden>
+      [[change._number]]
+    </a>
     <a class="cell subject" href$="[[changeURL]]">[[change.subject]]</a>
     <span class="cell status">[[_computeChangeStatusString(change)]]</span>
     <span class="cell owner">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
index 8ff66cb..bab2014 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -18,6 +18,7 @@
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior.html">
 <link rel="import" href="../../../styles/gr-change-list-styles.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-change-list-item/gr-change-list-item.html">
 
 <dom-module id="gr-change-list">
@@ -31,7 +32,8 @@
     <style include="gr-change-list-styles"></style>
     <div class="headerRow">
       <span class="topHeader keyboard"></span> <!-- keyboard position indicator -->
-      <span class="topHeader star" hidden$="[[!showStar]]"></span>
+      <span class="topHeader star" hidden$="[[!showStar]]" hidden></span>
+      <span class="topHeader number" hidden$="[[!showNumber]]" hidden>#</span>
       <span class="topHeader subject">Subject</span>
       <span class="topHeader status">Status</span>
       <span class="topHeader owner">Owner</span>
@@ -57,10 +59,12 @@
             selected$="[[_computeItemSelected(index, groupIndex, selectedIndex)]]"
             needs-review="[[_computeItemNeedsReview(account, change, showReviewedState)]]"
             change="[[change]]"
+            show-number="[[showNumber]]"
             show-star="[[showStar]]"
             label-names="[[labelNames]]"></gr-change-list-item>
       </template>
     </template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-change-list.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index d1b2456..4e17253 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -58,6 +58,7 @@
         type: Number,
         notify: true,
       },
+      showNumber: Boolean, // No default value to prevent flickering.
       showStar: {
         type: Boolean,
         value: false,
@@ -77,6 +78,31 @@
       Gerrit.RESTClientBehavior,
     ],
 
+    attached: function() {
+      this._loadPreferences();
+    },
+
+    _loadPreferences: function() {
+      return this._getLoggedIn().then(function(loggedIn) {
+        if (!loggedIn) {
+          this.showNumber = false;
+          return;
+        }
+        return this._getPreferences().then(function(preferences) {
+          this.showNumber = !!(preferences &&
+              preferences.legacycid_in_change_table);
+        }.bind(this));
+      }.bind(this));
+    },
+
+    _getLoggedIn: function() {
+      return this.$.restAPI.getLoggedIn();
+    },
+
+    _getPreferences: function() {
+      return this.$.restAPI.getPreferences();
+    },
+
     _computeLabelNames: function(groups) {
       if (!groups) { return []; }
       var labels = [];
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index f8f34da..aa77b77 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -46,6 +46,60 @@
       element = fixture('basic');
     });
 
+    function stubRestAPI(preferences) {
+      var loggedInPromise = Promise.resolve(preferences !== null);
+      var preferencesPromise = Promise.resolve(preferences);
+      stub('gr-rest-api-interface', {
+        getLoggedIn: sinon.stub().returns(loggedInPromise),
+        getPreferences: sinon.stub().returns(preferencesPromise),
+      });
+      return Promise.all([loggedInPromise, preferencesPromise]);
+    }
+
+    suite('test show change number not logged in', function() {
+      setup(function(done) {
+        return stubRestAPI(null).then(function() {
+          element = fixture('basic');
+          element._loadPreferences().then(function() { done(); });
+        });
+      });
+
+      test('show number disabled', function() {
+        assert.isFalse(element.showNumber);
+      });
+    });
+
+    suite('test show change number preference enabled', function() {
+      setup(function(done) {
+        return stubRestAPI(
+          {legacycid_in_change_table: true, time_format: 'HHMM_12'}
+        ).then(function() {
+          element = fixture('basic');
+          element._loadPreferences().then(function() { done(); });
+        });
+      });
+
+      test('show number enabled', function() {
+        assert.isTrue(element.showNumber);
+      });
+    });
+
+    suite('test show change number preference disabled', function() {
+      setup(function(done) {
+        // legacycid_in_change_table is not set when false.
+        return stubRestAPI(
+          {time_format: 'HHMM_12'}
+        ).then(function() {
+          element = fixture('basic');
+          element._loadPreferences().then(function() { done(); });
+        });
+      });
+
+      test('show number disabled', function() {
+        assert.isFalse(element.showNumber);
+      });
+    });
+
     test('computed fields', function() {
       assert.equal(element._computeLabelNames(
           [[{_number: 0, labels: {}}]]).length, 0);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index d94a828..1066625 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -16,7 +16,6 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../shared/gr-request/gr-request.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-file-list">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index ad2b925..71fee37 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -19,7 +19,6 @@
 <link rel="import" href="../../../bower_components/iron-selector/iron-selector.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-request/gr-request.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-reply-dialog">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 1cd0ce2..47324ba 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -46,7 +46,6 @@
       permittedLabels: Object,
 
       _account: Object,
-      _xhrPromise: Object,  // Used for testing.
     },
 
     behaviors: [
@@ -140,26 +139,24 @@
         obj.message = this.draft;
       }
       this.disabled = true;
-      this._send(obj).then(function(req) {
-        this.fire('send', null, {bubbles: false});
-        this.draft = '';
+      this._saveReview(obj).then(function(response) {
         this.disabled = false;
-      }.bind(this)).catch(function(err) {
-        alert('Oops. Something went wrong. Check the console and bug the ' +
-            'PolyGerrit team for assistance.');
-        throw err;
+        if (!response.ok) {
+          alert('Oops. Something went wrong. Check the console and bug the ' +
+              'PolyGerrit team for assistance.');
+          return response.text().then(function(text) {
+            console.error(text);
+          });
+        }
+
+        this.draft = '';
+        this.fire('send', null, {bubbles: false});
       }.bind(this));
     },
 
-    _send: function(payload) {
-      var xhr = document.createElement('gr-request');
-      this._xhrPromise = xhr.send({
-        method: 'POST',
-        url: this.changeBaseURL(this.changeNum, this.patchNum) + '/review',
-        body: payload,
-      });
-
-      return this._xhrPromise;
+    _saveReview: function(review) {
+      return this.$.restAPI.saveChangeReview(this.changeNum, this.patchNum,
+          review);
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index fb2de6a..bc850e3 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -34,11 +34,10 @@
 <script>
   suite('gr-reply-dialog tests', function() {
     var element;
-    var server;
 
     setup(function() {
-      stub('gr-reply-dialog', {
-        _getAccount: function() { return Promise.resolve({}); },
+      stub('gr-rest-api-interface', {
+        getAccount: function() { return Promise.resolve({}); },
       });
       element = fixture('basic');
       element.changeNum = 42;
@@ -76,31 +75,10 @@
         ]
       };
 
-      server = sinon.fakeServer.create();
-      server.respondWith(
-        'POST',
-        '/changes/42/revisions/1/review',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          '{' +
-            '"labels": {' +
-              '"Code-Review": -1,' +
-              '"Verified": -1' +
-            '}' +
-          '}'
-        ]
-      );
-
       // Allow the elements created by dom-repeat to be stamped.
       flushAsynchronousOperations();
     });
 
-    teardown(function() {
-      server.restore();
-    });
-
     test('cancel event', function(done) {
       element.addEventListener('cancel', function() { done(); });
       MockInteractions.tap(element.$$('.cancel'));
@@ -122,32 +100,32 @@
             'iron-selector[data-label="Verified"] > ' +
             'gr-button[data-value="-1"]'));
 
+        var saveReviewStub = sinon.stub(element, '_saveReview',
+            function(review) {
+          assert.deepEqual(review, {
+            drafts: 'PUBLISH_ALL_REVISIONS',
+            labels: {
+              'Code-Review': -1,
+              'Verified': -1
+            },
+            message: 'I wholeheartedly disapprove'
+          });
+          return Promise.resolve({ok: true});
+        });
+
+        element.addEventListener('send', function() {
+          assert.isFalse(element.disabled,
+              'Element should be enabled when done sending reply.');
+          assert.equal(element.draft.length, 0);
+          saveReviewStub.restore();
+          done();
+        });
+
         // This is needed on non-Blink engines most likely due to the ways in
         // which the dom-repeat elements are stamped.
         flush(function() {
           MockInteractions.tap(element.$$('.send'));
           assert.isTrue(element.disabled);
-
-          server.respond();
-
-          element._xhrPromise.then(function(req) {
-            assert.isFalse(element.disabled,
-                'Element should be enabled when done sending reply.');
-            assert.equal(req.status, 200);
-            assert.equal(req.url, '/changes/42/revisions/1/review');
-            var reqObj = JSON.parse(req.xhr.requestBody);
-            assert.deepEqual(reqObj, {
-              drafts: 'PUBLISH_ALL_REVISIONS',
-              labels: {
-                'Code-Review': -1,
-                'Verified': -1
-              },
-              message: 'I wholeheartedly disapprove'
-            });
-            assert.equal(req.response.labels['Code-Review'], -1);
-            assert.equal(req.response.labels.Verified, -1);
-            done();
-          });
         });
       });
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
new file mode 100644
index 0000000..28e45a4
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
@@ -0,0 +1,58 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-button/gr-button.html">
+
+<dom-module id="gr-alert">
+  <template>
+    <style>
+      /**
+       * ALERT: DO NOT ADD TRANSITION PROPERTIES WITHOUT PROPERLY UNDERSTANDING
+       * HOW THEY ARE USED IN THE CODE.
+       */
+      :host([toast]) {
+        background-color: #333;
+        bottom: 1.25rem;
+        border-radius: 3px;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+        color: #fff;
+        left: 1.25rem;
+        padding: 1em 1.5em;
+        position: fixed;
+        transform: translateY(5rem);
+        transition: transform var(--gr-alert-transition-duration, 80ms) ease-out;
+      }
+      :host([shown]) {
+        transform: translateY(0);
+      }
+      .action {
+        color: #a1c2fa;
+        font-weight: bold;
+        margin-left: 1em;
+        text-decoration: none;
+      }
+    </style>
+    [[text]]
+    <gr-button
+        link
+        class="action"
+        hidden$="[[!actionText]]"
+        on-tap="_handleActionTap">[[actionText]]</gr-button>
+  </template>
+  <script src="gr-alert.js"></script>
+</dom-module>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
new file mode 100644
index 0000000..ba481ec
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -0,0 +1,88 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-alert',
+
+    /**
+     * Fired when the action button is pressed.
+     *
+     * @event action
+     */
+
+    properties: {
+      text: String,
+      actionText: String,
+      shown: {
+        type: Boolean,
+        value: true,
+        readOnly: true,
+        reflectToAttribute: true,
+      },
+      toast: {
+        type: Boolean,
+        value: true,
+        reflectToAttribute: true,
+      },
+
+      _boundTransitionEndHandler: {
+        type: Function,
+        value: function() { return this._handleTransitionEnd.bind(this); },
+      },
+    },
+
+    attached: function() {
+      this.addEventListener('transitionend', this._boundTransitionEndHandler);
+    },
+
+    detached: function() {
+      this.removeEventListener('transitionend',
+          this._boundTransitionEndHandler);
+    },
+
+    show: function(text, opt_actionText) {
+      this.text = text;
+      this.actionText = opt_actionText;
+      document.body.appendChild(this);
+      this._setShown(true);
+    },
+
+    hide: function() {
+      this._setShown(false);
+      if (this._hasZeroTransitionDuration()) {
+        document.body.removeChild(this);
+      }
+    },
+
+    _hasZeroTransitionDuration: function() {
+      var style = window.getComputedStyle(this);
+      // transitionDuration is always given in seconds.
+      var duration = Math.round(parseFloat(style.transitionDuration) * 100);
+      return duration === 0;
+    },
+
+    _handleTransitionEnd: function(e) {
+      if (this.shown) { return; }
+
+      document.body.removeChild(this);
+    },
+
+    _handleActionTap: function(e) {
+      e.preventDefault();
+      this.fire('action', null, {bubbles: false});
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
new file mode 100644
index 0000000..067ac5b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2015 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-alert</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="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-alert.html">
+
+<script>
+  suite('gr-alert tests', function() {
+    var element;
+
+    setup(function() {
+      element = document.createElement('gr-alert');
+    });
+
+    teardown(function() {
+      if (element.parentNode) {
+        element.parentNode.removeChild(element);
+      }
+    });
+
+    test('show/hide', function() {
+      assert.isNull(element.parentNode);
+      element.show();
+      assert.equal(element.parentNode, document.body);
+      element.customStyle['--gr-alert-transition-duration'] = '0ms';
+      element.updateStyles();
+      element.hide();
+      assert.isNull(element.parentNode);
+    });
+
+    test('action event', function(done) {
+      element.show();
+      element.addEventListener('action', function() {
+        done();
+      });
+      MockInteractions.tap(element.$$('.action'));
+    });
+
+  });
+</script>
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 f23d50b..54ce15c 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
@@ -325,6 +325,12 @@
       return this.send(method, url, null, opt_errFn, opt_ctx);
     },
 
+    saveChangeReview: function(changeNum, patchNum, review, opt_errFn,
+        opt_ctx) {
+      var url = this.getChangeActionURL(changeNum, patchNum, '/review');
+      return this.send('POST', url, review, opt_errFn, opt_ctx);
+    },
+
     send: function(method, url, opt_body, opt_errFn, opt_ctx) {
       var headers = new Headers({
         'X-Gerrit-Auth': this._getCookie('XSRF_TOKEN'),
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index b4cc415..6e34c2b 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -44,6 +44,9 @@
       .star {
         padding-top: .05em;
       }
+      .number {
+        width: 4em;
+      }
       .subject {
         flex-grow: 1;
         flex-shrink: 1;
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 4238f9a..dc12bf9 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -48,6 +48,7 @@
     '../elements/diff/gr-diff-preferences/gr-diff-preferences_test.html',
     '../elements/diff/gr-diff-view/gr-diff-view_test.html',
     '../elements/diff/gr-patch-range-select/gr-patch-range-select_test.html',
+    '../elements/shared/gr-alert/gr-alert_test.html',
     '../elements/shared/gr-account-label/gr-account-label_test.html',
     '../elements/shared/gr-account-link/gr-account-link_test.html',
     '../elements/shared/gr-avatar/gr-avatar_test.html',