Merge "Load default site theme synchronously"
diff --git a/Documentation/config-accounts.txt b/Documentation/config-accounts.txt
index 97f698e..6378632 100644
--- a/Documentation/config-accounts.txt
+++ b/Documentation/config-accounts.txt
@@ -202,9 +202,8 @@
 If the value for a preference is the same as the default value for this
 preference, it can be omitted in the `preference.config` file.
 
-Defaults for general and diff preferences that apply for all accounts
-can be configured in the `refs/users/default` branch in the `All-Users`
-repository.
+Defaults for preferences that apply for all accounts can be configured
+in the `refs/users/default` branch in the `All-Users` repository.
 
 [[project-watches]]
 === Project Watches
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 0c30a4b..7b51567 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -306,6 +306,7 @@
 * `SKIP_MERGEABLE`: skip the `mergeable` field in
 link:#change-info[ChangeInfo]. For fast moving projects, this field must
 be recomputed often, which is slow for projects with big trees.
+--
 
 [[submittable]]
 --
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 8aa1f42..cba91bd 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1223,6 +1223,103 @@
   }
 ----
 
+[[get-edit-preferences]]
+=== Get Default Edit Preferences
+
+--
+'GET /config/server/preferences.edit'
+--
+
+Returns the default edit preferences for the server.
+
+.Request
+----
+  GET /a/config/server/preferences.edit HTTP/1.0
+----
+
+As response a link:rest-api-accounts.html#edit-preferences-info[
+EditPreferencesInfo] is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "tab_size": 8,
+    "line_length": 100,
+    "indent_unit": 2,
+    "cursor_blink_rate": 0,
+    "show_tabs": true,
+    "syntax_highlighting": true,
+    "match_brackets": true,
+    "auto_close_brackets": true,
+    "theme": "DEFAULT",
+    "key_map_type": "DEFAULT"
+  }
+----
+
+[[set-edit-preferences]]
+=== Set Default Edit Preferences
+
+--
+'PUT /config/server/preferences.edit'
+--
+
+Sets the default edit preferences for the server.
+
+The new edit preferences must be provided in the request body as a
+link:rest-api-accounts.html#edit-preferences-input[
+EditPreferencesInput] entity.
+
+To be allowed to set default edit preferences, a user must be a member
+of a group that is granted the
+link:access-control.html#capability_administrateServer[
+Administrate Server] capability.
+
+.Request
+----
+  PUT /a/config/server/preferences.edit HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "tab_size": 8,
+    "line_length": 80,
+    "indent_unit": 2,
+    "cursor_blink_rate": 0,
+    "show_tabs": true,
+    "syntax_highlighting": true,
+    "match_brackets": true,
+    "auto_close_brackets": true,
+    "theme": "DEFAULT",
+    "key_map_type": "DEFAULT"
+  }
+----
+
+As response a link:rest-api-accounts.html#edit-preferences-info[
+EditPreferencesInfo] is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "tab_size": 8,
+    "line_length": 80,
+    "indent_unit": 2,
+    "cursor_blink_rate": 0,
+    "show_tabs": true,
+    "syntax_highlighting": true,
+    "match_brackets": true,
+    "auto_close_brackets": true,
+    "theme": "DEFAULT",
+    "key_map_type": "DEFAULT"
+  }
+----
+
 
 [[ids]]
 == IDs
diff --git a/java/com/google/gerrit/extensions/api/config/Server.java b/java/com/google/gerrit/extensions/api/config/Server.java
index ba81698..5ec63af 100644
--- a/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/java/com/google/gerrit/extensions/api/config/Server.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.api.config;
 
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -34,6 +35,10 @@
 
   DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in) throws RestApiException;
 
+  EditPreferencesInfo getDefaultEditPreferences() throws RestApiException;
+
+  EditPreferencesInfo setDefaultEditPreferences(EditPreferencesInfo in) throws RestApiException;
+
   ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException;
 
   /**
@@ -74,6 +79,17 @@
     }
 
     @Override
+    public EditPreferencesInfo getDefaultEditPreferences() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public EditPreferencesInfo setDefaultEditPreferences(EditPreferencesInfo in)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 853d173..b417f6a 100644
--- a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -103,10 +103,10 @@
       res = byUserName(req.getParameter("user_name"));
 
     } else if (req.getParameter("preferred_email") != null) {
-      res = byPreferredEmail(req.getParameter("preferred_email"));
+      res = byPreferredEmail(req.getParameter("preferred_email")).orElse(null);
 
     } else if (req.getParameter("account_id") != null) {
-      res = byAccountId(req.getParameter("account_id"));
+      res = byAccountId(req.getParameter("account_id")).orElse(null);
 
     } else {
       byte[] raw;
@@ -183,11 +183,8 @@
     return HtmlDomUtil.toUTF8(doc);
   }
 
-  private AuthResult auth(AccountState account) {
-    if (account != null) {
-      return new AuthResult(account.getAccount().getId(), null, false);
-    }
-    return null;
+  private Optional<AuthResult> auth(Optional<AccountState> account) {
+    return account.map(a -> new AuthResult(a.getAccount().getId(), null, false));
   }
 
   private AuthResult auth(Account.Id account) {
@@ -216,29 +213,29 @@
     }
   }
 
-  private AuthResult byPreferredEmail(String email) {
+  private Optional<AuthResult> byPreferredEmail(String email) {
     try (ReviewDb db = schema.open()) {
       Optional<AccountState> match =
           queryProvider.get().byPreferredEmail(email).stream().findFirst();
-      return match.isPresent() ? auth(match.get()) : null;
+      return auth(match);
     } catch (OrmException e) {
       getServletContext().log("cannot query database", e);
-      return null;
+      return Optional.empty();
     }
   }
 
-  private AuthResult byAccountId(String idStr) {
+  private Optional<AuthResult> byAccountId(String idStr) {
     final Account.Id id;
     try {
       id = Account.Id.parse(idStr);
     } catch (NumberFormatException nfe) {
-      return null;
+      return Optional.empty();
     }
     try {
       return auth(accounts.get(id));
     } catch (IOException | ConfigInvalidException e) {
       getServletContext().log("cannot query database", e);
-      return null;
+      return Optional.empty();
     }
   }
 
diff --git a/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index 497354d..43dddf9 100644
--- a/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -176,7 +175,7 @@
     ChangeResource rsrc;
     try {
       rsrc = changes.parse(changeId);
-    } catch (ResourceNotFoundException e) {
+    } catch (RestApiException e) {
       throw new IOException(e);
     }
     addProjectOwnersAsReviewers(rsrc);
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index e9f5cd5..1c27f5e 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.client.AuthType;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
@@ -45,7 +44,6 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Optional;
 import org.apache.commons.validator.routines.EmailValidator;
@@ -151,13 +149,7 @@
             authorizedKeys.save("Add SSH key for initial admin user\n");
           }
 
-          AccountState as =
-              new AccountState(
-                  new AllUsersName(allUsers.get()),
-                  a,
-                  extIds,
-                  new HashMap<>(),
-                  GeneralPreferencesInfo.defaults());
+          AccountState as = AccountState.forAccount(new AllUsersName(allUsers.get()), a, extIds);
           for (AccountIndex accountIndex : accountIndexCollection.getWriteIndexes()) {
             accountIndex.replace(as);
           }
diff --git a/java/com/google/gerrit/pgm/init/api/Section.java b/java/com/google/gerrit/pgm/init/api/Section.java
index c1c8745..009e989 100644
--- a/java/com/google/gerrit/pgm/init/api/Section.java
+++ b/java/com/google/gerrit/pgm/init/api/Section.java
@@ -23,6 +23,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.EnumSet;
+import java.util.Objects;
 import java.util.Set;
 
 /** Helper to edit a section of the configuration files. */
@@ -100,7 +101,7 @@
     if (nullIfDefault && nv.equals(dv)) {
       nv = null;
     }
-    if (!eq(ov, nv)) {
+    if (!Objects.equals(ov, nv)) {
       set(name, nv);
     }
     return nv;
@@ -148,7 +149,7 @@
   public String select(final String title, String name, String dv, Set<String> allowedValues) {
     final String ov = get(name);
     String nv = ui.readString(ov != null ? ov : dv, allowedValues, "%s", title);
-    if (!eq(ov, nv)) {
+    if (!Objects.equals(ov, nv)) {
       set(name, nv);
     }
     return nv;
@@ -177,7 +178,7 @@
     }
 
     final String nv = ui.password("%s's password", user);
-    if (!eq(ov, nv)) {
+    if (!Objects.equals(ov, nv)) {
       setSecure(password, nv);
     }
     return nv;
@@ -195,7 +196,7 @@
     }
 
     final String nv = ui.password("%s", prompt);
-    if (!eq(ov, nv)) {
+    if (!Objects.equals(ov, nv)) {
       setSecure(passwordKey, nv);
     }
     return nv;
@@ -216,11 +217,4 @@
   String getName() {
     return section;
   }
-
-  private static boolean eq(String a, String b) {
-    if (a == null && b == null) {
-      return true;
-    }
-    return a != null && a.equals(b);
-  }
 }
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index e910ffb..57e14d6 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -20,7 +20,6 @@
 import com.google.common.cache.LoadingCache;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -34,8 +33,6 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 import java.io.IOException;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -131,12 +128,7 @@
   private AccountState missing(Account.Id accountId) {
     Account account = new Account(accountId, TimeUtil.nowTs());
     account.setActive(false);
-    return new AccountState(
-        allUsersName,
-        account,
-        Collections.emptySet(),
-        new HashMap<>(),
-        GeneralPreferencesInfo.defaults());
+    return AccountState.forAccount(allUsersName, account);
   }
 
   static class ByIdLoader extends CacheLoader<Account.Id, Optional<AccountState>> {
@@ -149,7 +141,7 @@
 
     @Override
     public Optional<AccountState> load(Account.Id who) throws Exception {
-      return Optional.ofNullable(accounts.get(who));
+      return accounts.get(who);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/account/AccountConfig.java b/java/com/google/gerrit/server/account/AccountConfig.java
index b20aa0b..d4379a2 100644
--- a/java/com/google/gerrit/server/account/AccountConfig.java
+++ b/java/com/google/gerrit/server/account/AccountConfig.java
@@ -19,7 +19,11 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -33,6 +37,7 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -157,7 +162,7 @@
    *
    * @return the project watches of the loaded account
    */
-  public Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches() {
+  public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> getProjectWatches() {
     checkLoaded();
     return watchConfig.getProjectWatches();
   }
@@ -173,6 +178,26 @@
   }
 
   /**
+   * Get the diff preferences of the loaded account.
+   *
+   * @return the diff preferences of the loaded account
+   */
+  public DiffPreferencesInfo getDiffPreferences() {
+    checkLoaded();
+    return prefConfig.getDiffPreferences();
+  }
+
+  /**
+   * Get the edit preferences of the loaded account.
+   *
+   * @return the edit preferences of the loaded account
+   */
+  public EditPreferencesInfo getEditPreferences() {
+    checkLoaded();
+    return prefConfig.getEditPreferences();
+  }
+
+  /**
    * Sets the account. This means the loaded account will be overwritten with the given account.
    *
    * <p>Changing the registration date of an account is not supported.
@@ -237,10 +262,6 @@
       Config accountConfig = readConfig(ACCOUNT_CONFIG);
       loadedAccount = Optional.of(parse(accountConfig, revision.name()));
 
-      Ref externalIdsRef = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
-      externalIdsRev =
-          externalIdsRef != null ? Optional.of(externalIdsRef.getObjectId()) : Optional.empty();
-
       watchConfig = new WatchConfig(accountId, readConfig(WatchConfig.WATCH_CONFIG), this);
 
       prefConfig =
@@ -256,7 +277,16 @@
       }
     } else {
       loadedAccount = Optional.empty();
+
+      watchConfig = new WatchConfig(accountId, new Config(), this);
+
+      prefConfig =
+          new PreferencesConfig(
+              accountId, new Config(), PreferencesConfig.readDefaultConfig(repo), this);
     }
+
+    Ref externalIdsRef = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
+    externalIdsRev = Optional.ofNullable(externalIdsRef).map(Ref::getObjectId);
   }
 
   private Account parse(Config cfg, String metaId) {
@@ -302,7 +332,7 @@
 
     Config accountConfig = saveAccount();
     saveProjectWatches();
-    saveGeneralPreferences();
+    savePreferences();
 
     // metaId is set in the commit(MetaDataUpdate) method after the commit is created
     loadedAccount = Optional.of(parse(accountConfig, null));
@@ -334,7 +364,8 @@
     if (accountUpdate.isPresent()
         && (!accountUpdate.get().getDeletedProjectWatches().isEmpty()
             || !accountUpdate.get().getUpdatedProjectWatches().isEmpty())) {
-      Map<ProjectWatchKey, Set<NotifyType>> projectWatches = watchConfig.getProjectWatches();
+      Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
+          new HashMap<>(watchConfig.getProjectWatches());
       accountUpdate.get().getDeletedProjectWatches().forEach(pw -> projectWatches.remove(pw));
       accountUpdate
           .get()
@@ -344,12 +375,20 @@
     }
   }
 
-  private void saveGeneralPreferences() throws IOException, ConfigInvalidException {
-    if (accountUpdate.isPresent() && accountUpdate.get().getGeneralPreferences().isPresent()) {
-      saveConfig(
-          PreferencesConfig.PREFERENCES_CONFIG,
-          prefConfig.saveGeneralPreferences(accountUpdate.get().getGeneralPreferences().get()));
+  private void savePreferences() throws IOException, ConfigInvalidException {
+    if (!accountUpdate.isPresent()
+        || (!accountUpdate.get().getGeneralPreferences().isPresent()
+            && !accountUpdate.get().getDiffPreferences().isPresent()
+            && !accountUpdate.get().getEditPreferences().isPresent())) {
+      return;
     }
+
+    saveConfig(
+        PreferencesConfig.PREFERENCES_CONFIG,
+        prefConfig.saveGeneralPreferences(
+            accountUpdate.get().getGeneralPreferences(),
+            accountUpdate.get().getDiffPreferences(),
+            accountUpdate.get().getEditPreferences()));
   }
 
   /**
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index aac515d..8913ac0 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -49,6 +49,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
@@ -225,36 +226,30 @@
 
     if (!realm.allowsEdit(AccountFieldName.FULL_NAME)
         && !Strings.isNullOrEmpty(who.getDisplayName())
-        && !eq(user.getAccount().getFullName(), who.getDisplayName())) {
+        && !Objects.equals(user.getAccount().getFullName(), who.getDisplayName())) {
       accountUpdates.add(u -> u.setFullName(who.getDisplayName()));
     }
 
     if (!realm.allowsEdit(AccountFieldName.USER_NAME)
         && who.getUserName() != null
-        && !eq(user.getUserName(), who.getUserName())) {
+        && !Objects.equals(user.getUserName(), who.getUserName())) {
       log.warn(
           String.format(
               "Not changing already set username %s to %s", user.getUserName(), who.getUserName()));
     }
 
     if (!accountUpdates.isEmpty()) {
-      Account account =
-          accountsUpdateFactory
-              .create()
-              .update(
-                  "Update Account on Login",
-                  user.getAccountId(),
-                  AccountUpdater.joinConsumers(accountUpdates));
-      if (account == null) {
-        throw new OrmException("Account " + user.getAccountId() + " has been deleted");
-      }
+      accountsUpdateFactory
+          .create()
+          .update(
+              "Update Account on Login",
+              user.getAccountId(),
+              AccountUpdater.joinConsumers(accountUpdates))
+          .orElseThrow(
+              () -> new OrmException("Account " + user.getAccountId() + " has been deleted"));
     }
   }
 
-  private static boolean eq(String a, String b) {
-    return (a == null && b == null) || (a != null && a.equals(b));
-  }
-
   private AuthResult create(ReviewDb db, AuthRequest who)
       throws OrmException, AccountException, IOException, ConfigInvalidException {
     Account.Id newId = new Account.Id(sequences.nextAccountId());
@@ -266,9 +261,9 @@
 
     boolean isFirstAccount = awaitsFirstAccountCheck.getAndSet(false) && !accounts.hasAnyAccount();
 
-    Account account;
+    AccountState accountState;
     try {
-      account =
+      accountState =
           accountsUpdateFactory
               .create()
               .insert(
@@ -318,7 +313,7 @@
       addGroupMember(db, adminGroupUuid, user);
     }
 
-    realm.onCreateAccount(who, account);
+    realm.onCreateAccount(who, accountState.getAccount());
     return new AuthResult(newId, extId.key(), true);
   }
 
@@ -378,7 +373,7 @@
               (a, u) -> {
                 u.addExternalId(
                     ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress()));
-                if (who.getEmailAddress() != null && a.getPreferredEmail() == null) {
+                if (who.getEmailAddress() != null && a.getAccount().getPreferredEmail() == null) {
                   u.setPreferredEmail(who.getEmailAddress());
                 }
               });
@@ -401,23 +396,26 @@
    */
   public AuthResult updateLink(Account.Id to, AuthRequest who)
       throws OrmException, AccountException, IOException, ConfigInvalidException {
-    Collection<ExternalId> filteredExtIdsByScheme =
-        externalIds.byAccount(to, who.getExternalIdKey().scheme());
+    accountsUpdateFactory
+        .create()
+        .update(
+            "Delete External IDs on Update Link",
+            to,
+            (a, u) -> {
+              Collection<ExternalId> filteredExtIdsByScheme =
+                  a.getExternalIds(who.getExternalIdKey().scheme());
+              if (filteredExtIdsByScheme.isEmpty()) {
+                return;
+              }
 
-    if (!filteredExtIdsByScheme.isEmpty()
-        && (filteredExtIdsByScheme.size() > 1
-            || !filteredExtIdsByScheme
-                .stream()
-                .filter(e -> e.key().equals(who.getExternalIdKey()))
-                .findAny()
-                .isPresent())) {
-      accountsUpdateFactory
-          .create()
-          .update(
-              "Delete External IDs on Update Link",
-              to,
-              u -> u.deleteExternalIds(filteredExtIdsByScheme));
-    }
+              if (filteredExtIdsByScheme.size() > 1
+                  || !filteredExtIdsByScheme
+                      .stream()
+                      .anyMatch(e -> e.key().equals(who.getExternalIdKey()))) {
+                u.deleteExternalIds(filteredExtIdsByScheme);
+              }
+            });
+
     return link(to, who);
   }
 
@@ -468,8 +466,10 @@
             from,
             (a, u) -> {
               u.deleteExternalIds(extIds);
-              if (a.getPreferredEmail() != null
-                  && extIds.stream().anyMatch(e -> a.getPreferredEmail().equals(e.email()))) {
+              if (a.getAccount().getPreferredEmail() != null
+                  && extIds
+                      .stream()
+                      .anyMatch(e -> a.getAccount().getPreferredEmail().equals(e.email()))) {
                 u.setPreferredEmail(null);
               }
             });
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index a1cca06..ae2bee6 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static java.util.stream.Collectors.toSet;
 
+import com.google.common.collect.Streams;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gwtorm.server.OrmException;
@@ -95,18 +97,12 @@
     Matcher m = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(nameOrEmail);
     if (m.matches()) {
       Account.Id id = Account.Id.parse(m.group(1));
-      if (accounts.get(id) != null) {
-        return Collections.singleton(id);
-      }
-      return Collections.emptySet();
+      return Streams.stream(accounts.get(id)).map(a -> id).collect(toImmutableSet());
     }
 
     if (nameOrEmail.matches("^[1-9][0-9]*$")) {
       Account.Id id = Account.Id.parse(nameOrEmail);
-      if (accounts.get(id) != null) {
-        return Collections.singleton(id);
-      }
-      return Collections.emptySet();
+      return Streams.stream(accounts.get(id)).map(a -> id).collect(toImmutableSet());
     }
 
     if (nameOrEmail.matches(Account.USER_NAME_PATTERN)) {
diff --git a/java/com/google/gerrit/server/account/AccountState.java b/java/com/google/gerrit/server/account/AccountState.java
index 6601df7..8bd12c0 100644
--- a/java/com/google/gerrit/server/account/AccountState.java
+++ b/java/com/google/gerrit/server/account/AccountState.java
@@ -14,14 +14,19 @@
 
 package com.google.gerrit.server.account;
 
-import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 
 import com.google.common.base.Function;
 import com.google.common.base.Strings;
+import com.google.common.base.Suppliers;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser.PropertyKey;
@@ -29,18 +34,24 @@
 import com.google.gerrit.server.account.WatchConfig.NotifyType;
 import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdNotes;
+import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.config.AllUsersName;
+import java.io.IOException;
 import java.util.Collection;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
+import java.util.Optional;
+import java.util.function.Supplier;
 import org.apache.commons.codec.DecoderException;
+import org.eclipse.jgit.lib.ObjectId;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
  * Superset of all information related to an Account. This includes external IDs, project watches,
  * and properties from the account config file. AccountState maps one-to-one to Account.
+ *
+ * <p>Most callers should not construct AccountStates directly but rather lookup accounts via the
+ * account cache (see {@link AccountCache#get(Account.Id)}).
  */
 public class AccountState {
   private static final Logger logger = LoggerFactory.getLogger(AccountState.class);
@@ -48,25 +59,126 @@
   public static final Function<AccountState, Account.Id> ACCOUNT_ID_FUNCTION =
       a -> a.getAccount().getId();
 
+  /**
+   * Creates an AccountState from the given account config.
+   *
+   * @param allUsersName the name of the All-Users repository
+   * @param externalIds class to access external IDs
+   * @param accountConfig the account config, must already be loaded
+   * @return the account state, {@link Optional#empty()} if the account doesn't exist
+   * @throws IOException if accessing the external IDs fails
+   */
+  public static Optional<AccountState> fromAccountConfig(
+      AllUsersName allUsersName, ExternalIds externalIds, AccountConfig accountConfig)
+      throws IOException {
+    return fromAccountConfig(allUsersName, externalIds, accountConfig, null);
+  }
+
+  /**
+   * Creates an AccountState from the given account config.
+   *
+   * <p>If external ID notes are provided the revision of the external IDs branch from which the
+   * external IDs for the account should be loaded is taken from the external ID notes. If external
+   * ID notes are not given the revision of the external IDs branch is taken from the account
+   * config. Updating external IDs is done via {@link ExternalIdNotes} and if external IDs were
+   * updated the revision of the external IDs branch in account config is outdated. Hence after
+   * updating external IDs the external ID notes must be provided.
+   *
+   * @param allUsersName the name of the All-Users repository
+   * @param externalIds class to access external IDs
+   * @param accountConfig the account config, must already be loaded
+   * @param extIdNotes external ID notes, must already be loaded, may be {@code null}
+   * @return the account state, {@link Optional#empty()} if the account doesn't exist
+   * @throws IOException if accessing the external IDs fails
+   */
+  public static Optional<AccountState> fromAccountConfig(
+      AllUsersName allUsersName,
+      ExternalIds externalIds,
+      AccountConfig accountConfig,
+      @Nullable ExternalIdNotes extIdNotes)
+      throws IOException {
+    if (!accountConfig.getLoadedAccount().isPresent()) {
+      return Optional.empty();
+    }
+    Account account = accountConfig.getLoadedAccount().get();
+
+    Optional<ObjectId> extIdsRev =
+        extIdNotes != null
+            ? Optional.ofNullable(extIdNotes.getRevision())
+            : accountConfig.getExternalIdsRev();
+    ImmutableSet<ExternalId> extIds =
+        extIdsRev.isPresent()
+            ? externalIds.byAccount(account.getId(), extIdsRev.get())
+            : ImmutableSet.of();
+
+    return Optional.of(
+        new AccountState(
+            allUsersName,
+            account,
+            extIds,
+            Suppliers.memoize(() -> accountConfig.getProjectWatches()),
+            Suppliers.memoize(() -> accountConfig.getGeneralPreferences()),
+            Suppliers.memoize(() -> accountConfig.getDiffPreferences()),
+            Suppliers.memoize(() -> accountConfig.getEditPreferences())));
+  }
+
+  /**
+   * Creates an AccountState for a given account with no external IDs, no project watches and
+   * default preferences.
+   *
+   * @param allUsersName the name of the All-Users repository
+   * @param account the account
+   * @return the account state
+   */
+  public static AccountState forAccount(AllUsersName allUsersName, Account account) {
+    return forAccount(allUsersName, account, ImmutableSet.of());
+  }
+
+  /**
+   * Creates an AccountState for a given account with no project watches and default preferences.
+   *
+   * @param allUsersName the name of the All-Users repository
+   * @param account the account
+   * @param extIds the external IDs
+   * @return the account state
+   */
+  public static AccountState forAccount(
+      AllUsersName allUsersName, Account account, Collection<ExternalId> extIds) {
+    return new AccountState(
+        allUsersName,
+        account,
+        ImmutableSet.copyOf(extIds),
+        Suppliers.ofInstance(ImmutableMap.of()),
+        Suppliers.ofInstance(GeneralPreferencesInfo.defaults()),
+        Suppliers.ofInstance(DiffPreferencesInfo.defaults()),
+        Suppliers.ofInstance(EditPreferencesInfo.defaults()));
+  }
+
   private final AllUsersName allUsersName;
   private final Account account;
-  private final Collection<ExternalId> externalIds;
-  private final Map<ProjectWatchKey, Set<NotifyType>> projectWatches;
-  private final GeneralPreferencesInfo generalPreferences;
+  private final ImmutableSet<ExternalId> externalIds;
+  private final Supplier<ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>>> projectWatches;
+  private final Supplier<GeneralPreferencesInfo> generalPreferences;
+  private final Supplier<DiffPreferencesInfo> diffPreferences;
+  private final Supplier<EditPreferencesInfo> editPreferences;
   private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
 
-  public AccountState(
+  private AccountState(
       AllUsersName allUsersName,
       Account account,
-      Collection<ExternalId> externalIds,
-      Map<ProjectWatchKey, Set<NotifyType>> projectWatches,
-      GeneralPreferencesInfo generalPreferences) {
+      ImmutableSet<ExternalId> externalIds,
+      Supplier<ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>>> projectWatches,
+      Supplier<GeneralPreferencesInfo> generalPreferences,
+      Supplier<DiffPreferencesInfo> diffPreferences,
+      Supplier<EditPreferencesInfo> editPreferences) {
     this.allUsersName = allUsersName;
     this.account = account;
     this.externalIds = externalIds;
     this.projectWatches = projectWatches;
     this.generalPreferences = generalPreferences;
-    this.account.setUserName(getUserName(externalIds));
+    this.diffPreferences = diffPreferences;
+    this.editPreferences = editPreferences;
+    this.account.setUserName(ExternalId.getUserName(externalIds).orElse(null));
   }
 
   public AllUsersName getAllUsersNameForIndexing() {
@@ -112,37 +224,33 @@
   }
 
   /** The external identities that identify the account holder. */
-  public Collection<ExternalId> getExternalIds() {
+  public ImmutableSet<ExternalId> getExternalIds() {
     return externalIds;
   }
 
+  /** The external identities that identify the account holder that match the given scheme. */
+  public ImmutableSet<ExternalId> getExternalIds(String scheme) {
+    return externalIds.stream().filter(e -> e.key().isScheme(scheme)).collect(toImmutableSet());
+  }
+
   /** The project watches of the account. */
-  public Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches() {
-    return projectWatches;
+  public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> getProjectWatches() {
+    return projectWatches.get();
   }
 
   /** The general preferences of the account. */
   public GeneralPreferencesInfo getGeneralPreferences() {
-    return generalPreferences;
+    return generalPreferences.get();
   }
 
-  public static String getUserName(Collection<ExternalId> ids) {
-    for (ExternalId extId : ids) {
-      if (extId.isScheme(SCHEME_USERNAME)) {
-        return extId.key().id();
-      }
-    }
-    return null;
+  /** The diff preferences of the account. */
+  public DiffPreferencesInfo getDiffPreferences() {
+    return diffPreferences.get();
   }
 
-  public static Set<String> getEmails(Collection<ExternalId> ids) {
-    Set<String> emails = new HashSet<>();
-    for (ExternalId extId : ids) {
-      if (extId.isScheme(SCHEME_MAILTO)) {
-        emails.add(extId.key().id());
-      }
-    }
-    return emails;
+  /** The edit preferences of the account. */
+  public EditPreferencesInfo getEditPreferences() {
+    return editPreferences.get();
   }
 
   /**
diff --git a/java/com/google/gerrit/server/account/Accounts.java b/java/com/google/gerrit/server/account/Accounts.java
index 45831ae..f72c9c3 100644
--- a/java/com/google/gerrit/server/account/Accounts.java
+++ b/java/com/google/gerrit/server/account/Accounts.java
@@ -18,8 +18,6 @@
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
-import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -56,10 +54,10 @@
     this.externalIds = externalIds;
   }
 
-  @Nullable
-  public AccountState get(Account.Id accountId) throws IOException, ConfigInvalidException {
+  public Optional<AccountState> get(Account.Id accountId)
+      throws IOException, ConfigInvalidException {
     try (Repository repo = repoManager.openRepository(allUsersName)) {
-      return read(repo, accountId).orElse(null);
+      return read(repo, accountId);
     }
   }
 
@@ -136,20 +134,8 @@
 
   private Optional<AccountState> read(Repository allUsersRepository, Account.Id accountId)
       throws IOException, ConfigInvalidException {
-    AccountConfig accountConfig = new AccountConfig(accountId, allUsersRepository).load();
-    if (!accountConfig.getLoadedAccount().isPresent()) {
-      return Optional.empty();
-    }
-    Account account = accountConfig.getLoadedAccount().get();
-    return Optional.of(
-        new AccountState(
-            allUsersName,
-            account,
-            accountConfig.getExternalIdsRev().isPresent()
-                ? externalIds.byAccount(accountId, accountConfig.getExternalIdsRev().get())
-                : ImmutableSet.of(),
-            accountConfig.getProjectWatches(),
-            accountConfig.getGeneralPreferences()));
+    return AccountState.fromAccountConfig(
+        allUsersName, externalIds, new AccountConfig(accountId, allUsersRepository).load());
   }
 
   public static Stream<Account.Id> readUserRefs(Repository repo) throws IOException {
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index fb8067c..5221a44 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes.ExternalIdNotesLoader;
+import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -48,6 +49,7 @@
 import java.util.function.Consumer;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
@@ -82,10 +84,10 @@
      * <p>Use the provided account only to read the current state of the account. Don't do updates
      * to the account. For updates use the provided account update builder.
      *
-     * @param account the account that is being updated
+     * @param accountState the account that is being updated
      * @param update account update builder
      */
-    void update(Account account, InternalAccountUpdate.Builder update);
+    void update(AccountState accountState, InternalAccountUpdate.Builder update);
 
     static AccountUpdater join(List<AccountUpdater> updaters) {
       return (a, u) -> updaters.stream().forEach(updater -> updater.update(a, u));
@@ -111,6 +113,7 @@
     private final GitRepositoryManager repoManager;
     private final GitReferenceUpdated gitRefUpdated;
     private final AllUsersName allUsersName;
+    private final ExternalIds externalIds;
     private final Provider<PersonIdent> serverIdentProvider;
     private final Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
     private final RetryHelper retryHelper;
@@ -121,6 +124,7 @@
         GitRepositoryManager repoManager,
         GitReferenceUpdated gitRefUpdated,
         AllUsersName allUsersName,
+        ExternalIds externalIds,
         @GerritPersonIdent Provider<PersonIdent> serverIdentProvider,
         Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
         RetryHelper retryHelper,
@@ -128,6 +132,7 @@
       this.repoManager = repoManager;
       this.gitRefUpdated = gitRefUpdated;
       this.allUsersName = allUsersName;
+      this.externalIds = externalIds;
       this.serverIdentProvider = serverIdentProvider;
       this.metaDataUpdateInternalFactory = metaDataUpdateInternalFactory;
       this.retryHelper = retryHelper;
@@ -141,6 +146,7 @@
           gitRefUpdated,
           null,
           allUsersName,
+          externalIds,
           metaDataUpdateInternalFactory,
           retryHelper,
           extIdNotesFactory,
@@ -163,6 +169,7 @@
     private final GitRepositoryManager repoManager;
     private final GitReferenceUpdated gitRefUpdated;
     private final AllUsersName allUsersName;
+    private final ExternalIds externalIds;
     private final Provider<PersonIdent> serverIdentProvider;
     private final Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
     private final RetryHelper retryHelper;
@@ -173,6 +180,7 @@
         GitRepositoryManager repoManager,
         GitReferenceUpdated gitRefUpdated,
         AllUsersName allUsersName,
+        ExternalIds externalIds,
         @GerritPersonIdent Provider<PersonIdent> serverIdentProvider,
         Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
         RetryHelper retryHelper,
@@ -180,6 +188,7 @@
       this.repoManager = repoManager;
       this.gitRefUpdated = gitRefUpdated;
       this.allUsersName = allUsersName;
+      this.externalIds = externalIds;
       this.serverIdentProvider = serverIdentProvider;
       this.metaDataUpdateInternalFactory = metaDataUpdateInternalFactory;
       this.retryHelper = retryHelper;
@@ -193,6 +202,7 @@
           gitRefUpdated,
           null,
           allUsersName,
+          externalIds,
           metaDataUpdateInternalFactory,
           retryHelper,
           extIdNotesFactory,
@@ -212,6 +222,7 @@
     private final GitRepositoryManager repoManager;
     private final GitReferenceUpdated gitRefUpdated;
     private final AllUsersName allUsersName;
+    private final ExternalIds externalIds;
     private final Provider<PersonIdent> serverIdentProvider;
     private final Provider<IdentifiedUser> identifiedUser;
     private final Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
@@ -223,6 +234,7 @@
         GitRepositoryManager repoManager,
         GitReferenceUpdated gitRefUpdated,
         AllUsersName allUsersName,
+        ExternalIds externalIds,
         @GerritPersonIdent Provider<PersonIdent> serverIdentProvider,
         Provider<IdentifiedUser> identifiedUser,
         Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
@@ -231,6 +243,7 @@
       this.repoManager = repoManager;
       this.gitRefUpdated = gitRefUpdated;
       this.allUsersName = allUsersName;
+      this.externalIds = externalIds;
       this.serverIdentProvider = serverIdentProvider;
       this.identifiedUser = identifiedUser;
       this.metaDataUpdateInternalFactory = metaDataUpdateInternalFactory;
@@ -247,6 +260,7 @@
           gitRefUpdated,
           user,
           allUsersName,
+          externalIds,
           metaDataUpdateInternalFactory,
           retryHelper,
           extIdNotesFactory,
@@ -263,18 +277,25 @@
   private final GitReferenceUpdated gitRefUpdated;
   @Nullable private final IdentifiedUser currentUser;
   private final AllUsersName allUsersName;
+  private final ExternalIds externalIds;
   private final Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
   private final RetryHelper retryHelper;
   private final ExternalIdNotesLoader extIdNotesLoader;
   private final PersonIdent committerIdent;
   private final PersonIdent authorIdent;
+
+  // Invoked after reading the account config.
   private final Runnable afterReadRevision;
 
+  // Invoked after updating the account but before committing the changes.
+  private final Runnable beforeCommit;
+
   private AccountsUpdate(
       GitRepositoryManager repoManager,
       GitReferenceUpdated gitRefUpdated,
       @Nullable IdentifiedUser currentUser,
       AllUsersName allUsersName,
+      ExternalIds externalIds,
       Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
       RetryHelper retryHelper,
       ExternalIdNotesLoader extIdNotesLoader,
@@ -285,11 +306,13 @@
         gitRefUpdated,
         currentUser,
         allUsersName,
+        externalIds,
         metaDataUpdateInternalFactory,
         retryHelper,
         extIdNotesLoader,
         committerIdent,
         authorIdent,
+        Runnables.doNothing(),
         Runnables.doNothing());
   }
 
@@ -299,23 +322,27 @@
       GitReferenceUpdated gitRefUpdated,
       @Nullable IdentifiedUser currentUser,
       AllUsersName allUsersName,
+      ExternalIds externalIds,
       Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
       RetryHelper retryHelper,
       ExternalIdNotesLoader extIdNotesLoader,
       PersonIdent committerIdent,
       PersonIdent authorIdent,
-      Runnable afterReadRevision) {
+      Runnable afterReadRevision,
+      Runnable beforeCommit) {
     this.repoManager = checkNotNull(repoManager, "repoManager");
     this.gitRefUpdated = checkNotNull(gitRefUpdated, "gitRefUpdated");
     this.currentUser = currentUser;
     this.allUsersName = checkNotNull(allUsersName, "allUsersName");
+    this.externalIds = checkNotNull(externalIds, "externalIds");
     this.metaDataUpdateInternalFactory =
         checkNotNull(metaDataUpdateInternalFactory, "metaDataUpdateInternalFactory");
     this.retryHelper = checkNotNull(retryHelper, "retryHelper");
     this.extIdNotesLoader = checkNotNull(extIdNotesLoader, "extIdNotesLoader");
     this.committerIdent = checkNotNull(committerIdent, "committerIdent");
     this.authorIdent = checkNotNull(authorIdent, "authorIdent");
-    this.afterReadRevision = afterReadRevision;
+    this.afterReadRevision = checkNotNull(afterReadRevision, "afterReadRevision");
+    this.beforeCommit = checkNotNull(beforeCommit, "beforeCommit");
   }
 
   /**
@@ -330,7 +357,7 @@
    * @throws OrmException if creating the user branch fails
    * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
-  public Account insert(
+  public AccountState insert(
       String message, Account.Id accountId, Consumer<InternalAccountUpdate.Builder> init)
       throws OrmException, IOException, ConfigInvalidException {
     return insert(message, accountId, AccountUpdater.fromConsumer(init));
@@ -348,23 +375,27 @@
    * @throws OrmException if creating the user branch fails
    * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
-  public Account insert(String message, Account.Id accountId, AccountUpdater updater)
+  public AccountState insert(String message, Account.Id accountId, AccountUpdater updater)
       throws OrmException, IOException, ConfigInvalidException {
     return updateAccount(
-        r -> {
-          AccountConfig accountConfig = read(r, accountId);
-          Account account =
-              accountConfig.getNewAccount(new Timestamp(committerIdent.getWhen().getTime()));
-          InternalAccountUpdate.Builder updateBuilder = InternalAccountUpdate.builder();
-          updater.update(account, updateBuilder);
+            r -> {
+              AccountConfig accountConfig = read(r, accountId);
+              Account account =
+                  accountConfig.getNewAccount(new Timestamp(committerIdent.getWhen().getTime()));
+              AccountState accountState = AccountState.forAccount(allUsersName, account);
+              InternalAccountUpdate.Builder updateBuilder = InternalAccountUpdate.builder();
+              updater.update(accountState, updateBuilder);
 
-          InternalAccountUpdate update = updateBuilder.build();
-          accountConfig.setAccountUpdate(update);
-          ExternalIdNotes extIdNotes = createExternalIdNotes(r, accountId, update);
-          UpdatedAccount updatedAccounts = new UpdatedAccount(message, accountConfig, extIdNotes);
-          updatedAccounts.setCreated(true);
-          return updatedAccounts;
-        });
+              InternalAccountUpdate update = updateBuilder.build();
+              accountConfig.setAccountUpdate(update);
+              ExternalIdNotes extIdNotes =
+                  createExternalIdNotes(r, accountConfig.getExternalIdsRev(), accountId, update);
+              UpdatedAccount updatedAccounts =
+                  new UpdatedAccount(allUsersName, externalIds, message, accountConfig, extIdNotes);
+              updatedAccounts.setCreated(true);
+              return updatedAccounts;
+            })
+        .get();
   }
 
   /**
@@ -375,12 +406,12 @@
    * @param message commit message for the account update, must not be {@code null or empty}
    * @param accountId ID of the account
    * @param update consumer to update the account, only invoked if the account exists
-   * @return the updated account, {@code null} if the account doesn't exist
+   * @return the updated account, {@link Optional#empty()} if the account doesn't exist
    * @throws IOException if updating the user branch fails due to an IO error
    * @throws OrmException if updating the user branch fails
    * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
-  public Account update(
+  public Optional<AccountState> update(
       String message, Account.Id accountId, Consumer<InternalAccountUpdate.Builder> update)
       throws OrmException, IOException, ConfigInvalidException {
     return update(message, accountId, AccountUpdater.fromConsumer(update));
@@ -394,18 +425,18 @@
    * @param message commit message for the account update, must not be {@code null or empty}
    * @param accountId ID of the account
    * @param updater updater to update the account, only invoked if the account exists
-   * @return the updated account, {@code null} if the account doesn't exist
+   * @return the updated account, {@link Optional#empty} if the account doesn't exist
    * @throws IOException if updating the user branch fails due to an IO error
    * @throws OrmException if updating the user branch fails
    * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
-  @Nullable
-  public Account update(String message, Account.Id accountId, AccountUpdater updater)
+  public Optional<AccountState> update(String message, Account.Id accountId, AccountUpdater updater)
       throws OrmException, IOException, ConfigInvalidException {
     return updateAccount(
         r -> {
           AccountConfig accountConfig = read(r, accountId);
-          Optional<Account> account = accountConfig.getLoadedAccount();
+          Optional<AccountState> account =
+              AccountState.fromAccountConfig(allUsersName, externalIds, accountConfig);
           if (!account.isPresent()) {
             return null;
           }
@@ -415,8 +446,10 @@
 
           InternalAccountUpdate update = updateBuilder.build();
           accountConfig.setAccountUpdate(update);
-          ExternalIdNotes extIdNotes = createExternalIdNotes(r, accountId, update);
-          UpdatedAccount updatedAccounts = new UpdatedAccount(message, accountConfig, extIdNotes);
+          ExternalIdNotes extIdNotes =
+              createExternalIdNotes(r, accountConfig.getExternalIdsRev(), accountId, update);
+          UpdatedAccount updatedAccounts =
+              new UpdatedAccount(allUsersName, externalIds, message, accountConfig, extIdNotes);
           return updatedAccounts;
         });
   }
@@ -428,7 +461,7 @@
     return accountConfig;
   }
 
-  private Account updateAccount(AccountUpdate accountUpdate)
+  private Optional<AccountState> updateAccount(AccountUpdate accountUpdate)
       throws IOException, ConfigInvalidException, OrmException {
     return retryHelper.execute(
         ActionType.ACCOUNT_UPDATE,
@@ -436,17 +469,20 @@
           try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
             UpdatedAccount updatedAccount = accountUpdate.update(allUsersRepo);
             if (updatedAccount == null) {
-              return null;
+              return Optional.empty();
             }
 
             commit(allUsersRepo, updatedAccount);
-            return updatedAccount.getAccount();
+            return Optional.of(updatedAccount.getAccount());
           }
         });
   }
 
   private ExternalIdNotes createExternalIdNotes(
-      Repository allUsersRepo, Account.Id accountId, InternalAccountUpdate update)
+      Repository allUsersRepo,
+      Optional<ObjectId> rev,
+      Account.Id accountId,
+      InternalAccountUpdate update)
       throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
     ExternalIdNotes.checkSameAccount(
         Iterables.concat(
@@ -455,14 +491,17 @@
             update.getDeletedExternalIds()),
         accountId);
 
-    ExternalIdNotes extIdNotes = extIdNotesLoader.load(allUsersRepo);
+    ExternalIdNotes extIdNotes = extIdNotesLoader.load(allUsersRepo, rev.orElse(ObjectId.zeroId()));
     extIdNotes.replace(update.getDeletedExternalIds(), update.getCreatedExternalIds());
     extIdNotes.upsert(update.getUpdatedExternalIds());
     return extIdNotes;
   }
 
   private void commit(Repository allUsersRepo, UpdatedAccount updatedAccount) throws IOException {
+    beforeCommit.run();
+
     BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
+
     if (updatedAccount.isCreated()) {
       commitNewAccountConfig(
           updatedAccount.getMessage(),
@@ -482,6 +521,7 @@
         allUsersRepo,
         batchRefUpdate,
         updatedAccount.getExternalIdNotes());
+
     RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
     updatedAccount.getExternalIdNotes().updateCaches();
     gitRefUpdated.fire(
@@ -554,6 +594,8 @@
   }
 
   private static class UpdatedAccount {
+    private final AllUsersName allUsersName;
+    private final ExternalIds externalIds;
     private final String message;
     private final AccountConfig accountConfig;
     private final ExternalIdNotes extIdNotes;
@@ -561,8 +603,14 @@
     private boolean created;
 
     private UpdatedAccount(
-        String message, AccountConfig accountConfig, ExternalIdNotes extIdNotes) {
+        AllUsersName allUsersName,
+        ExternalIds externalIds,
+        String message,
+        AccountConfig accountConfig,
+        ExternalIdNotes extIdNotes) {
       checkState(!Strings.isNullOrEmpty(message), "message for account update must be set");
+      this.allUsersName = checkNotNull(allUsersName);
+      this.externalIds = checkNotNull(externalIds);
       this.message = checkNotNull(message);
       this.accountConfig = checkNotNull(accountConfig);
       this.extIdNotes = checkNotNull(extIdNotes);
@@ -576,8 +624,9 @@
       return accountConfig;
     }
 
-    public Account getAccount() {
-      return accountConfig.getLoadedAccount().get();
+    public AccountState getAccount() throws IOException {
+      return AccountState.fromAccountConfig(allUsersName, externalIds, accountConfig, extIdNotes)
+          .get();
     }
 
     public ExternalIdNotes getExternalIdNotes() {
diff --git a/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index e7ff314..4aea478 100644
--- a/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.account;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -95,7 +97,7 @@
       info.secondaryEmails = externalIds != null ? getSecondaryEmails(account, externalIds) : null;
     }
     if (options.contains(FillOptions.USERNAME)) {
-      info.username = externalIds != null ? AccountState.getUserName(externalIds) : null;
+      info.username = externalIds != null ? ExternalId.getUserName(externalIds).orElse(null) : null;
     }
 
     if (options.contains(FillOptions.STATUS)) {
@@ -125,12 +127,10 @@
   }
 
   public List<String> getSecondaryEmails(Account account, Collection<ExternalId> externalIds) {
-    List<String> emails = new ArrayList<>(AccountState.getEmails(externalIds));
-    if (account.getPreferredEmail() != null) {
-      emails.remove(account.getPreferredEmail());
-    }
-    Collections.sort(emails);
-    return emails;
+    return ExternalId.getEmails(externalIds)
+        .filter(e -> !e.equals(account.getPreferredEmail()))
+        .sorted()
+        .collect(toList());
   }
 
   private static void addAvatar(
diff --git a/java/com/google/gerrit/server/account/InternalAccountUpdate.java b/java/com/google/gerrit/server/account/InternalAccountUpdate.java
index 05c431e..c7fa309 100644
--- a/java/com/google/gerrit/server/account/InternalAccountUpdate.java
+++ b/java/com/google/gerrit/server/account/InternalAccountUpdate.java
@@ -18,6 +18,8 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.WatchConfig.NotifyType;
@@ -123,6 +125,26 @@
   public abstract Optional<GeneralPreferencesInfo> getGeneralPreferences();
 
   /**
+   * Returns the new value for the diff preferences.
+   *
+   * <p>Only preferences that are non-null in the returned DiffPreferencesInfo should be updated.
+   *
+   * @return the new value for the diff preferences, {@code Optional#empty()} if the diff
+   *     preferences are not being updated, the wrapped value is never {@code null}
+   */
+  public abstract Optional<DiffPreferencesInfo> getDiffPreferences();
+
+  /**
+   * Returns the new value for the edit preferences.
+   *
+   * <p>Only preferences that are non-null in the returned DiffPreferencesInfo should be updated.
+   *
+   * @return the new value for the edit preferences, {@code Optional#empty()} if the edit
+   *     preferences are not being updated, the wrapped value is never {@code null}
+   */
+  public abstract Optional<EditPreferencesInfo> getEditPreferences();
+
+  /**
    * Class to build an account update.
    *
    * <p>Account data is only updated if the corresponding setter is invoked. If a setter is not
@@ -385,6 +407,26 @@
     public abstract Builder setGeneralPreferences(GeneralPreferencesInfo generalPreferences);
 
     /**
+     * Sets the diff preferences for the account.
+     *
+     * <p>Updates any preference that is non-null in the provided DiffPreferencesInfo.
+     *
+     * @param diffPreferences the diff preferences that should be set
+     * @return the builder
+     */
+    public abstract Builder setDiffPreferences(DiffPreferencesInfo diffPreferences);
+
+    /**
+     * Sets the edit preferences for the account.
+     *
+     * <p>Updates any preference that is non-null in the provided EditPreferencesInfo.
+     *
+     * @param editPreferences the edit preferences that should be set
+     * @return the builder
+     */
+    public abstract Builder setEditPreferences(EditPreferencesInfo editPreferences);
+
+    /**
      * Builds the account update.
      *
      * @return the account update
@@ -520,6 +562,18 @@
         delegate.setGeneralPreferences(generalPreferences);
         return this;
       }
+
+      @Override
+      public Builder setDiffPreferences(DiffPreferencesInfo diffPreferences) {
+        delegate.setDiffPreferences(diffPreferences);
+        return this;
+      }
+
+      @Override
+      public Builder setEditPreferences(EditPreferencesInfo editPreferences) {
+        delegate.setEditPreferences(editPreferences);
+        return this;
+      }
     }
   }
 }
diff --git a/java/com/google/gerrit/server/account/PreferencesConfig.java b/java/com/google/gerrit/server/account/PreferencesConfig.java
index 32df659..c471139 100644
--- a/java/com/google/gerrit/server/account/PreferencesConfig.java
+++ b/java/com/google/gerrit/server/account/PreferencesConfig.java
@@ -31,6 +31,8 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.MenuItem;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -47,6 +49,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
@@ -65,6 +68,8 @@
   private final ValidationError.Sink validationErrorSink;
 
   private GeneralPreferencesInfo generalPreferences;
+  private DiffPreferencesInfo diffPreferences;
+  private EditPreferencesInfo editPreferences;
 
   public PreferencesConfig(
       Account.Id accountId,
@@ -84,29 +89,85 @@
     return generalPreferences;
   }
 
-  public void parse() {
-    generalPreferences = parse(null);
+  public DiffPreferencesInfo getDiffPreferences() {
+    if (diffPreferences == null) {
+      parse();
+    }
+    return diffPreferences;
   }
 
-  public Config saveGeneralPreferences(GeneralPreferencesInfo input) throws ConfigInvalidException {
-    // merge configs
-    input = parse(input);
+  public EditPreferencesInfo getEditPreferences() {
+    if (editPreferences == null) {
+      parse();
+    }
+    return editPreferences;
+  }
 
-    storeSection(
-        cfg, UserConfigSections.GENERAL, null, input, parseDefaultPreferences(defaultCfg, null));
-    setChangeTable(cfg, input.changeTable);
-    setMy(cfg, input.my);
-    setUrlAliases(cfg, input.urlAliases);
+  public void parse() {
+    generalPreferences = parseGeneralPreferences(null);
+    diffPreferences = parseDiffPreferences(null);
+    editPreferences = parseEditPreferences(null);
+  }
 
-    // evict the cached general preferences
-    this.generalPreferences = null;
+  public Config saveGeneralPreferences(
+      Optional<GeneralPreferencesInfo> generalPreferencesInput,
+      Optional<DiffPreferencesInfo> diffPreferencesInput,
+      Optional<EditPreferencesInfo> editPreferencesInput)
+      throws ConfigInvalidException {
+    if (generalPreferencesInput.isPresent()) {
+      GeneralPreferencesInfo mergedGeneralPreferencesInput =
+          parseGeneralPreferences(generalPreferencesInput.get());
+
+      storeSection(
+          cfg,
+          UserConfigSections.GENERAL,
+          null,
+          mergedGeneralPreferencesInput,
+          parseDefaultGeneralPreferences(defaultCfg, null));
+      setChangeTable(cfg, mergedGeneralPreferencesInput.changeTable);
+      setMy(cfg, mergedGeneralPreferencesInput.my);
+      setUrlAliases(cfg, mergedGeneralPreferencesInput.urlAliases);
+
+      // evict the cached general preferences
+      this.generalPreferences = null;
+    }
+
+    if (diffPreferencesInput.isPresent()) {
+      DiffPreferencesInfo mergedDiffPreferencesInput =
+          parseDiffPreferences(diffPreferencesInput.get());
+
+      storeSection(
+          cfg,
+          UserConfigSections.DIFF,
+          null,
+          mergedDiffPreferencesInput,
+          parseDefaultDiffPreferences(defaultCfg, null));
+
+      // evict the cached diff preferences
+      this.diffPreferences = null;
+    }
+
+    if (editPreferencesInput.isPresent()) {
+      EditPreferencesInfo mergedEditPreferencesInput =
+          parseEditPreferences(editPreferencesInput.get());
+
+      storeSection(
+          cfg,
+          UserConfigSections.EDIT,
+          null,
+          mergedEditPreferencesInput,
+          parseDefaultEditPreferences(defaultCfg, null));
+
+      // evict the cached edit preferences
+      this.editPreferences = null;
+    }
 
     return cfg;
   }
 
-  private GeneralPreferencesInfo parse(@Nullable GeneralPreferencesInfo input) {
+  private GeneralPreferencesInfo parseGeneralPreferences(@Nullable GeneralPreferencesInfo input) {
     try {
-      return parse(cfg, defaultCfg, input);
+      return parseGeneralPreferences(cfg, defaultCfg, input);
     } catch (ConfigInvalidException e) {
       validationErrorSink.error(
           new ValidationError(
@@ -118,7 +179,33 @@
     }
   }
 
-  private static GeneralPreferencesInfo parse(
+  private DiffPreferencesInfo parseDiffPreferences(@Nullable DiffPreferencesInfo input) {
+    try {
+      return parseDiffPreferences(cfg, defaultCfg, input);
+    } catch (ConfigInvalidException e) {
+      validationErrorSink.error(
+          new ValidationError(
+              PREFERENCES_CONFIG,
+              String.format(
+                  "Invalid diff preferences for account %d: %s", accountId.get(), e.getMessage())));
+      return new DiffPreferencesInfo();
+    }
+  }
+
+  private EditPreferencesInfo parseEditPreferences(@Nullable EditPreferencesInfo input) {
+    try {
+      return parseEditPreferences(cfg, defaultCfg, input);
+    } catch (ConfigInvalidException e) {
+      validationErrorSink.error(
+          new ValidationError(
+              PREFERENCES_CONFIG,
+              String.format(
+                  "Invalid edit preferences for account %d: %s", accountId.get(), e.getMessage())));
+      return new EditPreferencesInfo();
+    }
+  }
+
+  private static GeneralPreferencesInfo parseGeneralPreferences(
       Config cfg, @Nullable Config defaultCfg, @Nullable GeneralPreferencesInfo input)
       throws ConfigInvalidException {
     GeneralPreferencesInfo r =
@@ -128,7 +215,7 @@
             null,
             new GeneralPreferencesInfo(),
             defaultCfg != null
-                ? parseDefaultPreferences(defaultCfg, input)
+                ? parseDefaultGeneralPreferences(defaultCfg, input)
                 : GeneralPreferencesInfo.defaults(),
             input);
     if (input != null) {
@@ -143,7 +230,35 @@
     return r;
   }
 
-  private static GeneralPreferencesInfo parseDefaultPreferences(
+  private static DiffPreferencesInfo parseDiffPreferences(
+      Config cfg, @Nullable Config defaultCfg, @Nullable DiffPreferencesInfo input)
+      throws ConfigInvalidException {
+    return loadSection(
+        cfg,
+        UserConfigSections.DIFF,
+        null,
+        new DiffPreferencesInfo(),
+        defaultCfg != null
+            ? parseDefaultDiffPreferences(defaultCfg, input)
+            : DiffPreferencesInfo.defaults(),
+        input);
+  }
+
+  private static EditPreferencesInfo parseEditPreferences(
+      Config cfg, @Nullable Config defaultCfg, @Nullable EditPreferencesInfo input)
+      throws ConfigInvalidException {
+    return loadSection(
+        cfg,
+        UserConfigSections.EDIT,
+        null,
+        new EditPreferencesInfo(),
+        defaultCfg != null
+            ? parseDefaultEditPreferences(defaultCfg, input)
+            : EditPreferencesInfo.defaults(),
+        input);
+  }
+
+  private static GeneralPreferencesInfo parseDefaultGeneralPreferences(
       Config defaultCfg, GeneralPreferencesInfo input) throws ConfigInvalidException {
     GeneralPreferencesInfo allUserPrefs = new GeneralPreferencesInfo();
     loadSection(
@@ -153,10 +268,37 @@
         allUserPrefs,
         GeneralPreferencesInfo.defaults(),
         input);
-    return updateDefaults(allUserPrefs);
+    return updateGeneralPreferencesDefaults(allUserPrefs);
   }
 
-  private static GeneralPreferencesInfo updateDefaults(GeneralPreferencesInfo input) {
+  private static DiffPreferencesInfo parseDefaultDiffPreferences(
+      Config defaultCfg, DiffPreferencesInfo input) throws ConfigInvalidException {
+    DiffPreferencesInfo allUserPrefs = new DiffPreferencesInfo();
+    loadSection(
+        defaultCfg,
+        UserConfigSections.DIFF,
+        null,
+        allUserPrefs,
+        DiffPreferencesInfo.defaults(),
+        input);
+    return updateDiffPreferencesDefaults(allUserPrefs);
+  }
+
+  private static EditPreferencesInfo parseDefaultEditPreferences(
+      Config defaultCfg, EditPreferencesInfo input) throws ConfigInvalidException {
+    EditPreferencesInfo allUserPrefs = new EditPreferencesInfo();
+    loadSection(
+        defaultCfg,
+        UserConfigSections.EDIT,
+        null,
+        allUserPrefs,
+        EditPreferencesInfo.defaults(),
+        input);
+    return updateEditPreferencesDefaults(allUserPrefs);
+  }
+
+  private static GeneralPreferencesInfo updateGeneralPreferencesDefaults(
+      GeneralPreferencesInfo input) {
     GeneralPreferencesInfo result = GeneralPreferencesInfo.defaults();
     try {
       for (Field field : input.getClass().getDeclaredFields()) {
@@ -175,6 +317,44 @@
     return result;
   }
 
+  private static DiffPreferencesInfo updateDiffPreferencesDefaults(DiffPreferencesInfo input) {
+    DiffPreferencesInfo result = DiffPreferencesInfo.defaults();
+    try {
+      for (Field field : input.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        Object newVal = field.get(input);
+        if (newVal != null) {
+          field.set(result, newVal);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      log.error("Failed to apply default diff preferences", e);
+      return DiffPreferencesInfo.defaults();
+    }
+    return result;
+  }
+
+  private static EditPreferencesInfo updateEditPreferencesDefaults(EditPreferencesInfo input) {
+    EditPreferencesInfo result = EditPreferencesInfo.defaults();
+    try {
+      for (Field field : input.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        Object newVal = field.get(input);
+        if (newVal != null) {
+          field.set(result, newVal);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      log.error("Failed to apply default edit preferences", e);
+      return EditPreferencesInfo.defaults();
+    }
+    return result;
+  }
+
   private static List<String> parseChangeTableColumns(Config cfg, @Nullable Config defaultCfg) {
     List<String> changeTable = changeTable(cfg);
     if (changeTable == null && defaultCfg != null) {
@@ -207,9 +387,19 @@
     return urlAliases;
   }
 
-  public static GeneralPreferencesInfo readDefaultPreferences(Repository allUsersRepo)
+  public static GeneralPreferencesInfo readDefaultGeneralPreferences(Repository allUsersRepo)
       throws IOException, ConfigInvalidException {
-    return parse(readDefaultConfig(allUsersRepo), null, null);
+    return parseGeneralPreferences(readDefaultConfig(allUsersRepo), null, null);
+  }
+
+  public static DiffPreferencesInfo readDefaultDiffPreferences(Repository allUsersRepo)
+      throws IOException, ConfigInvalidException {
+    return parseDiffPreferences(readDefaultConfig(allUsersRepo), null, null);
+  }
+
+  public static EditPreferencesInfo readDefaultEditPreferences(Repository allUsersRepo)
+      throws IOException, ConfigInvalidException {
+    return parseEditPreferences(readDefaultConfig(allUsersRepo), null, null);
   }
 
   static Config readDefaultConfig(Repository allUsersRepo)
@@ -219,7 +409,7 @@
     return defaultPrefs.getConfig();
   }
 
-  public static GeneralPreferencesInfo updateDefaultPreferences(
+  public static GeneralPreferencesInfo updateDefaultGeneralPreferences(
       MetaDataUpdate md, GeneralPreferencesInfo input) throws IOException, ConfigInvalidException {
     VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
     defaultPrefs.load(md);
@@ -234,7 +424,37 @@
     setUrlAliases(defaultPrefs.getConfig(), input.urlAliases);
     defaultPrefs.commit(md);
 
-    return parse(defaultPrefs.getConfig(), null, null);
+    return parseGeneralPreferences(defaultPrefs.getConfig(), null, null);
+  }
+
+  public static DiffPreferencesInfo updateDefaultDiffPreferences(
+      MetaDataUpdate md, DiffPreferencesInfo input) throws IOException, ConfigInvalidException {
+    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
+    defaultPrefs.load(md);
+    storeSection(
+        defaultPrefs.getConfig(),
+        UserConfigSections.DIFF,
+        null,
+        input,
+        DiffPreferencesInfo.defaults());
+    defaultPrefs.commit(md);
+
+    return parseDiffPreferences(defaultPrefs.getConfig(), null, null);
+  }
+
+  public static EditPreferencesInfo updateDefaultEditPreferences(
+      MetaDataUpdate md, EditPreferencesInfo input) throws IOException, ConfigInvalidException {
+    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
+    defaultPrefs.load(md);
+    storeSection(
+        defaultPrefs.getConfig(),
+        UserConfigSections.EDIT,
+        null,
+        input,
+        EditPreferencesInfo.defaults());
+    defaultPrefs.commit(md);
+
+    return parseEditPreferences(defaultPrefs.getConfig(), null, null);
   }
 
   private static List<String> changeTable(Config cfg) {
diff --git a/java/com/google/gerrit/server/account/SetInactiveFlag.java b/java/com/google/gerrit/server/account/SetInactiveFlag.java
index f8cd650..6e4ec63 100644
--- a/java/com/google/gerrit/server/account/SetInactiveFlag.java
+++ b/java/com/google/gerrit/server/account/SetInactiveFlag.java
@@ -39,22 +39,19 @@
   public Response<?> deactivate(Account.Id accountId)
       throws RestApiException, IOException, ConfigInvalidException, OrmException {
     AtomicBoolean alreadyInactive = new AtomicBoolean(false);
-    Account account =
-        accountsUpdate
-            .create()
-            .update(
-                "Deactivate Account via API",
-                accountId,
-                (a, u) -> {
-                  if (!a.isActive()) {
-                    alreadyInactive.set(true);
-                  } else {
-                    u.setActive(false);
-                  }
-                });
-    if (account == null) {
-      throw new ResourceNotFoundException("account not found");
-    }
+    accountsUpdate
+        .create()
+        .update(
+            "Deactivate Account via API",
+            accountId,
+            (a, u) -> {
+              if (!a.getAccount().isActive()) {
+                alreadyInactive.set(true);
+              } else {
+                u.setActive(false);
+              }
+            })
+        .orElseThrow(() -> new ResourceNotFoundException("account not found"));
     if (alreadyInactive.get()) {
       throw new ResourceConflictException("account not active");
     }
@@ -64,22 +61,19 @@
   public Response<String> activate(Account.Id accountId)
       throws ResourceNotFoundException, IOException, ConfigInvalidException, OrmException {
     AtomicBoolean alreadyActive = new AtomicBoolean(false);
-    Account account =
-        accountsUpdate
-            .create()
-            .update(
-                "Activate Account via API",
-                accountId,
-                (a, u) -> {
-                  if (a.isActive()) {
-                    alreadyActive.set(true);
-                  } else {
-                    u.setActive(true);
-                  }
-                });
-    if (account == null) {
-      throw new ResourceNotFoundException("account not found");
-    }
+    accountsUpdate
+        .create()
+        .update(
+            "Activate Account via API",
+            accountId,
+            (a, u) -> {
+              if (a.getAccount().isActive()) {
+                alreadyActive.set(true);
+              } else {
+                u.setActive(true);
+              }
+            })
+        .orElseThrow(() -> new ResourceNotFoundException("account not found"));
     return alreadyActive.get() ? Response.ok("") : Response.created("");
   }
 }
diff --git a/java/com/google/gerrit/server/account/WatchConfig.java b/java/com/google/gerrit/server/account/WatchConfig.java
index 7adadf7..a51a1f0 100644
--- a/java/com/google/gerrit/server/account/WatchConfig.java
+++ b/java/com/google/gerrit/server/account/WatchConfig.java
@@ -23,8 +23,10 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
@@ -104,7 +106,7 @@
   private final Config cfg;
   private final ValidationError.Sink validationErrorSink;
 
-  private Map<ProjectWatchKey, Set<NotifyType>> projectWatches;
+  private ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches;
 
   public WatchConfig(Account.Id accountId, Config cfg, ValidationError.Sink validationErrorSink) {
     this.accountId = checkNotNull(accountId, "accountId");
@@ -112,7 +114,7 @@
     this.validationErrorSink = checkNotNull(validationErrorSink, "validationErrorSink");
   }
 
-  public Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches() {
+  public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> getProjectWatches() {
     if (projectWatches == null) {
       parse();
     }
@@ -123,8 +125,30 @@
     projectWatches = parse(accountId, cfg, validationErrorSink);
   }
 
+  /**
+   * Parses project watches from the given config file and returns them as a map.
+   *
+   * <p>A project watch is defined on a project and has a filter to match changes for which the
+   * project watch should be applied. The project and the filter form the map key. The map value is
+   * a set of notify types that decide for which events email notifications should be sent.
+   *
+   * <p>A project watch on the {@code All-Projects} project applies for all projects unless the
+   * project has a matching project watch.
+   *
+   * <p>A project watch can have an empty set of notify types. An empty set of notify types means
+   * that no notification for matching changes should be set. This is different from no project
+   * watch as it overwrites matching project watches from the {@code All-Projects} project.
+   *
+   * <p>Since we must be able to differentiate a project watch with an empty set of notify types
+   * from no project watch we can't use a {@link Multimap} as return type.
+   *
+   * @param accountId the ID of the account for which the project watches should be parsed
+   * @param cfg the config file from which the project watches should be parsed
+   * @param validationErrorSink validation error sink
+   * @return the parsed project watches
+   */
   @VisibleForTesting
-  public static Map<ProjectWatchKey, Set<NotifyType>> parse(
+  public static ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> parse(
       Account.Id accountId, Config cfg, ValidationError.Sink validationErrorSink) {
     Map<ProjectWatchKey, Set<NotifyType>> projectWatches = new HashMap<>();
     for (String projectName : cfg.getSubsections(PROJECT)) {
@@ -148,11 +172,11 @@
         projectWatches.get(key).addAll(notifyValue.notifyTypes());
       }
     }
-    return projectWatches;
+    return immutableCopyOf(projectWatches);
   }
 
   public Config save(Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
-    this.projectWatches = projectWatches;
+    this.projectWatches = immutableCopyOf(projectWatches);
 
     for (String projectName : cfg.getSubsections(PROJECT)) {
       cfg.unsetSection(PROJECT, projectName);
@@ -172,6 +196,16 @@
     return cfg;
   }
 
+  private static ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> immutableCopyOf(
+      Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
+    ImmutableMap.Builder<ProjectWatchKey, ImmutableSet<NotifyType>> b = ImmutableMap.builder();
+    projectWatches
+        .entrySet()
+        .stream()
+        .forEach(e -> b.put(e.getKey(), ImmutableSet.copyOf(e.getValue())));
+    return b.build();
+  }
+
   @AutoValue
   public abstract static class NotifyValue {
     public static NotifyValue parse(
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 8be7092..9f5e671 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -32,8 +32,10 @@
 import java.io.Serializable;
 import java.util.Collection;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
+import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
@@ -47,6 +49,34 @@
     return USER_NAME_PATTERN.matcher(username).matches();
   }
 
+  /**
+   * Returns the ID of the first external ID from the provided external IDs that has the {@link
+   * ExternalId#SCHEME_USERNAME} scheme.
+   *
+   * @param extIds external IDs
+   * @return the ID of the first external ID from the provided external IDs that has the {@link
+   *     ExternalId#SCHEME_USERNAME} scheme
+   */
+  public static Optional<String> getUserName(Collection<ExternalId> extIds) {
+    return extIds
+        .stream()
+        .filter(e -> e.isScheme(SCHEME_USERNAME))
+        .map(e -> e.key().id())
+        .findFirst();
+  }
+
+  /**
+   * Returns all IDs of the provided external IDs that have the {@link ExternalId#SCHEME_MAILTO}
+   * scheme as a distinct stream.
+   *
+   * @param extIds external IDs
+   * @return distinct stream of all IDs of the provided external IDs that have the {@link
+   *     ExternalId#SCHEME_MAILTO} scheme
+   */
+  public static Stream<String> getEmails(Collection<ExternalId> extIds) {
+    return extIds.stream().filter(e -> e.isScheme(SCHEME_MAILTO)).map(e -> e.key().id()).distinct();
+  }
+
   private static final long serialVersionUID = 1L;
 
   private static final String EXTERNAL_ID_SECTION = "externalId";
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index 8e69f2e..7acc0ab 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -79,7 +79,26 @@
   private static final int MAX_NOTE_SZ = 1 << 19;
 
   public interface ExternalIdNotesLoader {
+    /**
+     * Loads the external ID notes from the current tip of the {@code refs/meta/external-ids}
+     * branch.
+     *
+     * @param allUsersRepo the All-Users repository
+     */
     ExternalIdNotes load(Repository allUsersRepo) throws IOException, ConfigInvalidException;
+
+    /**
+     * Loads the external ID notes from the specified revision of the {@code refs/meta/external-ids}
+     * branch.
+     *
+     * @param allUsersRepo the All-Users repository
+     * @param rev the revision from which the external ID notes should be loaded, if {@code null}
+     *     the external ID notes are loaded from the current tip, if {@link ObjectId#zeroId()} it's
+     *     assumed that the {@code refs/meta/external-ids} branch doesn't exist and the loaded
+     *     external IDs will be empty
+     */
+    ExternalIdNotes load(Repository allUsersRepo, @Nullable ObjectId rev)
+        throws IOException, ConfigInvalidException;
   }
 
   @Singleton
@@ -98,6 +117,12 @@
         throws IOException, ConfigInvalidException {
       return new ExternalIdNotes(externalIdCache, accountCache, allUsersRepo).load();
     }
+
+    @Override
+    public ExternalIdNotes load(Repository allUsersRepo, @Nullable ObjectId rev)
+        throws IOException, ConfigInvalidException {
+      return new ExternalIdNotes(externalIdCache, accountCache, allUsersRepo).load(rev);
+    }
   }
 
   @Singleton
@@ -114,11 +139,17 @@
         throws IOException, ConfigInvalidException {
       return new ExternalIdNotes(externalIdCache, null, allUsersRepo).load();
     }
+
+    @Override
+    public ExternalIdNotes load(Repository allUsersRepo, @Nullable ObjectId rev)
+        throws IOException, ConfigInvalidException {
+      return new ExternalIdNotes(externalIdCache, null, allUsersRepo).load(rev);
+    }
   }
 
   /**
    * Loads the external ID notes for reading only. The external ID notes are loaded from the current
-   * HEAD revision of the {@code refs/meta/external-ids} branch.
+   * tip of the {@code refs/meta/external-ids} branch.
    *
    * @return read-only {@link ExternalIdNotes} instance
    */
@@ -134,7 +165,9 @@
    * specified revision of the {@code refs/meta/external-ids} branch.
    *
    * @param rev the revision from which the external ID notes should be loaded, if {@code null} the
-   *     external ID notes are loaded from the current HEAD revision
+   *     external ID notes are loaded from the current tip, if {@link ObjectId#zeroId()} it's
+   *     assumed that the {@code refs/meta/external-ids} branch doesn't exist and the loaded
+   *     external IDs will be empty
    * @return read-only {@link ExternalIdNotes} instance
    */
   public static ExternalIdNotes loadReadOnly(Repository allUsersRepo, @Nullable ObjectId rev)
@@ -146,7 +179,7 @@
 
   /**
    * Loads the external ID notes for updates without cache evictions. The external ID notes are
-   * loaded from the current HEAD revision of the {@code refs/meta/external-ids} branch.
+   * loaded from the current tip of the {@code refs/meta/external-ids} branch.
    *
    * @return {@link ExternalIdNotes} instance that doesn't updates caches on save
    */
@@ -200,8 +233,7 @@
   }
 
   /**
-   * Loads the external ID notes from the current HEAD revision of the {@code
-   * refs/meta/external-ids} branch.
+   * Loads the external ID notes from the current tip of the {@code refs/meta/external-ids} branch.
    *
    * @return {@link ExternalIdNotes} instance for chaining
    */
@@ -215,13 +247,19 @@
    * branch.
    *
    * @param rev the revision from which the external ID notes should be loaded, if {@code null} the
-   *     external ID notes are loaded from the current HEAD revision
+   *     external ID notes are loaded from the current tip, if {@link ObjectId#zeroId()} it's
+   *     assumed that the {@code refs/meta/external-ids} branch doesn't exist and the loaded
+   *     external IDs will be empty
    * @return {@link ExternalIdNotes} instance for chaining
    */
   ExternalIdNotes load(@Nullable ObjectId rev) throws IOException, ConfigInvalidException {
     if (rev == null) {
       return load();
     }
+    if (ObjectId.zeroId().equals(rev)) {
+      load(repo, null);
+      return this;
+    }
     load(repo, rev);
     return this;
   }
@@ -269,19 +307,19 @@
    *
    * @return all external IDs
    */
-  public Set<ExternalId> all() throws IOException {
+  public ImmutableSet<ExternalId> all() throws IOException {
     checkLoaded();
     try (RevWalk rw = new RevWalk(repo)) {
-      Set<ExternalId> extIds = new HashSet<>();
+      ImmutableSet.Builder<ExternalId> b = ImmutableSet.builder();
       for (Note note : noteMap) {
         byte[] raw = readNoteData(rw, note.getData());
         try {
-          extIds.add(ExternalId.parse(note.getName(), raw, note.getData()));
+          b.add(ExternalId.parse(note.getName(), raw, note.getData()));
         } catch (ConfigInvalidException | RuntimeException e) {
           log.error(String.format("Ignoring invalid external ID note %s", note.getName()), e);
         }
       }
-      return extIds;
+      return b.build();
     }
   }
 
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java b/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
index b4ff539..8d040d6 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.account.externalids;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
@@ -26,7 +27,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -93,7 +93,7 @@
   }
 
   /** Reads and returns all external IDs. */
-  Set<ExternalId> all() throws IOException, ConfigInvalidException {
+  ImmutableSet<ExternalId> all() throws IOException, ConfigInvalidException {
     checkReadEnabled();
 
     try (Timer0.Context ctx = readAllLatency.start();
@@ -107,10 +107,12 @@
    * refs/meta/external-ids} branch.
    *
    * @param rev the revision from which the external IDs should be read, if {@code null} the
-   *     external IDs are read from the current HEAD revision
+   *     external IDs are read from the current tip, if {@link ObjectId#zeroId()} it's assumed that
+   *     the {@code refs/meta/external-ids} branch doesn't exist and the loaded external IDs will be
+   *     empty
    * @return all external IDs that were read from the specified revision
    */
-  Set<ExternalId> all(@Nullable ObjectId rev) throws IOException, ConfigInvalidException {
+  ImmutableSet<ExternalId> all(@Nullable ObjectId rev) throws IOException, ConfigInvalidException {
     checkReadEnabled();
 
     try (Timer0.Context ctx = readAllLatency.start();
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIds.java b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
index a8ecc40..11f5855c 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
@@ -14,15 +14,15 @@
 
 package com.google.gerrit.server.account.externalids;
 
-import static java.util.stream.Collectors.toSet;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -43,12 +43,12 @@
   }
 
   /** Returns all external IDs. */
-  public Set<ExternalId> all() throws IOException, ConfigInvalidException {
+  public ImmutableSet<ExternalId> all() throws IOException, ConfigInvalidException {
     return externalIdReader.all();
   }
 
   /** Returns all external IDs from the specified revision of the refs/meta/external-ids branch. */
-  public Set<ExternalId> all(ObjectId rev) throws IOException, ConfigInvalidException {
+  public ImmutableSet<ExternalId> all(ObjectId rev) throws IOException, ConfigInvalidException {
     return externalIdReader.all(rev);
   }
 
@@ -66,27 +66,31 @@
   }
 
   /** Returns the external IDs of the specified account. */
-  public Set<ExternalId> byAccount(Account.Id accountId) throws IOException {
+  public ImmutableSet<ExternalId> byAccount(Account.Id accountId) throws IOException {
     return externalIdCache.byAccount(accountId);
   }
 
   /** Returns the external IDs of the specified account that have the given scheme. */
-  public Set<ExternalId> byAccount(Account.Id accountId, String scheme) throws IOException {
-    return byAccount(accountId).stream().filter(e -> e.key().isScheme(scheme)).collect(toSet());
+  public ImmutableSet<ExternalId> byAccount(Account.Id accountId, String scheme)
+      throws IOException {
+    return byAccount(accountId)
+        .stream()
+        .filter(e -> e.key().isScheme(scheme))
+        .collect(toImmutableSet());
   }
 
   /** Returns the external IDs of the specified account. */
-  public Set<ExternalId> byAccount(Account.Id accountId, ObjectId rev) throws IOException {
+  public ImmutableSet<ExternalId> byAccount(Account.Id accountId, ObjectId rev) throws IOException {
     return externalIdCache.byAccount(accountId, rev);
   }
 
   /** Returns the external IDs of the specified account that have the given scheme. */
-  public Set<ExternalId> byAccount(Account.Id accountId, String scheme, ObjectId rev)
+  public ImmutableSet<ExternalId> byAccount(Account.Id accountId, String scheme, ObjectId rev)
       throws IOException {
     return byAccount(accountId, rev)
         .stream()
         .filter(e -> e.key().isScheme(scheme))
-        .collect(toSet());
+        .collect(toImmutableSet());
   }
 
   /** Returns all external IDs by account. */
@@ -107,7 +111,7 @@
    *
    * @see #byEmails(String...)
    */
-  public Set<ExternalId> byEmail(String email) throws IOException {
+  public ImmutableSet<ExternalId> byEmail(String email) throws IOException {
     return externalIdCache.byEmail(email);
   }
 
diff --git a/java/com/google/gerrit/server/api/config/ServerImpl.java b/java/com/google/gerrit/server/api/config/ServerImpl.java
index ce87d1c..ec08507 100644
--- a/java/com/google/gerrit/server/api/config/ServerImpl.java
+++ b/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -21,15 +21,18 @@
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
 import com.google.gerrit.extensions.api.config.Server;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.restapi.config.CheckConsistency;
 import com.google.gerrit.server.restapi.config.GetDiffPreferences;
+import com.google.gerrit.server.restapi.config.GetEditPreferences;
 import com.google.gerrit.server.restapi.config.GetPreferences;
 import com.google.gerrit.server.restapi.config.GetServerInfo;
 import com.google.gerrit.server.restapi.config.SetDiffPreferences;
+import com.google.gerrit.server.restapi.config.SetEditPreferences;
 import com.google.gerrit.server.restapi.config.SetPreferences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -41,6 +44,8 @@
   private final SetPreferences setPreferences;
   private final GetDiffPreferences getDiffPreferences;
   private final SetDiffPreferences setDiffPreferences;
+  private final GetEditPreferences getEditPreferences;
+  private final SetEditPreferences setEditPreferences;
   private final GetServerInfo getServerInfo;
   private final Provider<CheckConsistency> checkConsistency;
 
@@ -50,12 +55,16 @@
       SetPreferences setPreferences,
       GetDiffPreferences getDiffPreferences,
       SetDiffPreferences setDiffPreferences,
+      GetEditPreferences getEditPreferences,
+      SetEditPreferences setEditPreferences,
       GetServerInfo getServerInfo,
       Provider<CheckConsistency> checkConsistency) {
     this.getPreferences = getPreferences;
     this.setPreferences = setPreferences;
     this.getDiffPreferences = getDiffPreferences;
     this.setDiffPreferences = setDiffPreferences;
+    this.getEditPreferences = getEditPreferences;
+    this.setEditPreferences = setEditPreferences;
     this.getServerInfo = getServerInfo;
     this.checkConsistency = checkConsistency;
   }
@@ -113,6 +122,25 @@
   }
 
   @Override
+  public EditPreferencesInfo getDefaultEditPreferences() throws RestApiException {
+    try {
+      return getEditPreferences.apply(new ConfigResource());
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get default edit preferences", e);
+    }
+  }
+
+  @Override
+  public EditPreferencesInfo setDefaultEditPreferences(EditPreferencesInfo in)
+      throws RestApiException {
+    try {
+      return setEditPreferences.apply(new ConfigResource(), in);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set default edit preferences", e);
+    }
+  }
+
+  @Override
   public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException {
     try {
       return checkConsistency.get().apply(new ConfigResource(), in);
diff --git a/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java b/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
index 016a593..07924fa 100644
--- a/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
 import com.google.gerrit.extensions.common.SetDashboardInput;
 import com.google.gerrit.extensions.restapi.IdString;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.DashboardResource;
@@ -88,8 +87,7 @@
   }
 
   private DashboardResource resource()
-      throws ResourceNotFoundException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     return dashboards.parse(project, IdString.fromDecoded(id));
   }
 }
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 8ba755f..fe00904 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -123,7 +123,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.RemoveReviewerControl;
 import com.google.gerrit.server.project.SubmitRuleOptions;
@@ -344,7 +343,6 @@
         | OrmException
         | IOException
         | PermissionBackendException
-        | NoSuchProjectException
         | RuntimeException e) {
       if (!has(CHECK)) {
         Throwables.throwIfInstanceOf(e, OrmException.class);
@@ -423,7 +421,6 @@
             | OrmException
             | IOException
             | PermissionBackendException
-            | NoSuchProjectException
             | RuntimeException e) {
           if (has(CHECK)) {
             i = checkOnly(cd);
@@ -489,8 +486,8 @@
   }
 
   private ChangeInfo toChangeInfo(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
-      throws PatchListNotAvailableException, GpgException, OrmException, IOException,
-          PermissionBackendException, NoSuchProjectException {
+      throws PatchListNotAvailableException, GpgException, OrmException, PermissionBackendException,
+          IOException {
     ChangeInfo out = new ChangeInfo();
     CurrentUser user = userProvider.get();
 
@@ -1122,7 +1119,7 @@
   }
 
   private Collection<AccountInfo> removableReviewers(ChangeData cd, ChangeInfo out)
-      throws PermissionBackendException, NoSuchProjectException, OrmException, IOException {
+      throws PermissionBackendException, OrmException {
     // Although this is called removableReviewers, this method also determines
     // which CCs are removable.
     //
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index e10197f..c58cde7 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -222,7 +222,7 @@
 
   private void checkOwner() {
     try {
-      if (accounts.get(change().getOwner()) == null) {
+      if (!accounts.get(change().getOwner()).isPresent()) {
         problem("Missing change owner: " + change().getOwner());
       }
     } catch (IOException | ConfigInvalidException e) {
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 292fa7a..a9e3ed5 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -92,6 +92,7 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.SetHashtagsOp;
@@ -174,6 +175,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
@@ -2444,7 +2446,8 @@
       // or no parents were updated (rebase), else warn that only part
       // of the commit was modified.
       if (newCommit.getTree().equals(priorCommit.getTree())) {
-        boolean messageEq = eq(newCommit.getFullMessage(), priorCommit.getFullMessage());
+        boolean messageEq =
+            Objects.equals(newCommit.getFullMessage(), priorCommit.getFullMessage());
         boolean parentsEq = parentsEqual(newCommit, priorCommit);
         boolean authorEq = authorEqual(newCommit, priorCommit);
         ObjectReader reader = rp.getRevWalk().getObjectReader();
@@ -2722,18 +2725,8 @@
       return false;
     }
 
-    return eq(aAuthor.getName(), bAuthor.getName())
-        && eq(aAuthor.getEmailAddress(), bAuthor.getEmailAddress());
-  }
-
-  static boolean eq(String a, String b) {
-    if (a == null && b == null) {
-      return true;
-    } else if (a == null || b == null) {
-      return false;
-    } else {
-      return a.equals(b);
-    }
+    return Objects.equals(aAuthor.getName(), bAuthor.getName())
+        && Objects.equals(aAuthor.getEmailAddress(), bAuthor.getEmailAddress());
   }
 
   private boolean validRefOperation(ReceiveCommand cmd) {
@@ -2980,20 +2973,20 @@
     }
     logDebug("Updating full name of caller");
     try {
-      Account account =
+      Optional<AccountState> accountState =
           accountsUpdate
               .create()
               .update(
                   "Set Full Name on Receive Commits",
                   user.getAccountId(),
                   (a, u) -> {
-                    if (Strings.isNullOrEmpty(a.getFullName())) {
+                    if (Strings.isNullOrEmpty(a.getAccount().getFullName())) {
                       u.setFullName(setFullNameTo);
                     }
                   });
-      if (account != null) {
-        user.getAccount().setFullName(account.getFullName());
-      }
+      accountState
+          .map(AccountState::getAccount)
+          .ifPresent(a -> user.getAccount().setFullName(a.getFullName()));
     } catch (OrmException | IOException | ConfigInvalidException e) {
       logWarn("Failed to update full name of caller", e);
     }
diff --git a/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java b/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java
index a0eae4a..2335d32 100644
--- a/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java
@@ -34,6 +34,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -114,7 +115,7 @@
     }
 
     for (Account.Id id : g.getMembers().asList()) {
-      AccountState account;
+      Optional<AccountState> account;
       try {
         account = accounts.get(id);
       } catch (ConfigInvalidException e) {
@@ -124,7 +125,7 @@
                 g.getName(), g.getGroupUUID(), id, e.getMessage()));
         continue;
       }
-      if (account == null) {
+      if (!account.isPresent()) {
         problems.add(
             error("group %s (%s) has nonexistent member %s", g.getName(), g.getGroupUUID(), id));
       }
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index 8b0cc7f..93ed4f13 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.index.query.Predicate;
@@ -69,7 +70,8 @@
 
     for (AccountState a : args.accountQueryProvider.get().byWatchedProject(project)) {
       Account.Id accountId = a.getAccount().getId();
-      for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : a.getProjectWatches().entrySet()) {
+      for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyType>> e :
+          a.getProjectWatches().entrySet()) {
         if (project.equals(e.getKey().project())
             && add(matching, accountId, e.getKey(), e.getValue(), type)) {
           // We only want to prevent matching All-Projects if this filter hits
@@ -79,7 +81,8 @@
     }
 
     for (AccountState a : args.accountQueryProvider.get().byWatchedProject(args.allProjectsName)) {
-      for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : a.getProjectWatches().entrySet()) {
+      for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyType>> e :
+          a.getProjectWatches().entrySet()) {
         if (args.allProjectsName.equals(e.getKey().project())) {
           Account.Id accountId = a.getAccount().getId();
           if (!projectWatchers.contains(accountId)) {
diff --git a/java/com/google/gerrit/server/project/DefaultPermissionBackend.java b/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
index b679fca..c33c01f 100644
--- a/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
@@ -45,10 +45,13 @@
   private static final CurrentUser.PropertyKey<Boolean> IS_ADMIN = CurrentUser.PropertyKey.create();
 
   private final ProjectCache projectCache;
+  private final ProjectControl.Factory projectControlFactory;
 
   @Inject
-  DefaultPermissionBackend(ProjectCache projectCache) {
+  DefaultPermissionBackend(
+      ProjectCache projectCache, ProjectControl.Factory projectControlFactory) {
     this.projectCache = projectCache;
+    this.projectControlFactory = projectControlFactory;
   }
 
   private CapabilityCollection capabilities() {
@@ -73,7 +76,7 @@
       try {
         ProjectState state = projectCache.checkedGet(project);
         if (state != null) {
-          return state.controlFor(user).asForProject().database(db);
+          return projectControlFactory.create(user, state).asForProject().database(db);
         }
         return FailedPermissionBackend.project("not found", new NoSuchProjectException(project));
       } catch (IOException e) {
diff --git a/java/com/google/gerrit/server/project/ProjectControl.java b/java/com/google/gerrit/server/project/ProjectControl.java
index 68dbf86..4d6cdfa 100644
--- a/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/java/com/google/gerrit/server/project/ProjectControl.java
@@ -79,7 +79,7 @@
 
   private final Set<AccountGroup.UUID> uploadGroups;
   private final Set<AccountGroup.UUID> receiveGroups;
-  private final PermissionBackend.WithUser perm;
+  private final PermissionBackend permissionBackend;
   private final CurrentUser user;
   private final ProjectState state;
   private final ChangeControl.Factory changeControlFactory;
@@ -102,13 +102,21 @@
     this.uploadGroups = uploadGroups;
     this.receiveGroups = receiveGroups;
     this.permissionFilter = permissionFilter;
-    this.perm = permissionBackend.user(who);
+    this.permissionBackend = permissionBackend;
     user = who;
     state = ps;
   }
 
   ProjectControl forUser(CurrentUser who) {
-    ProjectControl r = state.controlFor(who);
+    ProjectControl r =
+        new ProjectControl(
+            uploadGroups,
+            receiveGroups,
+            permissionFilter,
+            changeControlFactory,
+            permissionBackend,
+            who,
+            state);
     // Not per-user, and reusing saves lookup time.
     r.allSections = allSections;
     return r;
@@ -173,7 +181,7 @@
 
   boolean isAdmin() {
     try {
-      perm.check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
       return true;
     } catch (AuthException | PermissionBackendException e) {
       return false;
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index a8dc993..b45f119 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -87,7 +87,6 @@
   private final SitePaths sitePaths;
   private final AllProjectsName allProjectsName;
   private final ProjectCache projectCache;
-  private final ProjectControl.Factory projectControlFactory;
   private final PrologEnvironment.Factory envFactory;
   private final GitRepositoryManager gitMgr;
   private final RulesCache rulesCache;
@@ -121,7 +120,6 @@
       final ProjectCache projectCache,
       final AllProjectsName allProjectsName,
       final AllUsersName allUsersName,
-      final ProjectControl.Factory projectControlFactory,
       final PrologEnvironment.Factory envFactory,
       final GitRepositoryManager gitMgr,
       final RulesCache rulesCache,
@@ -133,7 +131,6 @@
     this.isAllProjects = config.getProject().getNameKey().equals(allProjectsName);
     this.isAllUsers = config.getProject().getNameKey().equals(allUsersName);
     this.allProjectsName = allProjectsName;
-    this.projectControlFactory = projectControlFactory;
     this.envFactory = envFactory;
     this.gitMgr = gitMgr;
     this.rulesCache = rulesCache;
@@ -355,10 +352,6 @@
     return result;
   }
 
-  public ProjectControl controlFor(CurrentUser user) {
-    return projectControlFactory.create(user, this);
-  }
-
   /**
    * @return an iterable that walks through this project and then the parents of this project.
    *     Starts from this project and progresses up the hierarchy to All-Projects.
diff --git a/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index e91d36e..a0091a3 100644
--- a/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -22,39 +22,36 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.IOException;
 
 @Singleton
 public class RemoveReviewerControl {
   private final PermissionBackend permissionBackend;
   private final Provider<ReviewDb> dbProvider;
-  private final ProjectCache projectCache;
 
   @Inject
-  RemoveReviewerControl(
-      PermissionBackend permissionBackend,
-      Provider<ReviewDb> dbProvider,
-      ProjectCache projectCache) {
+  RemoveReviewerControl(PermissionBackend permissionBackend, Provider<ReviewDb> dbProvider) {
     this.permissionBackend = permissionBackend;
     this.dbProvider = dbProvider;
-    this.projectCache = projectCache;
   }
 
   /**
    * Checks if removing the given reviewer and patch set approval is OK.
    *
    * @throws AuthException if this user is not allowed to remove this approval.
+   * @throws PermissionBackendException on failure of permission checks.
    */
   public void checkRemoveReviewer(
       ChangeNotes notes, CurrentUser currentUser, PatchSetApproval approval)
-      throws PermissionBackendException, AuthException, NoSuchProjectException, IOException {
+      throws PermissionBackendException, AuthException {
     checkRemoveReviewer(notes, currentUser, approval.getAccountId(), approval.getValue());
   }
 
@@ -63,17 +60,19 @@
    * reviewer might have given is OK.
    *
    * @throws AuthException if this user is not allowed to remove this approval.
+   * @throws PermissionBackendException on failure of permission checks.
    */
   public void checkRemoveReviewer(ChangeNotes notes, CurrentUser currentUser, Account.Id reviewer)
-      throws PermissionBackendException, AuthException, NoSuchProjectException, IOException {
+      throws PermissionBackendException, AuthException {
     checkRemoveReviewer(notes, currentUser, reviewer, 0);
   }
 
   /** @return true if the user is allowed to remove this reviewer. */
   public boolean testRemoveReviewer(
       ChangeData cd, CurrentUser currentUser, Account.Id reviewer, int value)
-      throws PermissionBackendException, NoSuchProjectException, OrmException, IOException {
-    if (canRemoveReviewerWithoutPermissionCheck(cd.change(), currentUser, reviewer, value)) {
+      throws PermissionBackendException, OrmException {
+    if (canRemoveReviewerWithoutPermissionCheck(
+        permissionBackend, cd.change(), currentUser, reviewer, value)) {
       return true;
     }
     return permissionBackend
@@ -85,8 +84,9 @@
 
   private void checkRemoveReviewer(
       ChangeNotes notes, CurrentUser currentUser, Account.Id reviewer, int val)
-      throws PermissionBackendException, AuthException, NoSuchProjectException, IOException {
-    if (canRemoveReviewerWithoutPermissionCheck(notes.getChange(), currentUser, reviewer, val)) {
+      throws PermissionBackendException, AuthException {
+    if (canRemoveReviewerWithoutPermissionCheck(
+        permissionBackend, notes.getChange(), currentUser, reviewer, val)) {
       return;
     }
 
@@ -97,9 +97,13 @@
         .check(ChangePermission.REMOVE_REVIEWER);
   }
 
-  private boolean canRemoveReviewerWithoutPermissionCheck(
-      Change change, CurrentUser currentUser, Account.Id reviewer, int value)
-      throws NoSuchProjectException, IOException {
+  private static boolean canRemoveReviewerWithoutPermissionCheck(
+      PermissionBackend permissionBackend,
+      Change change,
+      CurrentUser currentUser,
+      Account.Id reviewer,
+      int value)
+      throws PermissionBackendException {
     if (!change.getStatus().isOpen()) {
       return false;
     }
@@ -115,17 +119,32 @@
 
     // Users with the remove reviewer permission, the branch owner, project
     // owner and site admin can remove anyone
-    // TODO(hiesel): Remove all Control usage
-    ProjectState projectState = projectCache.checkedGet(change.getProject());
-    if (projectState == null) {
-      throw new NoSuchProjectException(change.getProject());
-    }
-    ProjectControl ctl = projectState.controlFor(currentUser);
-    if (ctl.controlForRef(change.getDest()).isOwner() // branch owner
-        || ctl.isOwner() // project owner
-        || ctl.isAdmin()) { // project admin
+    PermissionBackend.WithUser withUser = permissionBackend.user(currentUser);
+    PermissionBackend.ForProject forProject = withUser.project(change.getProject());
+    if (check(forProject.ref(change.getDest().get()), RefPermission.WRITE_CONFIG)
+        || check(withUser, GlobalPermission.ADMINISTRATE_SERVER)) {
       return true;
     }
     return false;
   }
+
+  private static boolean check(PermissionBackend.ForRef forRef, RefPermission perm)
+      throws PermissionBackendException {
+    try {
+      forRef.check(perm);
+      return true;
+    } catch (AuthException e) {
+      return false;
+    }
+  }
+
+  private static boolean check(PermissionBackend.WithUser withUser, GlobalPermission perm)
+      throws PermissionBackendException {
+    try {
+      withUser.check(perm);
+      return true;
+    } catch (AuthException e) {
+      return false;
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java b/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
index c173079..3281f6e 100644
--- a/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
@@ -14,19 +14,13 @@
 
 package com.google.gerrit.server.restapi.account;
 
-import static com.google.gerrit.server.config.ConfigUtil.loadSection;
-import static com.google.gerrit.server.config.ConfigUtil.skipField;
-
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.account.VersionedAccountPreferences;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.UserConfigSections;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -34,32 +28,20 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.lang.reflect.Field;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class GetDiffPreferences implements RestReadView<AccountResource> {
-  private static final Logger log = LoggerFactory.getLogger(GetDiffPreferences.class);
-
   private final Provider<CurrentUser> self;
-  private final Provider<AllUsersName> allUsersName;
   private final PermissionBackend permissionBackend;
-  private final GitRepositoryManager gitMgr;
+  private final AccountCache accountCache;
 
   @Inject
   GetDiffPreferences(
-      Provider<CurrentUser> self,
-      Provider<AllUsersName> allUsersName,
-      PermissionBackend permissionBackend,
-      GitRepositoryManager gitMgr) {
+      Provider<CurrentUser> self, PermissionBackend permissionBackend, AccountCache accountCache) {
     this.self = self;
-    this.allUsersName = allUsersName;
     this.permissionBackend = permissionBackend;
-    this.gitMgr = gitMgr;
+    this.accountCache = accountCache;
   }
 
   @Override
@@ -70,53 +52,6 @@
     }
 
     Account.Id id = rsrc.getUser().getAccountId();
-    return readFromGit(id, gitMgr, allUsersName.get(), null);
-  }
-
-  static DiffPreferencesInfo readFromGit(
-      Account.Id id, GitRepositoryManager gitMgr, AllUsersName allUsersName, DiffPreferencesInfo in)
-      throws IOException, ConfigInvalidException, RepositoryNotFoundException {
-    try (Repository git = gitMgr.openRepository(allUsersName)) {
-      VersionedAccountPreferences p = VersionedAccountPreferences.forUser(id);
-      p.load(git);
-      DiffPreferencesInfo prefs = new DiffPreferencesInfo();
-      loadSection(
-          p.getConfig(), UserConfigSections.DIFF, null, prefs, readDefaultsFromGit(git, in), in);
-      return prefs;
-    }
-  }
-
-  static DiffPreferencesInfo readDefaultsFromGit(Repository git, DiffPreferencesInfo in)
-      throws ConfigInvalidException, IOException {
-    VersionedAccountPreferences dp = VersionedAccountPreferences.forDefault();
-    dp.load(git);
-    DiffPreferencesInfo allUserPrefs = new DiffPreferencesInfo();
-    loadSection(
-        dp.getConfig(),
-        UserConfigSections.DIFF,
-        null,
-        allUserPrefs,
-        DiffPreferencesInfo.defaults(),
-        in);
-    return updateDefaults(allUserPrefs);
-  }
-
-  private static DiffPreferencesInfo updateDefaults(DiffPreferencesInfo input) {
-    DiffPreferencesInfo result = DiffPreferencesInfo.defaults();
-    try {
-      for (Field field : input.getClass().getDeclaredFields()) {
-        if (skipField(field)) {
-          continue;
-        }
-        Object newVal = field.get(input);
-        if (newVal != null) {
-          field.set(result, newVal);
-        }
-      }
-    } catch (IllegalAccessException e) {
-      log.warn("Cannot get default diff preferences from All-Users", e);
-      return DiffPreferencesInfo.defaults();
-    }
-    return result;
+    return accountCache.get(id).getDiffPreferences();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java b/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
index 95a6e7c..a51da10 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
@@ -14,18 +14,13 @@
 
 package com.google.gerrit.server.restapi.account;
 
-import static com.google.gerrit.server.config.ConfigUtil.loadSection;
-
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.account.VersionedAccountPreferences;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.UserConfigSections;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -34,26 +29,19 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Repository;
 
 @Singleton
 public class GetEditPreferences implements RestReadView<AccountResource> {
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
-  private final AllUsersName allUsersName;
-  private final GitRepositoryManager gitMgr;
+  private final AccountCache accountCache;
 
   @Inject
   GetEditPreferences(
-      Provider<CurrentUser> self,
-      PermissionBackend permissionBackend,
-      AllUsersName allUsersName,
-      GitRepositoryManager gitMgr) {
+      Provider<CurrentUser> self, PermissionBackend permissionBackend, AccountCache accountCache) {
     this.self = self;
     this.permissionBackend = permissionBackend;
-    this.allUsersName = allUsersName;
-    this.gitMgr = gitMgr;
+    this.accountCache = accountCache;
   }
 
   @Override
@@ -63,23 +51,7 @@
       permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
-    return readFromGit(rsrc.getUser().getAccountId(), gitMgr, allUsersName, null);
-  }
-
-  static EditPreferencesInfo readFromGit(
-      Account.Id id, GitRepositoryManager gitMgr, AllUsersName allUsersName, EditPreferencesInfo in)
-      throws IOException, ConfigInvalidException, RepositoryNotFoundException {
-    try (Repository git = gitMgr.openRepository(allUsersName)) {
-      VersionedAccountPreferences p = VersionedAccountPreferences.forUser(id);
-      p.load(git);
-
-      return loadSection(
-          p.getConfig(),
-          UserConfigSections.EDIT,
-          null,
-          new EditPreferencesInfo(),
-          EditPreferencesInfo.defaults(),
-          in);
-    }
+    Account.Id id = rsrc.getUser().getAccountId();
+    return accountCache.get(id).getEditPreferences();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
index ffddc7c..460d072 100644
--- a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -40,7 +41,6 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
@@ -66,12 +66,10 @@
     }
 
     Account.Id accountId = rsrc.getUser().getAccountId();
-    AccountState account = accounts.get(accountId);
-    if (account == null) {
-      throw new ResourceNotFoundException();
-    }
+    AccountState account = accounts.get(accountId).orElseThrow(ResourceNotFoundException::new);
     List<ProjectWatchInfo> projectWatchInfos = new ArrayList<>();
-    for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : account.getProjectWatches().entrySet()) {
+    for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyType>> e :
+        account.getProjectWatches().entrySet()) {
       ProjectWatchInfo pwi = new ProjectWatchInfo();
       pwi.filter = e.getKey().filter();
       pwi.project = e.getKey().project().get();
diff --git a/java/com/google/gerrit/server/restapi/account/PutName.java b/java/com/google/gerrit/server/restapi/account/PutName.java
index 4981e7a..0c85608 100644
--- a/java/com/google/gerrit/server/restapi/account/PutName.java
+++ b/java/com/google/gerrit/server/restapi/account/PutName.java
@@ -22,10 +22,10 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -79,15 +79,13 @@
     }
 
     String newName = input.name;
-    Account account =
+    AccountState accountState =
         accountsUpdate
             .create()
-            .update("Set Full Name via API", user.getAccountId(), u -> u.setFullName(newName));
-    if (account == null) {
-      throw new ResourceNotFoundException("account not found");
-    }
-    return Strings.isNullOrEmpty(account.getFullName())
+            .update("Set Full Name via API", user.getAccountId(), u -> u.setFullName(newName))
+            .orElseThrow(() -> new ResourceNotFoundException("account not found"));
+    return Strings.isNullOrEmpty(accountState.getAccount().getFullName())
         ? Response.none()
-        : Response.ok(account.getFullName());
+        : Response.ok(accountState.getAccount().getFullName());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/PutPreferred.java b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
index b66a611..5c22273 100644
--- a/java/com/google/gerrit/server/restapi/account/PutPreferred.java
+++ b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
@@ -65,22 +64,19 @@
   public Response<String> apply(IdentifiedUser user, String email)
       throws ResourceNotFoundException, IOException, ConfigInvalidException, OrmException {
     AtomicBoolean alreadyPreferred = new AtomicBoolean(false);
-    Account account =
-        accountsUpdate
-            .create()
-            .update(
-                "Set Preferred Email via API",
-                user.getAccountId(),
-                (a, u) -> {
-                  if (email.equals(a.getPreferredEmail())) {
-                    alreadyPreferred.set(true);
-                  } else {
-                    u.setPreferredEmail(email);
-                  }
-                });
-    if (account == null) {
-      throw new ResourceNotFoundException("account not found");
-    }
+    accountsUpdate
+        .create()
+        .update(
+            "Set Preferred Email via API",
+            user.getAccountId(),
+            (a, u) -> {
+              if (email.equals(a.getAccount().getPreferredEmail())) {
+                alreadyPreferred.set(true);
+              } else {
+                u.setPreferredEmail(email);
+              }
+            })
+        .orElseThrow(() -> new ResourceNotFoundException("account not found"));
     return alreadyPreferred.get() ? Response.ok("") : Response.created("");
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/PutStatus.java b/java/com/google/gerrit/server/restapi/account/PutStatus.java
index 23958a2..40ae90f 100644
--- a/java/com/google/gerrit/server/restapi/account/PutStatus.java
+++ b/java/com/google/gerrit/server/restapi/account/PutStatus.java
@@ -20,10 +20,10 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -68,15 +68,13 @@
     }
 
     String newStatus = input.status;
-    Account account =
+    AccountState accountState =
         accountsUpdate
             .create()
-            .update("Set Status via API", user.getAccountId(), u -> u.setStatus(newStatus));
-    if (account == null) {
-      throw new ResourceNotFoundException("account not found");
-    }
-    return Strings.isNullOrEmpty(account.getStatus())
+            .update("Set Status via API", user.getAccountId(), u -> u.setStatus(newStatus))
+            .orElseThrow(() -> new ResourceNotFoundException("account not found"));
+    return Strings.isNullOrEmpty(accountState.getAccount().getStatus())
         ? Response.none()
-        : Response.ok(account.getStatus());
+        : Response.ok(accountState.getAccount().getStatus());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
index c16e0f2..4700b96 100644
--- a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
@@ -14,26 +14,19 @@
 
 package com.google.gerrit.server.restapi.account;
 
-import static com.google.gerrit.server.config.ConfigUtil.loadSection;
-import static com.google.gerrit.server.config.ConfigUtil.storeSection;
-import static com.google.gerrit.server.restapi.account.GetDiffPreferences.readDefaultsFromGit;
-import static com.google.gerrit.server.restapi.account.GetDiffPreferences.readFromGit;
-
 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.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.account.VersionedAccountPreferences;
-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.UserConfigSections;
+import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -44,52 +37,38 @@
 @Singleton
 public class SetDiffPreferences implements RestModifyView<AccountResource, DiffPreferencesInfo> {
   private final Provider<CurrentUser> self;
-  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
-  private final AllUsersName allUsersName;
   private final PermissionBackend permissionBackend;
-  private final GitRepositoryManager gitMgr;
+  private final AccountCache accountCache;
+  private final AccountsUpdate.User accountsUpdate;
 
   @Inject
   SetDiffPreferences(
       Provider<CurrentUser> self,
-      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
-      AllUsersName allUsersName,
       PermissionBackend permissionBackend,
-      GitRepositoryManager gitMgr) {
+      AccountCache accountCache,
+      AccountsUpdate.User accountsUpdate) {
     this.self = self;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
-    this.allUsersName = allUsersName;
     this.permissionBackend = permissionBackend;
-    this.gitMgr = gitMgr;
+    this.accountCache = accountCache;
+    this.accountsUpdate = accountsUpdate;
   }
 
   @Override
-  public DiffPreferencesInfo apply(AccountResource rsrc, DiffPreferencesInfo in)
+  public DiffPreferencesInfo apply(AccountResource rsrc, DiffPreferencesInfo input)
       throws AuthException, BadRequestException, ConfigInvalidException,
-          RepositoryNotFoundException, IOException, PermissionBackendException {
+          RepositoryNotFoundException, IOException, PermissionBackendException, OrmException {
     if (self.get() != rsrc.getUser()) {
       permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
-    if (in == null) {
+    if (input == null) {
       throw new BadRequestException("input must be provided");
     }
 
     Account.Id id = rsrc.getUser().getAccountId();
-    return writeToGit(readFromGit(id, gitMgr, allUsersName, in), id);
-  }
-
-  private DiffPreferencesInfo writeToGit(DiffPreferencesInfo in, Account.Id userId)
-      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    DiffPreferencesInfo out = new DiffPreferencesInfo();
-    try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
-      DiffPreferencesInfo allUserPrefs = readDefaultsFromGit(md.getRepository(), null);
-      VersionedAccountPreferences prefs = VersionedAccountPreferences.forUser(userId);
-      prefs.load(md);
-      storeSection(prefs.getConfig(), UserConfigSections.DIFF, null, in, allUserPrefs);
-      prefs.commit(md);
-      loadSection(prefs.getConfig(), UserConfigSections.DIFF, null, out, allUserPrefs, null);
-    }
-    return out;
+    accountsUpdate
+        .create()
+        .update("Set Diff Preferences via API", id, u -> u.setDiffPreferences(input));
+    return accountCache.get(id).getDiffPreferences();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
index 3574377..f16bb9e 100644
--- a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
@@ -14,25 +14,19 @@
 
 package com.google.gerrit.server.restapi.account;
 
-import static com.google.gerrit.server.config.ConfigUtil.loadSection;
-import static com.google.gerrit.server.config.ConfigUtil.storeSection;
-import static com.google.gerrit.server.restapi.account.GetEditPreferences.readFromGit;
-
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.account.VersionedAccountPreferences;
-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.UserConfigSections;
+import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -44,61 +38,38 @@
 public class SetEditPreferences implements RestModifyView<AccountResource, EditPreferencesInfo> {
 
   private final Provider<CurrentUser> self;
-  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final PermissionBackend permissionBackend;
-  private final GitRepositoryManager gitMgr;
-  private final AllUsersName allUsersName;
+  private final AccountCache accountCache;
+  private final AccountsUpdate.User accountsUpdate;
 
   @Inject
   SetEditPreferences(
       Provider<CurrentUser> self,
-      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       PermissionBackend permissionBackend,
-      GitRepositoryManager gitMgr,
-      AllUsersName allUsersName) {
+      AccountCache accountCache,
+      AccountsUpdate.User accountsUpdate) {
     this.self = self;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.permissionBackend = permissionBackend;
-    this.gitMgr = gitMgr;
-    this.allUsersName = allUsersName;
+    this.accountCache = accountCache;
+    this.accountsUpdate = accountsUpdate;
   }
 
   @Override
-  public EditPreferencesInfo apply(AccountResource rsrc, EditPreferencesInfo in)
+  public EditPreferencesInfo apply(AccountResource rsrc, EditPreferencesInfo input)
       throws AuthException, BadRequestException, RepositoryNotFoundException, IOException,
-          ConfigInvalidException, PermissionBackendException {
+          ConfigInvalidException, PermissionBackendException, OrmException {
     if (self.get() != rsrc.getUser()) {
       permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
-    if (in == null) {
+    if (input == null) {
       throw new BadRequestException("input must be provided");
     }
 
-    Account.Id accountId = rsrc.getUser().getAccountId();
-
-    VersionedAccountPreferences prefs;
-    EditPreferencesInfo out = new EditPreferencesInfo();
-    try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
-      prefs = VersionedAccountPreferences.forUser(accountId);
-      prefs.load(md);
-      storeSection(
-          prefs.getConfig(),
-          UserConfigSections.EDIT,
-          null,
-          readFromGit(accountId, gitMgr, allUsersName, in),
-          EditPreferencesInfo.defaults());
-      prefs.commit(md);
-      out =
-          loadSection(
-              prefs.getConfig(),
-              UserConfigSections.EDIT,
-              null,
-              out,
-              EditPreferencesInfo.defaults(),
-              null);
-    }
-
-    return out;
+    Account.Id id = rsrc.getUser().getAccountId();
+    accountsUpdate
+        .create()
+        .update("Set Diff Preferences via API", id, u -> u.setEditPreferences(input));
+    return accountCache.get(id).getEditPreferences();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/SetPreferences.java b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
index 9b2b231..8131748 100644
--- a/java/com/google/gerrit/server/restapi/account/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
@@ -73,7 +73,7 @@
 
     accountsUpdate
         .create()
-        .update("Set Preferences via API", id, u -> u.setGeneralPreferences(input));
+        .update("Set General Preferences via API", id, u -> u.setGeneralPreferences(input));
     return cache.get(id).getGeneralPreferences();
   }
 
diff --git a/java/com/google/gerrit/server/restapi/account/StarredChanges.java b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
index f908d9f..1d3fae0 100644
--- a/java/com/google/gerrit/server/restapi/account/StarredChanges.java
+++ b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
@@ -73,7 +73,7 @@
 
   @Override
   public AccountResource.StarredChange parse(AccountResource parent, IdString id)
-      throws RestApiException, OrmException, PermissionBackendException {
+      throws RestApiException, OrmException, PermissionBackendException, IOException {
     IdentifiedUser user = parent.getUser();
     ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
     if (starredChangesUtil
@@ -109,7 +109,7 @@
       return createProvider.get().setChange(changes.parse(TopLevelResource.INSTANCE, id));
     } catch (ResourceNotFoundException e) {
       throw new UnprocessableEntityException(String.format("change %s not found", id.get()));
-    } catch (OrmException | PermissionBackendException e) {
+    } catch (OrmException | PermissionBackendException | IOException e) {
       log.error("cannot resolve change", e);
       throw new UnprocessableEntityException("internal server error");
     }
diff --git a/java/com/google/gerrit/server/restapi/account/Stars.java b/java/com/google/gerrit/server/restapi/account/Stars.java
index 2ee6aef..6862e8a 100644
--- a/java/com/google/gerrit/server/restapi/account/Stars.java
+++ b/java/com/google/gerrit/server/restapi/account/Stars.java
@@ -40,6 +40,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
 import java.util.Set;
@@ -67,7 +68,7 @@
 
   @Override
   public Star parse(AccountResource parent, IdString id)
-      throws RestApiException, OrmException, PermissionBackendException {
+      throws RestApiException, OrmException, PermissionBackendException, IOException {
     IdentifiedUser user = parent.getUser();
     ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
     Set<String> labels = starredChangesUtil.getLabels(user.getAccountId(), change.getId());
diff --git a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
index 9203134..b8b411f 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeFinder;
 import com.google.gerrit.server.CurrentUser;
@@ -32,10 +33,13 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import java.util.List;
 
 @Singleton
@@ -49,6 +53,7 @@
   private final CreateChange createChange;
   private final ChangeResource.Factory changeResourceFactory;
   private final PermissionBackend permissionBackend;
+  private final ProjectCache projectCache;
 
   @Inject
   ChangesCollection(
@@ -59,7 +64,8 @@
       ChangeFinder changeFinder,
       CreateChange createChange,
       ChangeResource.Factory changeResourceFactory,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ProjectCache projectCache) {
     this.db = db;
     this.user = user;
     this.queryFactory = queryFactory;
@@ -68,6 +74,7 @@
     this.createChange = createChange;
     this.changeResourceFactory = changeResourceFactory;
     this.permissionBackend = permissionBackend;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -82,7 +89,7 @@
 
   @Override
   public ChangeResource parse(TopLevelResource root, IdString id)
-      throws RestApiException, OrmException, PermissionBackendException {
+      throws RestApiException, OrmException, PermissionBackendException, IOException {
     List<ChangeNotes> notes = changeFinder.find(id.encoded(), true);
     if (notes.isEmpty()) {
       throw new ResourceNotFoundException(id);
@@ -94,11 +101,12 @@
     if (!canRead(change)) {
       throw new ResourceNotFoundException(id);
     }
+    checkProjectStatePermitsRead(change.getProjectName());
     return changeResourceFactory.create(change, user.get());
   }
 
   public ChangeResource parse(Change.Id id)
-      throws ResourceNotFoundException, OrmException, PermissionBackendException {
+      throws RestApiException, OrmException, PermissionBackendException, IOException {
     List<ChangeNotes> notes = changeFinder.find(id);
     if (notes.isEmpty()) {
       throw new ResourceNotFoundException(toIdString(id));
@@ -110,6 +118,7 @@
     if (!canRead(change)) {
       throw new ResourceNotFoundException(toIdString(id));
     }
+    checkProjectStatePermitsRead(change.getProjectName());
     return changeResourceFactory.create(change, user.get());
   }
 
@@ -134,4 +143,13 @@
       return false;
     }
   }
+
+  private void checkProjectStatePermitsRead(Project.NameKey project)
+      throws IOException, RestApiException {
+    ProjectState projectState = projectCache.checkedGet(project);
+    if (projectState == null) {
+      throw new ResourceNotFoundException("project not found: " + project.get());
+    }
+    projectState.checkStatePermitsRead();
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java
index aac16660..85ad78f 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java
@@ -40,7 +40,6 @@
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.RemoveReviewerControl;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -121,7 +120,7 @@
   @Override
   public boolean updateChange(ChangeContext ctx)
       throws AuthException, ResourceNotFoundException, OrmException, PermissionBackendException,
-          IOException, NoSuchProjectException {
+          IOException {
     Account.Id reviewerId = reviewer.getId();
     // Check of removing this reviewer (even if there is no vote processed by the loop below) is OK
     removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), reviewerId);
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 268425e..e0ae92d3 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -44,7 +44,6 @@
 import com.google.gerrit.server.mail.send.DeleteVoteSender;
 import com.google.gerrit.server.mail.send.ReplyToChangeSender;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-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.RemoveReviewerControl;
@@ -166,7 +165,7 @@
     @Override
     public boolean updateChange(ChangeContext ctx)
         throws OrmException, AuthException, ResourceNotFoundException, IOException,
-            PermissionBackendException, NoSuchProjectException {
+            PermissionBackendException {
       change = ctx.getChange();
       PatchSet.Id psId = change.currentPatchSetId();
       ps = psUtil.current(db.get(), ctx.getNotes());
diff --git a/java/com/google/gerrit/server/restapi/config/ConfigRestModule.java b/java/com/google/gerrit/server/restapi/config/ConfigRestModule.java
index 71b2f9c..90fa5c4 100644
--- a/java/com/google/gerrit/server/restapi/config/ConfigRestModule.java
+++ b/java/com/google/gerrit/server/restapi/config/ConfigRestModule.java
@@ -41,6 +41,8 @@
     put(CONFIG_KIND, "preferences").to(SetPreferences.class);
     get(CONFIG_KIND, "preferences.diff").to(GetDiffPreferences.class);
     put(CONFIG_KIND, "preferences.diff").to(SetDiffPreferences.class);
+    get(CONFIG_KIND, "preferences.edit").to(GetEditPreferences.class);
+    put(CONFIG_KIND, "preferences.edit").to(SetEditPreferences.class);
     put(CONFIG_KIND, "email.confirm").to(ConfirmEmail.class);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java b/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
index cc264db..222f292 100644
--- a/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
@@ -14,22 +14,18 @@
 
 package com.google.gerrit.server.restapi.config;
 
-import static com.google.gerrit.server.config.ConfigUtil.loadSection;
-
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.account.PreferencesConfig;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.UserConfigSections;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
 
 @Singleton
@@ -47,25 +43,8 @@
   @Override
   public DiffPreferencesInfo apply(ConfigResource configResource)
       throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
-    return readFromGit(gitManager, allUsersName, null);
-  }
-
-  static DiffPreferencesInfo readFromGit(
-      GitRepositoryManager gitMgr, AllUsersName allUsersName, DiffPreferencesInfo in)
-      throws IOException, ConfigInvalidException, RepositoryNotFoundException {
-    try (Repository git = gitMgr.openRepository(allUsersName)) {
-      // Load all users prefs.
-      VersionedAccountPreferences dp = VersionedAccountPreferences.forDefault();
-      dp.load(git);
-      DiffPreferencesInfo allUserPrefs = new DiffPreferencesInfo();
-      loadSection(
-          dp.getConfig(),
-          UserConfigSections.DIFF,
-          null,
-          allUserPrefs,
-          DiffPreferencesInfo.defaults(),
-          in);
-      return allUserPrefs;
+    try (Repository git = gitManager.openRepository(allUsersName)) {
+      return PreferencesConfig.readDefaultDiffPreferences(git);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java b/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java
new file mode 100644
index 0000000..bd8174e
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2018 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.restapi.config;
+
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.PreferencesConfig;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
+
+@Singleton
+public class GetEditPreferences implements RestReadView<ConfigResource> {
+  private final AllUsersName allUsersName;
+  private final GitRepositoryManager gitManager;
+
+  @Inject
+  GetEditPreferences(GitRepositoryManager gitManager, AllUsersName allUsersName) {
+    this.allUsersName = allUsersName;
+    this.gitManager = gitManager;
+  }
+
+  @Override
+  public EditPreferencesInfo apply(ConfigResource configResource)
+      throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
+    try (Repository git = gitManager.openRepository(allUsersName)) {
+      return PreferencesConfig.readDefaultEditPreferences(git);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/GetPreferences.java b/java/com/google/gerrit/server/restapi/config/GetPreferences.java
index 3c7453c..e81ce24 100644
--- a/java/com/google/gerrit/server/restapi/config/GetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetPreferences.java
@@ -41,7 +41,7 @@
   public GeneralPreferencesInfo apply(ConfigResource rsrc)
       throws IOException, ConfigInvalidException {
     try (Repository git = gitMgr.openRepository(allUsersName)) {
-      return PreferencesConfig.readDefaultPreferences(git);
+      return PreferencesConfig.readDefaultGeneralPreferences(git);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java b/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
index e4f8782..ca7b739 100644
--- a/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
@@ -14,28 +14,24 @@
 
 package com.google.gerrit.server.restapi.config;
 
-import static com.google.gerrit.server.config.ConfigUtil.loadSection;
 import static com.google.gerrit.server.config.ConfigUtil.skipField;
-import static com.google.gerrit.server.config.ConfigUtil.storeSection;
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.PreferencesConfig;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ConfigResource;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.UserConfigSections;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.lang.reflect.Field;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -46,48 +42,33 @@
 
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final AllUsersName allUsersName;
-  private final GitRepositoryManager gitManager;
+  private final AccountCache accountCache;
 
   @Inject
   SetDiffPreferences(
-      GitRepositoryManager gitManager,
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
-      AllUsersName allUsersName) {
-    this.gitManager = gitManager;
+      AllUsersName allUsersName,
+      AccountCache accountCache) {
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.allUsersName = allUsersName;
+    this.accountCache = accountCache;
   }
 
   @Override
-  public DiffPreferencesInfo apply(ConfigResource configResource, DiffPreferencesInfo in)
+  public DiffPreferencesInfo apply(ConfigResource configResource, DiffPreferencesInfo input)
       throws BadRequestException, IOException, ConfigInvalidException {
-    if (in == null) {
+    if (input == null) {
       throw new BadRequestException("input must be provided");
     }
-    if (!hasSetFields(in)) {
+    if (!hasSetFields(input)) {
       throw new BadRequestException("unsupported option");
     }
-    return writeToGit(GetDiffPreferences.readFromGit(gitManager, allUsersName, in));
-  }
 
-  private DiffPreferencesInfo writeToGit(DiffPreferencesInfo in)
-      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    DiffPreferencesInfo out = new DiffPreferencesInfo();
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
-      VersionedAccountPreferences prefs = VersionedAccountPreferences.forDefault();
-      prefs.load(md);
-      DiffPreferencesInfo defaults = DiffPreferencesInfo.defaults();
-      storeSection(prefs.getConfig(), UserConfigSections.DIFF, null, in, defaults);
-      prefs.commit(md);
-      loadSection(
-          prefs.getConfig(),
-          UserConfigSections.DIFF,
-          null,
-          out,
-          DiffPreferencesInfo.defaults(),
-          null);
+      DiffPreferencesInfo updatedPrefs = PreferencesConfig.updateDefaultDiffPreferences(md, input);
+      accountCache.evictAllNoReindex();
+      return updatedPrefs;
     }
-    return out;
   }
 
   private static boolean hasSetFields(DiffPreferencesInfo in) {
diff --git a/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java b/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java
new file mode 100644
index 0000000..f17881c
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2018 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.restapi.config;
+
+import static com.google.gerrit.server.config.ConfigUtil.skipField;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.PreferencesConfig;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@Singleton
+public class SetEditPreferences implements RestModifyView<ConfigResource, EditPreferencesInfo> {
+  private static final Logger log = LoggerFactory.getLogger(SetDiffPreferences.class);
+
+  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
+  private final AllUsersName allUsersName;
+  private final AccountCache accountCache;
+
+  @Inject
+  SetEditPreferences(
+      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+      AllUsersName allUsersName,
+      AccountCache accountCache) {
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.allUsersName = allUsersName;
+    this.accountCache = accountCache;
+  }
+
+  @Override
+  public EditPreferencesInfo apply(ConfigResource configResource, EditPreferencesInfo input)
+      throws BadRequestException, IOException, ConfigInvalidException {
+    if (input == null) {
+      throw new BadRequestException("input must be provided");
+    }
+    if (!hasSetFields(input)) {
+      throw new BadRequestException("unsupported option");
+    }
+
+    try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
+      EditPreferencesInfo updatedPrefs = PreferencesConfig.updateDefaultEditPreferences(md, input);
+      accountCache.evictAllNoReindex();
+      return updatedPrefs;
+    }
+  }
+
+  private static boolean hasSetFields(EditPreferencesInfo in) {
+    try {
+      for (Field field : in.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        if (field.get(in) != null) {
+          return true;
+        }
+      }
+    } catch (IllegalAccessException e) {
+      log.warn("Unable to verify input", e);
+    }
+    return false;
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/SetPreferences.java b/java/com/google/gerrit/server/restapi/config/SetPreferences.java
index be990e2..84ef7fe 100644
--- a/java/com/google/gerrit/server/restapi/config/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetPreferences.java
@@ -62,7 +62,8 @@
     }
     PreferencesConfig.validateMy(input.my);
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
-      GeneralPreferencesInfo updatedPrefs = PreferencesConfig.updateDefaultPreferences(md, input);
+      GeneralPreferencesInfo updatedPrefs =
+          PreferencesConfig.updateDefaultGeneralPreferences(md, input);
       accountCache.evictAllNoReindex();
       return updatedPrefs;
     }
diff --git a/java/com/google/gerrit/server/restapi/project/BranchesCollection.java b/java/com/google/gerrit/server/restapi/project/BranchesCollection.java
index b90bd9c..b10a90a 100644
--- a/java/com/google/gerrit/server/restapi/project/BranchesCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/BranchesCollection.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -71,7 +72,8 @@
 
   @Override
   public BranchResource parse(ProjectResource parent, IdString id)
-      throws ResourceNotFoundException, IOException, PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
+    parent.getProjectState().checkStatePermitsRead();
     Project.NameKey project = parent.getNameKey();
     try (Repository repo = repoManager.openRepository(project)) {
       Ref ref = repo.exactRef(RefNames.fullName(id.get()));
diff --git a/java/com/google/gerrit/server/restapi/project/ChildProjectsCollection.java b/java/com/google/gerrit/server/restapi/project/ChildProjectsCollection.java
index ee572fe..9f6676d 100644
--- a/java/com/google/gerrit/server/restapi/project/ChildProjectsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/ChildProjectsCollection.java
@@ -56,6 +56,7 @@
   @Override
   public ChildProjectResource parse(ProjectResource parent, IdString id)
       throws RestApiException, IOException, PermissionBackendException {
+    parent.getProjectState().checkStatePermitsRead();
     ProjectResource p = projectsCollection.parse(TopLevelResource.INSTANCE, id);
     for (ProjectState pp : p.getProjectState().parents()) {
       if (parent.getNameKey().equals(pp.getProject().getNameKey())) {
diff --git a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
index 6aa639f..3eb0e26 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -70,7 +71,8 @@
 
   @Override
   public CommitResource parse(ProjectResource parent, IdString id)
-      throws ResourceNotFoundException, IOException {
+      throws RestApiException, IOException {
+    parent.getProjectState().checkStatePermitsRead();
     ObjectId objectId;
     try {
       objectId = ObjectId.fromString(id.get());
diff --git a/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java b/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java
index e3ffa4d..2f2ace8 100644
--- a/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java
@@ -99,6 +99,7 @@
   @Override
   public RestModifyView<ProjectResource, ?> create(ProjectResource parent, IdString id)
       throws RestApiException {
+    parent.getProjectState().checkStatePermitsWrite();
     if (isDefaultDashboard(id)) {
       return createDefault.get();
     }
@@ -107,8 +108,8 @@
 
   @Override
   public DashboardResource parse(ProjectResource parent, IdString id)
-      throws ResourceNotFoundException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
+    parent.getProjectState().checkStatePermitsRead();
     if (isDefaultDashboard(id)) {
       return DashboardResource.projectDefault(parent.getProjectState(), parent.getUser());
     }
diff --git a/java/com/google/gerrit/server/restapi/project/GetDashboard.java b/java/com/google/gerrit/server/restapi/project/GetDashboard.java
index dde77e5..2ec67e7 100644
--- a/java/com/google/gerrit/server/restapi/project/GetDashboard.java
+++ b/java/com/google/gerrit/server/restapi/project/GetDashboard.java
@@ -81,8 +81,7 @@
   }
 
   private DashboardResource defaultOf(ProjectState projectState, CurrentUser user)
-      throws ResourceNotFoundException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     String id = projectState.getProject().getLocalDefaultDashboard();
     if (Strings.isNullOrEmpty(id)) {
       id = projectState.getProject().getDefaultDashboard();
@@ -107,8 +106,7 @@
   }
 
   private DashboardResource parse(ProjectState projectState, CurrentUser user, String id)
-      throws ResourceNotFoundException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     List<String> p = Lists.newArrayList(Splitter.on(':').limit(2).split(id));
     String ref = Url.encode(p.get(0));
     String path = Url.encode(p.get(1));
diff --git a/java/com/google/gerrit/server/restapi/project/ListBranches.java b/java/com/google/gerrit/server/restapi/project/ListBranches.java
index 416a5c2..05b6def 100644
--- a/java/com/google/gerrit/server/restapi/project/ListBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/ListBranches.java
@@ -22,8 +22,8 @@
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.webui.UiAction;
@@ -134,8 +134,8 @@
 
   @Override
   public List<BranchInfo> apply(ProjectResource rsrc)
-      throws ResourceNotFoundException, IOException, BadRequestException,
-          PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
+    rsrc.getProjectState().checkStatePermitsRead();
     return new RefFilter<BranchInfo>(Constants.R_HEADS)
         .subString(matchSubstring)
         .regex(matchRegex)
diff --git a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
index c514f90..5727c6b 100644
--- a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
@@ -17,6 +17,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
@@ -69,7 +70,9 @@
   }
 
   @Override
-  public List<ProjectInfo> apply(ProjectResource rsrc) throws PermissionBackendException {
+  public List<ProjectInfo> apply(ProjectResource rsrc)
+      throws PermissionBackendException, ResourceConflictException {
+    rsrc.getProjectState().checkStatePermitsRead();
     if (recursive) {
       return childProjects.list(rsrc.getNameKey());
     }
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index af4586c..a038bfe 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -18,9 +18,9 @@
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CommonConverters;
@@ -130,7 +130,9 @@
 
   @Override
   public List<TagInfo> apply(ProjectResource resource)
-      throws IOException, ResourceNotFoundException, BadRequestException {
+      throws IOException, ResourceNotFoundException, RestApiException {
+    resource.getProjectState().checkStatePermitsRead();
+
     List<TagInfo> tags = new ArrayList<>();
 
     PermissionBackend.ForProject perm = permissionBackend.user(user).project(resource.getNameKey());
diff --git a/java/com/google/gerrit/server/restapi/project/TagsCollection.java b/java/com/google/gerrit/server/restapi/project/TagsCollection.java
index a1b0395..c503094 100644
--- a/java/com/google/gerrit/server/restapi/project/TagsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/TagsCollection.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.TagResource;
@@ -50,9 +51,10 @@
   }
 
   @Override
-  public TagResource parse(ProjectResource rsrc, IdString id)
-      throws ResourceNotFoundException, IOException {
-    return new TagResource(rsrc.getProjectState(), rsrc.getUser(), list.get().get(rsrc, id));
+  public TagResource parse(ProjectResource parent, IdString id)
+      throws RestApiException, IOException {
+    parent.getProjectState().checkStatePermitsRead();
+    return new TagResource(parent.getProjectState(), parent.getUser(), list.get().get(parent, id));
   }
 
   @Override
diff --git a/java/com/google/gerrit/sshd/ChangeArgumentParser.java b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
index f442032..961860c 100644
--- a/java/com/google/gerrit/sshd/ChangeArgumentParser.java
+++ b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -32,6 +32,7 @@
 import com.google.gerrit.sshd.BaseCommand.UnloggedFailure;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -62,13 +63,13 @@
   }
 
   public void addChange(String id, Map<Change.Id, ChangeResource> changes)
-      throws UnloggedFailure, OrmException, PermissionBackendException {
+      throws UnloggedFailure, OrmException, PermissionBackendException, IOException {
     addChange(id, changes, null);
   }
 
   public void addChange(
       String id, Map<Change.Id, ChangeResource> changes, ProjectState projectState)
-      throws UnloggedFailure, OrmException, PermissionBackendException {
+      throws UnloggedFailure, OrmException, PermissionBackendException, IOException {
     addChange(id, changes, projectState, true);
   }
 
@@ -77,7 +78,7 @@
       Map<Change.Id, ChangeResource> changes,
       ProjectState projectState,
       boolean useIndex)
-      throws UnloggedFailure, OrmException, PermissionBackendException {
+      throws UnloggedFailure, OrmException, PermissionBackendException, IOException {
     List<ChangeNotes> matched = useIndex ? changeFinder.find(id) : changeFromNotesFactory(id);
     List<ChangeNotes> toAdd = new ArrayList<>(changes.size());
     boolean canMaintainServer;
@@ -109,7 +110,7 @@
     ChangeResource changeResource;
     try {
       changeResource = changesCollection.parse(cId);
-    } catch (ResourceNotFoundException e) {
+    } catch (RestApiException e) {
       throw new UnloggedFailure(1, "\"" + id + "\" no such change");
     }
     changes.put(cId, changeResource);
diff --git a/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index d9a0a37..fce4337 100644
--- a/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
@@ -133,6 +134,8 @@
         childProjects.addAll(getChildrenForReparenting(oldParent));
       } catch (PermissionBackendException e) {
         throw new Failure(1, "permissions unavailable", e);
+      } catch (RestApiException e) {
+        throw new Failure(1, "failure in request", e);
       }
     }
 
@@ -196,7 +199,7 @@
    * reparenting.
    */
   private List<Project.NameKey> getChildrenForReparenting(ProjectState parent)
-      throws PermissionBackendException {
+      throws PermissionBackendException, RestApiException {
     final List<Project.NameKey> childProjects = new ArrayList<>();
     final List<Project.NameKey> excluded = new ArrayList<>(excludedChildren.size());
     for (ProjectState excludedChild : excludedChildren) {
diff --git a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
index 3ed856b..9a98257 100644
--- a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import java.io.IOException;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import org.kohsuke.args4j.Argument;
@@ -44,7 +45,7 @@
   void addChange(String token) {
     try {
       changeArgumentParser.addChange(token, changes, null, false);
-    } catch (UnloggedFailure | OrmException | PermissionBackendException e) {
+    } catch (UnloggedFailure | OrmException | PermissionBackendException | IOException e) {
       writeError("warning", e.getMessage());
     }
   }
diff --git a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 4c7d59d..c777afa 100644
--- a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -76,7 +77,7 @@
   void addChange(String token) {
     try {
       changeArgumentParser.addChange(token, changes, projectState);
-    } catch (UnloggedFailure e) {
+    } catch (IOException | UnloggedFailure e) {
       throw new IllegalArgumentException(e.getMessage(), e);
     } catch (OrmException e) {
       throw new IllegalArgumentException("database is down", e);
diff --git a/java/com/google/gerrit/sshd/commands/UploadArchive.java b/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 95dbb40..acb2eca 100644
--- a/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -203,15 +203,16 @@
       } catch (GitAPIException e) {
         throw new Failure(7, "fatal: git api exception, " + e);
       }
-    } catch (Failure f) {
-      // Report the error in ERROR sideband channel
+    } catch (Throwable t) {
+      // Report the error in ERROR sideband channel. Catch Throwable too so we can also catch
+      // NoClassDefFound.
       try (SideBandOutputStream sidebandError =
           new SideBandOutputStream(
               SideBandOutputStream.CH_ERROR, SideBandOutputStream.MAX_BUF, out)) {
-        sidebandError.write(f.getMessage().getBytes(UTF_8));
+        sidebandError.write(t.getMessage().getBytes(UTF_8));
         sidebandError.flush();
       }
-      throw f;
+      throw t;
     } finally {
       // In any case, cleanly close the packetOut channel
       packetOut.end();
diff --git a/java/com/google/gerrit/testing/FakeAccountCache.java b/java/com/google/gerrit/testing/FakeAccountCache.java
index 4a94997..58cc48f 100644
--- a/java/com/google/gerrit/testing/FakeAccountCache.java
+++ b/java/com/google/gerrit/testing/FakeAccountCache.java
@@ -14,10 +14,8 @@
 
 package com.google.gerrit.testing;
 
-import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
@@ -79,11 +77,6 @@
   }
 
   private static AccountState newState(Account account) {
-    return new AccountState(
-        new AllUsersName(AllUsersNameProvider.DEFAULT),
-        account,
-        ImmutableSet.of(),
-        new HashMap<>(),
-        GeneralPreferencesInfo.defaults());
+    return AccountState.forAccount(new AllUsersName(AllUsersNameProvider.DEFAULT), account);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 4b1b5d0..62d07c1 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.deleteRef;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
 import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
@@ -46,6 +47,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.io.BaseEncoding;
 import com.google.common.util.concurrent.AtomicLongMap;
+import com.google.common.util.concurrent.Runnables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.AccountCreator;
 import com.google.gerrit.acceptance.GerritConfig;
@@ -372,14 +374,14 @@
       Account.Id accountId = new Account.Id(seq.nextAccountId());
       String fullName = "Foo";
       ExternalId extId = ExternalId.createEmail(accountId, "foo@example.com");
-      Account account =
+      AccountState accountState =
           accountsUpdate
               .create()
               .insert(
                   "Create Account Atomically",
                   accountId,
                   u -> u.setFullName(fullName).addExternalId(extId));
-      assertThat(account.getFullName()).isEqualTo(fullName);
+      assertThat(accountState.getAccount().getFullName()).isEqualTo(fullName);
 
       AccountInfo info = gApi.accounts().id(accountId.get()).get();
       assertThat(info.name).isEqualTo(fullName);
@@ -400,12 +402,12 @@
   public void updateNonExistingAccount() throws Exception {
     Account.Id nonExistingAccountId = new Account.Id(999999);
     AtomicBoolean consumerCalled = new AtomicBoolean();
-    Account account =
+    Optional<AccountState> accountState =
         accountsUpdate
             .create()
             .update(
                 "Update Non-Existing Account", nonExistingAccountId, a -> consumerCalled.set(true));
-    assertThat(account).isNull();
+    assertThat(accountState).isEmpty();
     assertThat(consumerCalled.get()).isFalse();
   }
 
@@ -415,11 +417,12 @@
     assertUserBranchWithoutAccountConfig(anonymousCoward.getId());
 
     String status = "OOO";
-    Account account =
+    Optional<AccountState> accountState =
         accountsUpdate
             .create()
             .update("Set status", anonymousCoward.getId(), u -> u.setStatus(status));
-    assertThat(account).isNotNull();
+    assertThat(accountState).isPresent();
+    Account account = accountState.get().getAccount();
     assertThat(account.getFullName()).isNull();
     assertThat(account.getStatus()).isEqualTo(status);
     assertUserBranch(anonymousCoward.getId(), null, status);
@@ -1935,18 +1938,21 @@
   @Test
   public void checkMetaId() throws Exception {
     // metaId is set when account is loaded
-    assertThat(accounts.get(admin.getId()).getAccount().getMetaId())
+    assertThat(accounts.get(admin.getId()).get().getAccount().getMetaId())
         .isEqualTo(getMetaId(admin.getId()));
 
     // metaId is set when account is created
     AccountsUpdate au = accountsUpdate.create();
     Account.Id accountId = new Account.Id(seq.nextAccountId());
-    Account account = au.insert("Create Test Account", accountId, u -> {});
-    assertThat(account.getMetaId()).isEqualTo(getMetaId(accountId));
+    AccountState accountState = au.insert("Create Test Account", accountId, u -> {});
+    assertThat(accountState.getAccount().getMetaId()).isEqualTo(getMetaId(accountId));
 
     // metaId is set when account is updated
-    Account updatedAccount = au.update("Set Full Name", accountId, u -> u.setFullName("foo"));
-    assertThat(account.getMetaId()).isNotEqualTo(updatedAccount.getMetaId());
+    Optional<AccountState> updatedAccountState =
+        au.update("Set Full Name", accountId, u -> u.setFullName("foo"));
+    assertThat(updatedAccountState.isPresent()).isTrue();
+    Account updatedAccount = updatedAccountState.get().getAccount();
+    assertThat(accountState.getAccount().getMetaId()).isNotEqualTo(updatedAccount.getMetaId());
     assertThat(updatedAccount.getMetaId()).isEqualTo(getMetaId(accountId));
   }
 
@@ -2021,6 +2027,7 @@
             gitReferenceUpdated,
             null,
             allUsers,
+            externalIds,
             metaDataUpdateInternalFactory,
             new RetryHelper(
                 cfg,
@@ -2040,15 +2047,19 @@
                   // Ignore, the successful update of the account is asserted later
                 }
               }
-            });
+            },
+            Runnables.doNothing());
     assertThat(doneBgUpdate.get()).isFalse();
     AccountInfo accountInfo = gApi.accounts().id(admin.id.get()).get();
     assertThat(accountInfo.status).isNull();
     assertThat(accountInfo.name).isNotEqualTo(fullName);
 
-    Account updatedAccount = update.update("Set Full Name", admin.id, u -> u.setFullName(fullName));
+    Optional<AccountState> updatedAccountState =
+        update.update("Set Full Name", admin.id, u -> u.setFullName(fullName));
     assertThat(doneBgUpdate.get()).isTrue();
 
+    assertThat(updatedAccountState.isPresent()).isTrue();
+    Account updatedAccount = updatedAccountState.get().getAccount();
     assertThat(updatedAccount.getStatus()).isEqualTo(status);
     assertThat(updatedAccount.getFullName()).isEqualTo(fullName);
 
@@ -2069,6 +2080,7 @@
             gitReferenceUpdated,
             null,
             allUsers,
+            externalIds,
             metaDataUpdateInternalFactory,
             new RetryHelper(
                 cfg,
@@ -2093,7 +2105,8 @@
               } catch (IOException | ConfigInvalidException | OrmException e) {
                 // Ignore, the expected exception is asserted later
               }
-            });
+            },
+            Runnables.doNothing());
     assertThat(bgCounter.get()).isEqualTo(0);
     AccountInfo accountInfo = gApi.accounts().id(admin.id.get()).get();
     assertThat(accountInfo.status).isNull();
@@ -2107,7 +2120,7 @@
     }
     assertThat(bgCounter.get()).isEqualTo(status.size());
 
-    Account updatedAccount = accounts.get(admin.id).getAccount();
+    Account updatedAccount = accounts.get(admin.id).get().getAccount();
     assertThat(updatedAccount.getStatus()).isEqualTo(Iterables.getLast(status));
     assertThat(updatedAccount.getFullName()).isEqualTo(admin.fullName);
 
@@ -2117,6 +2130,156 @@
   }
 
   @Test
+  public void atomicReadMofifyWrite() throws Exception {
+    gApi.accounts().id(admin.id.get()).setStatus("A-1");
+
+    AtomicInteger bgCounterA1 = new AtomicInteger(0);
+    AtomicInteger bgCounterA2 = new AtomicInteger(0);
+    PersonIdent ident = serverIdent.get();
+    AccountsUpdate update =
+        new AccountsUpdate(
+            repoManager,
+            gitReferenceUpdated,
+            null,
+            allUsers,
+            externalIds,
+            metaDataUpdateInternalFactory,
+            new RetryHelper(
+                cfg,
+                retryMetrics,
+                null,
+                null,
+                null,
+                r -> r.withBlockStrategy(noSleepBlockStrategy)),
+            extIdNotesFactory,
+            ident,
+            ident,
+            Runnables.doNothing(),
+            () -> {
+              try {
+                accountsUpdate.create().update("Set Status", admin.id, u -> u.setStatus("A-2"));
+              } catch (IOException | ConfigInvalidException | OrmException e) {
+                // Ignore, the expected exception is asserted later
+              }
+            });
+    assertThat(bgCounterA1.get()).isEqualTo(0);
+    assertThat(bgCounterA2.get()).isEqualTo(0);
+    assertThat(gApi.accounts().id(admin.id.get()).get().status).isEqualTo("A-1");
+
+    Optional<AccountState> updatedAccountState =
+        update.update(
+            "Set Status",
+            admin.id,
+            (a, u) -> {
+              if ("A-1".equals(a.getAccount().getStatus())) {
+                bgCounterA1.getAndIncrement();
+                u.setStatus("B-1");
+              }
+
+              if ("A-2".equals(a.getAccount().getStatus())) {
+                bgCounterA2.getAndIncrement();
+                u.setStatus("B-2");
+              }
+            });
+
+    assertThat(bgCounterA1.get()).isEqualTo(1);
+    assertThat(bgCounterA2.get()).isEqualTo(1);
+
+    assertThat(updatedAccountState.isPresent()).isTrue();
+    assertThat(updatedAccountState.get().getAccount().getStatus()).isEqualTo("B-2");
+    assertThat(accounts.get(admin.id).get().getAccount().getStatus()).isEqualTo("B-2");
+    assertThat(gApi.accounts().id(admin.id.get()).get().status).isEqualTo("B-2");
+  }
+
+  @Test
+  public void atomicReadMofifyWriteExternalIds() throws Exception {
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
+    Account.Id accountId = new Account.Id(seq.nextAccountId());
+    ExternalId extIdA1 = ExternalId.create("foo", "A-1", accountId);
+    accountsUpdate.create().insert("Create Test Account", accountId, u -> u.addExternalId(extIdA1));
+
+    AtomicInteger bgCounterA1 = new AtomicInteger(0);
+    AtomicInteger bgCounterA2 = new AtomicInteger(0);
+    PersonIdent ident = serverIdent.get();
+    ExternalId extIdA2 = ExternalId.create("foo", "A-2", accountId);
+    AccountsUpdate update =
+        new AccountsUpdate(
+            repoManager,
+            gitReferenceUpdated,
+            null,
+            allUsers,
+            externalIds,
+            metaDataUpdateInternalFactory,
+            new RetryHelper(
+                cfg,
+                retryMetrics,
+                null,
+                null,
+                null,
+                r -> r.withBlockStrategy(noSleepBlockStrategy)),
+            extIdNotesFactory,
+            ident,
+            ident,
+            Runnables.doNothing(),
+            () -> {
+              try {
+                accountsUpdate
+                    .create()
+                    .update(
+                        "Update External ID",
+                        accountId,
+                        u -> u.replaceExternalId(extIdA1, extIdA2));
+              } catch (IOException | ConfigInvalidException | OrmException e) {
+                // Ignore, the expected exception is asserted later
+              }
+            });
+    assertThat(bgCounterA1.get()).isEqualTo(0);
+    assertThat(bgCounterA2.get()).isEqualTo(0);
+    assertThat(
+            gApi.accounts()
+                .id(accountId.get())
+                .getExternalIds()
+                .stream()
+                .map(i -> i.identity)
+                .collect(toSet()))
+        .containsExactly(extIdA1.key().get());
+
+    ExternalId extIdB1 = ExternalId.create("foo", "B-1", accountId);
+    ExternalId extIdB2 = ExternalId.create("foo", "B-2", accountId);
+    Optional<AccountState> updatedAccount =
+        update.update(
+            "Update External ID",
+            accountId,
+            (a, u) -> {
+              if (a.getExternalIds().contains(extIdA1)) {
+                bgCounterA1.getAndIncrement();
+                u.replaceExternalId(extIdA1, extIdB1);
+              }
+
+              if (a.getExternalIds().contains(extIdA2)) {
+                bgCounterA2.getAndIncrement();
+                u.replaceExternalId(extIdA2, extIdB2);
+              }
+            });
+
+    assertThat(bgCounterA1.get()).isEqualTo(1);
+    assertThat(bgCounterA2.get()).isEqualTo(1);
+
+    assertThat(updatedAccount.isPresent()).isTrue();
+    assertThat(updatedAccount.get().getExternalIds()).containsExactly(extIdB2);
+    assertThat(accounts.get(accountId).get().getExternalIds()).containsExactly(extIdB2);
+    assertThat(
+            gApi.accounts()
+                .id(accountId.get())
+                .getExternalIds()
+                .stream()
+                .map(i -> i.identity)
+                .collect(toSet()))
+        .containsExactly(extIdB2.key().get());
+  }
+
+  @Test
   public void stalenessChecker() throws Exception {
     // Newly created account is not stale.
     AccountInfo accountInfo = gApi.accounts().create(name("foo")).get();
diff --git a/javatests/com/google/gerrit/acceptance/api/config/EditPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/config/EditPreferencesIT.java
new file mode 100644
index 0000000..e89aa3d
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/config/EditPreferencesIT.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.config;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.AssertUtil.assertPrefs;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import org.junit.Test;
+
+@NoHttpd
+public class EditPreferencesIT extends AbstractDaemonTest {
+
+  @Test
+  public void getEditPreferences() throws Exception {
+    EditPreferencesInfo result = gApi.config().server().getDefaultEditPreferences();
+    assertPrefs(result, EditPreferencesInfo.defaults());
+  }
+
+  @Test
+  public void setEditPreferences() throws Exception {
+    int newLineLength = EditPreferencesInfo.defaults().lineLength + 10;
+    EditPreferencesInfo update = new EditPreferencesInfo();
+    update.lineLength = newLineLength;
+    EditPreferencesInfo result = gApi.config().server().setDefaultEditPreferences(update);
+    assertThat(result.lineLength).named("lineLength").isEqualTo(newLineLength);
+
+    result = gApi.config().server().getDefaultEditPreferences();
+    EditPreferencesInfo expected = EditPreferencesInfo.defaults();
+    expected.lineLength = newLineLength;
+    assertPrefs(result, expected);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/BUILD b/javatests/com/google/gerrit/acceptance/ssh/BUILD
index ec386a5..87b2920 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/BUILD
+++ b/javatests/com/google/gerrit/acceptance/ssh/BUILD
@@ -4,5 +4,6 @@
     srcs = glob(["*IT.java"]),
     group = "ssh",
     labels = ["ssh"],
+    vm_args = ["-Xmx512m"],
     deps = ["//lib/commons:compress"],
 )
diff --git a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
index f4f81f8..aa7a987 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -51,7 +51,7 @@
   @Test
   @GerritConfig(name = "download.archive", value = "off")
   public void archiveFeatureOff() throws Exception {
-    archiveNotPermitted();
+    assertArchiveNotPermitted();
   }
 
   @Test
@@ -60,14 +60,14 @@
     values = {"tar", "tbz2", "tgz", "txz"}
   )
   public void zipFormatDisabled() throws Exception {
-    archiveNotPermitted();
+    assertArchiveNotPermitted();
   }
 
   @Test
   public void zipFormat() throws Exception {
     PushOneCommit.Result r = createChange();
     String abbreviated = r.getCommit().abbreviate(8).name();
-    String c = command(r, abbreviated);
+    String c = command(r, "zip", abbreviated);
 
     InputStream out =
         adminSshSession.exec2("git-upload-archive " + project.get(), argumentsToInputStream(c));
@@ -99,23 +99,49 @@
         .inOrder();
   }
 
-  private String command(PushOneCommit.Result r, String abbreviated) {
+  // Make sure we have coverage for the dependency on xz.
+  @Test
+  public void txzFormat() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String abbreviated = r.getCommit().abbreviate(8).name();
+    String c = command(r, "tar.xz", abbreviated);
+
+    try (InputStream out =
+        adminSshSession.exec2("git-upload-archive " + project.get(), argumentsToInputStream(c))) {
+
+      // Wrap with PacketLineIn to read ACK bytes from output stream
+      PacketLineIn in = new PacketLineIn(out);
+      String packet = in.readString();
+      assertThat(packet).isEqualTo("ACK");
+
+      // Discard first bit of data, which should be empty.
+      packet = in.readString();
+      assertThat(packet).isEmpty();
+
+      // Make sure the next one is not on the error channel
+      packet = in.readString();
+
+      // 1 = DATA. It would be nicer to parse the OutputStream with SideBandInputStream from JGit, but
+      // that is currently not public.
+      char channel = packet.charAt(0);
+      if (channel != 1) {
+        fail("got packet on channel " + (int) channel, packet);
+      }
+    }
+  }
+
+  private String command(PushOneCommit.Result r, String format, String abbreviated) {
     String c =
-        "-f=zip "
-            + "-9 "
-            + "--prefix="
-            + abbreviated
-            + "/ "
-            + r.getCommit().name()
-            + " "
-            + PushOneCommit.FILE_NAME;
+        String.format(
+            "-f=%s --prefix=%s/ %s %s",
+            format, abbreviated, r.getCommit().name(), PushOneCommit.FILE_NAME);
     return c;
   }
 
-  private void archiveNotPermitted() throws Exception {
+  private void assertArchiveNotPermitted() throws Exception {
     PushOneCommit.Result r = createChange();
     String abbreviated = r.getCommit().abbreviate(8).name();
-    String c = command(r, abbreviated);
+    String c = command(r, "zip", abbreviated);
 
     InputStream out =
         adminSshSession.exec2("git-upload-archive " + project.get(), argumentsToInputStream(c));
diff --git a/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index d130c20..1e5ca4f 100644
--- a/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -40,7 +40,6 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -55,7 +54,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Set;
 import org.bouncycastle.openpgp.PGPPublicKey;
 import org.bouncycastle.openpgp.PGPPublicKeyRing;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
@@ -85,8 +83,6 @@
 
   @Inject private ThreadLocalRequestContext requestContext;
 
-  @Inject private ExternalIds externalIds;
-
   private LifecycleManager lifecycle;
   private ReviewDb db;
   private Account.Id userId;
@@ -222,10 +218,12 @@
 
   @Test
   public void noExternalIds() throws Exception {
-    Set<ExternalId> extIds = externalIds.byAccount(user.getAccountId());
     accountsUpdate
         .create()
-        .update("Delete External IDs", user.getAccountId(), u -> u.deleteExternalIds(extIds));
+        .update(
+            "Delete External IDs",
+            user.getAccountId(),
+            (a, u) -> u.deleteExternalIds(a.getExternalIds()));
     reloadUser();
 
     TestKey key = validKeyWithSecondUserId();
diff --git a/javatests/com/google/gerrit/server/account/WatchConfigTest.java b/javatests/com/google/gerrit/server/account/WatchConfigTest.java
index cf61de2..fd2c108 100644
--- a/javatests/com/google/gerrit/server/account/WatchConfigTest.java
+++ b/javatests/com/google/gerrit/server/account/WatchConfigTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.WatchConfig.NotifyType;
@@ -52,7 +53,7 @@
             + "[project \"otherProject\"]\n"
             + "  notify = [NEW_PATCHSETS]\n"
             + "  notify = * [NEW_PATCHSETS, ALL_COMMENTS]\n");
-    Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
+    Map<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches =
         WatchConfig.parse(new Account.Id(1000000), cfg, this);
 
     assertThat(validationErrors).isEmpty();
diff --git a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
index 1542fe5..f0230d5 100644
--- a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
@@ -18,12 +18,10 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
 
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountState;
@@ -43,14 +41,7 @@
     String metaId = "0e39795bb25dc914118224995c53c5c36923a461";
     account.setMetaId(metaId);
     List<String> values =
-        toStrings(
-            AccountField.REF_STATE.get(
-                new AccountState(
-                    allUsersName,
-                    account,
-                    ImmutableSet.of(),
-                    ImmutableMap.of(),
-                    GeneralPreferencesInfo.defaults())));
+        toStrings(AccountField.REF_STATE.get(AccountState.forAccount(allUsersName, account)));
     assertThat(values).hasSize(1);
     String expectedValue =
         allUsersName.get() + ":" + RefNames.refsUsers(account.getId()) + ":" + metaId;
@@ -78,12 +69,7 @@
     List<String> values =
         toStrings(
             AccountField.EXTERNAL_ID_STATE.get(
-                new AccountState(
-                    null,
-                    account,
-                    ImmutableSet.of(extId1, extId2),
-                    ImmutableMap.of(),
-                    GeneralPreferencesInfo.defaults())));
+                AccountState.forAccount(null, account, ImmutableSet.of(extId1, extId2))));
     String expectedValue1 = extId1.key().sha1().name() + ":" + extId1.blobId().name();
     String expectedValue2 = extId2.key().sha1().name() + ":" + extId2.blobId().name();
     assertThat(values).containsExactly(expectedValue1, expectedValue2);
diff --git a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
index 01e8225..ae01caf 100644
--- a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -22,7 +22,6 @@
 import static org.easymock.EasyMock.verify;
 
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
@@ -30,8 +29,6 @@
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.mail.Address;
 import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -385,11 +382,6 @@
     final Account account = new Account(userId, TimeUtil.nowTs());
     account.setFullName(name);
     account.setPreferredEmail(email);
-    return new AccountState(
-        new AllUsersName(AllUsersNameProvider.DEFAULT),
-        account,
-        Collections.emptySet(),
-        new HashMap<>(),
-        GeneralPreferencesInfo.defaults());
+    return AccountState.forAccount(new AllUsersName(AllUsersNameProvider.DEFAULT), account);
   }
 }
diff --git a/javatests/com/google/gerrit/server/project/RefControlTest.java b/javatests/com/google/gerrit/server/project/RefControlTest.java
index 8892a50..643519b 100644
--- a/javatests/com/google/gerrit/server/project/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/project/RefControlTest.java
@@ -208,7 +208,6 @@
   @Inject private SingleVersionListener singleVersionListener;
   @Inject private InMemoryDatabase schemaFactory;
   @Inject private ThreadLocalRequestContext requestContext;
-  @Inject private ProjectControl.Factory projectControlFactory;
 
   @Before
   public void setUp() throws Exception {
@@ -856,7 +855,6 @@
             projectCache,
             allProjectsName,
             allUsersName,
-            projectControlFactory,
             envFactory,
             repoManager,
             rulesCache,
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
index 14662dd..350df7d 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
@@ -21,6 +21,7 @@
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-icons/gr-icons.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-permission/gr-permission.html">
 
@@ -36,22 +37,31 @@
       fieldset {
         border: 1px solid #d1d2d3;
       }
+      .name {
+        align-items: center;
+        display: flex;
+      }
       .header,
       .editingRef .editContainer,
       #deletedContainer {
-        align-items: baseline;
+        align-items: center;
         background: #f6f6f6;
         border-bottom: 1px dotted #d1d2d3;
         display: flex;
         justify-content: space-between;
-        padding: .7em .7em;
+        min-height: 3em;
+        padding: 0 .7em;
       }
       #deletedContainer {
         border-bottom: 0;
       }
+      #editRefInput {
+        width: 70%;
+      }
       .sectionContent {
         padding: .7em;
       }
+      #editBtn,
       #deletedContainer,
       .deleted #mainContainer,
       .global,
@@ -61,6 +71,9 @@
       .editContainer {
         display: none;
       }
+      .editing #editBtn {
+        display: flex;
+      }
       /* TODO @beckysiegel add back when editing allowed */
       /* .deleted #deletedContainer,
       #mainContainer,
@@ -77,15 +90,18 @@
         class$="gr-form-styles [[_computeSectionClass(editing, _editingRef, _deleted)]]">
       <div id="mainContainer">
         <div class="header">
-          <span class="name">
+          <div class="name">
             <h3>[[_computeSectionName(section.id)]]</h3>
-          </span>
-          <div id="updateBtns">
             <gr-button
                 id="editBtn"
+                link
                 class$="[[_computeEditBtnClass(section.id)]]"
-                on-tap="_handleEditReference">Edit Reference</gr-button>
-            <gr-button
+                on-tap="_handleEditReference">
+              <iron-icon id="icon" icon="gr-icons:create"></iron-icon>
+            </gr-button>
+          </div>
+          <div id="updateBtns">
+          <gr-button
                 id="deleteBtn"
                 on-tap="_handleRemoveReference">Remove</gr-button>
           </div><!-- end updateBtns -->
@@ -95,10 +111,8 @@
               id="editRefInput"
               bind-value="{{section.id}}"
               is="iron-input"
-              type="text">
-          <gr-button
-              id="undoEdit"
-              on-tap="_undoReferenceEdit">Undo</gr-button>
+              type="text"
+              on-input="_handleValueChange">
         </div><!-- end editContainer -->
         <div class="sectionContent">
           <template
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
index 07a7a62..c603828 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
@@ -31,13 +31,14 @@
       section: {
         type: Object,
         notify: true,
-        observer: '_sectionChanged',
+        observer: '_updateSection',
       },
       groups: Object,
       labels: Object,
       editing: {
         type: Boolean,
         value: false,
+        observer: '_handleEditingChanged',
       },
       _originalId: String,
       _editingRef: {
@@ -55,11 +56,40 @@
       Gerrit.AccessBehavior,
     ],
 
-    _sectionChanged(section) {
+    listeners: {
+      'access-saved': '_handleAccessSaved',
+    },
+
+    _updateSection(section) {
       this._permissions = this.toSortedArray(section.value.permissions);
       this._originalId = section.id;
     },
 
+    _handleAccessSaved() {
+      // Set a new 'original' value to keep track of after the value has been
+      // saved.
+      this._updateSection(this.section);
+    },
+
+    _handleValueChange() {
+      this.section.value.modified = this.section.id !== this._originalId;
+      this.section.value.updatedId = this.section.id;
+
+      // Allows overall access page to know a change has been made.
+      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+    },
+
+    _handleEditingChanged(editing, editingOld) {
+      // Ignore when editing gets set initially.
+      if (!editingOld) { return; }
+      // Restore original values if no longer editing.
+      if (!editing) {
+        this._editingRef = false;
+        // Restore section ref.
+        this.set(['section', 'id'], this._originalId);
+      }
+    },
+
     _computePermissions(name, capabilities, labels) {
       let allPermissions;
       if (name === GLOBAL_NAME) {
@@ -141,11 +171,6 @@
       this._editingRef = true;
     },
 
-    _undoReferenceEdit() {
-      this._editingRef = false;
-      this.set('section.id', this._originalId);
-    },
-
     _computeSectionClass(editing, editingRef, deleted) {
       const classList = [];
       if (editing) {
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
index 38c5d67..fb07f7a 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
@@ -88,12 +88,12 @@
             default_value: 0,
           },
         };
-        element._sectionChanged(element.section);
+        element._updateSection(element.section);
         flushAsynchronousOperations();
       });
 
-      test('_sectionChanged', () => {
-        // _sectionChanged was called in setup, so just make assertions.
+      test('_updateSection', () => {
+        // _updateSection was called in setup, so just make assertions.
         const expectedPermissions = [
           {
             id: 'read',
@@ -128,6 +128,13 @@
             expectedLabelOptions);
       });
 
+      test('_handleAccessSaved', () => {
+        assert.equal(element._originalId, 'refs/*');
+        element.section.id = 'refs/for/bar';
+        element._handleAccessSaved();
+        assert.equal(element._originalId, 'refs/for/bar');
+      });
+
       test('_computePermissions', () => {
         sandbox.stub(element, 'toSortedArray').returns(
             [{
@@ -262,15 +269,6 @@
         assert.isTrue(element._editingRef);
       });
 
-      test('_undoReferenceEdit', () => {
-        element._originalId = 'refs/for/old';
-        element.section.id = 'refs/for/new';
-        element.editing = true;
-        element._undoReferenceEdit();
-        assert.isFalse(element._editingRef);
-        assert.equal(element.section.id, 'refs/for/old');
-      });
-
       test('_computeSectionClass', () => {
         let editingRef = false;
         let editing = false;
@@ -348,7 +346,7 @@
               name: 'Create Account',
             },
           };
-          element._sectionChanged(element.section);
+          element._updateSection(element.section);
           flushAsynchronousOperations();
         });
 
@@ -372,7 +370,7 @@
             },
           };
           element.capabilities = {};
-          element._sectionChanged(element.section);
+          element._updateSection(element.section);
           flushAsynchronousOperations();
         });
 
@@ -429,6 +427,7 @@
         });
 
         test('edit section reference', () => {
+          element.section = {id: 'refs/for/bar', value: {permissions: {}}};
           assert.isFalse(element.$.section.classList.contains('editing'));
           element.editing = true;
           assert.isTrue(element.$.section.classList.contains('editing'));
@@ -439,11 +438,25 @@
           assert.equal(element.section.id, 'new/ref');
           assert.isTrue(element._editingRef);
           assert.isTrue(element.$.section.classList.contains('editingRef'));
-          MockInteractions.tap(element.$.undoEdit);
-          flushAsynchronousOperations();
+          element.editing = false;
           assert.isFalse(element._editingRef);
-          assert.isFalse(element.$.section.classList.contains('editingRef'));
-          assert.equal(element.section.id, 'refs/*');
+          assert.equal(element.section.id, 'refs/for/bar');
+        });
+
+        test('_handleValueChange', () => {
+          const modifiedHandler = sandbox.stub();
+          element.section = {id: 'refs/for/bar', value: {permissions: {}}};
+          assert.notOk(element.section.value.updatedId);
+          element.section.id = 'refs/for/baz';
+          element.addEventListener('access-modified', modifiedHandler);
+          assert.isNotOk(element.section.value.modified);
+          element._handleValueChange();
+          assert.equal(element.section.value.updatedId, 'refs/for/baz');
+          assert.isTrue(element.section.value.modified);
+          assert.isTrue(modifiedHandler.called);
+          element.section.id = 'refs/for/bar';
+          element._handleValueChange();
+          assert.isFalse(element.section.value.modified);
         });
 
         test('remove section', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
index d903404..bf5ad0b 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -16,6 +16,14 @@
 
   const Defs = {};
 
+  const NOTHING_TO_SAVE = 'No changes to save.';
+
+  /**
+   * Fired when save is a no-op
+   *
+   * @event show-alert
+   */
+
   /**
    * @typedef {{
    *    value: !Object,
@@ -42,14 +50,14 @@
   /**
    * Can be an empty object or consist of permissions.
    *
-   * @typedef {Object<string, Defs.permissions>}
+   * @typedef {!Object<string, Defs.permissions>}
    */
   Defs.sections;
 
   /**
    * @typedef {{
-   *    remove: Defs.sections,
-   *    add: Defs.sections,
+   *    remove: !Defs.sections,
+   *    add: !Defs.sections,
    * }}
    */
   Defs.projectAccessInput;
@@ -156,13 +164,12 @@
       for (const item of path) {
         if (!curPos[item]) {
           if (item === path[path.length - 1] && type === 'remove') {
-            // TODO(beckysiegel) This if statement should be removed when
-            // https://gerrit-review.googlesource.com/c/gerrit/+/150851
-            // is live.
             if (path[path.length - 2] === 'permissions') {
               curPos[item] = {rules: {}};
+            } else if (path.length === 1) {
+              curPos[item] = {permissions: {}};
             } else {
-              curPos[item] = null;
+              curPos[item] = {};
             }
           } else if (item === path[path.length - 1] && type === 'add') {
             curPos[item] = opt_value;
@@ -175,6 +182,22 @@
       return addRemoveObj;
     },
 
+    /**
+     * Used to recursively remove any objects with a 'deleted' bit.
+     */
+    _recursivelyRemoveDeleted(obj) {
+      for (const k in obj) {
+        if (!obj.hasOwnProperty(k)) { return; }
+        if (typeof obj[k] == 'object') {
+          if (obj[k].deleted) {
+            delete obj[k];
+            return;
+          }
+          this._recursivelyRemoveDeleted(obj[k]);
+        }
+      }
+    },
+
     _recursivelyUpdateAddRemoveObj(obj, addRemoveObj, path = []) {
       for (const k in obj) {
         if (!obj.hasOwnProperty(k)) { return; }
@@ -186,11 +209,24 @@
           } else if (obj[k].modified) {
             this._updateAddRemoveObj(addRemoveObj,
                 path.concat(k), 'remove');
-            this._updateAddRemoveObj(addRemoveObj,
-                path.concat(k), 'add', obj[k]);
+
+            const updatedId = obj[k].updatedId;
+            const ref = updatedId ? updatedId : k;
+            this._updateAddRemoveObj(addRemoveObj, path.concat(ref), 'add',
+                obj[k]);
+            /* Special case for ref changes because they need to be added and
+             removed in a different way. The new ref needs to include all
+             changes but also the initial state. To do this, instead of
+             continuing with the same recursion, just remove anything that is
+             deleted in the current state. */
+            if (updatedId && updatedId !== k) {
+              this._recursivelyRemoveDeleted(addRemoveObj.add[updatedId]);
+            }
+            continue;
           } else if (obj[k].added) {
             this._updateAddRemoveObj(addRemoveObj,
                 path.concat(k), 'add', obj[k]);
+            continue;
           }
           this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj,
               path.concat(k));
@@ -215,9 +251,15 @@
     },
 
     _handleSaveForReview() {
-      // Use saving rather than editing here because rules have to handle
-      // save prior to toggling editing.
       const addRemoveObj = this._computeAddAndRemove();
+
+      // If there are no changes, don't actually save.
+      if (!Object.keys(addRemoveObj.add).length &&
+          !Object.keys(addRemoveObj.remove).length) {
+        this.dispatchEvent(new CustomEvent('show-alert',
+            {detail: {message: NOTHING_TO_SAVE}, bubbles: true}));
+        return;
+      }
       return this.$.restAPI.setProjectAccessRightsForReview(this.repo, {
         add: addRemoveObj.add,
         remove: addRemoveObj.remove,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
index b26121c..12d2e4a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
@@ -227,7 +227,52 @@
         assert.isTrue(element._handleAccessModified.called);
       });
 
-      test('_computeAddAndRemove rules', () => {
+      test('_handleAccessModified called with event fired', () => {
+        const saveStub =
+            sandbox.stub(element.$.restAPI, 'setProjectAccessRightsForReview');
+        sandbox.stub(element, '_computeAddAndRemove').returns({
+          add: {},
+          remove: {},
+        });
+        element._handleSaveForReview();
+        assert.isFalse(saveStub.called);
+      });
+
+      test('_recursivelyRemoveDeleted', () => {
+        const obj = {
+          'refs/*': {
+            permissions: {
+              owner: {
+                rules: {
+                  234: {action: 'ALLOW'},
+                  123: {action: 'DENY', deleted: true},
+                },
+              },
+              read: {
+                deleted: true,
+                rules: {
+                  234: {action: 'ALLOW'},
+                },
+              },
+            },
+          },
+        };
+        const expectedResult = {
+          'refs/*': {
+            permissions: {
+              owner: {
+                rules: {
+                  234: {action: 'ALLOW'},
+                },
+              },
+            },
+          },
+        };
+        element._recursivelyRemoveDeleted(obj);
+        assert.deepEqual(obj, expectedResult);
+      });
+
+      test('_handleSaveForReview with no changes', () => {
         element._local = JSON.parse(JSON.stringify(accessRes.local));
         assert.deepEqual(element._computeAddAndRemove(), {add: {}, remove: {}});
         element._local['refs/*'].permissions.owner.rules[123].deleted = true;
@@ -238,7 +283,7 @@
               permissions: {
                 owner: {
                   rules: {
-                    123: null,
+                    123: {},
                   },
                 },
               },
@@ -265,7 +310,7 @@
               permissions: {
                 owner: {
                   rules: {
-                    123: null,
+                    123: {},
                   },
                 },
               },
@@ -317,6 +362,40 @@
         assert.deepEqual(element._computeAddAndRemove(), expectedInput);
       });
 
+      test('_computeAddAndRemove sections', () => {
+        element._local = JSON.parse(JSON.stringify(accessRes.local));
+        assert.deepEqual(element._computeAddAndRemove(), {add: {}, remove: {}});
+        element._local['refs/*'].updatedId = 'refs/for/bar';
+        element._local['refs/*'].modified = true;
+        const expectedInput = {
+          add: {
+            'refs/for/bar': {
+              modified: true,
+              updatedId: 'refs/for/bar',
+              permissions: {
+                owner: {
+                  rules: {
+                    234: {action: 'ALLOW'},
+                    123: {action: 'DENY'},
+                  },
+                },
+                read: {
+                  rules: {
+                    234: {action: 'ALLOW'},
+                  },
+                },
+              },
+            },
+          },
+          remove: {
+            'refs/*': {
+              permissions: {},
+            },
+          },
+        };
+        assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+      });
+
       test('_computeAddAndRemove combinations', () => {
         // Modify rule and delete permission that it is inside of.
         element._local = JSON.parse(JSON.stringify(accessRes.local));
@@ -393,6 +472,34 @@
           },
         };
         assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+        // Change one of the refs
+        element._local['refs/*'].updatedId = 'refs/for/bar';
+        element._local['refs/*'].modified = true;
+
+        expectedInput = {
+          add: {
+            'refs/for/bar': {
+              modified: true,
+              updatedId: 'refs/for/bar',
+              permissions: {
+                read: {
+                  exclusive: true,
+                  modified: true,
+                  rules: {
+                    234: {action: 'ALLOW'},
+                  },
+                },
+              },
+            },
+          },
+          remove: {
+            'refs/*': {
+              permissions: {},
+            },
+          },
+        };
+        assert.deepEqual(element._computeAddAndRemove(), expectedInput);
       });
 
       test('_handleSaveForReview', done => {
@@ -413,7 +520,7 @@
               permissions: {
                 owner: {
                   rules: {
-                    123: null,
+                    123: {},
                   },
                 },
               },
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 6370374..5786264 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -94,6 +94,10 @@
         },
       };
       sandbox = sinon.sandbox.create();
+      sandbox.stub(element.$.confirmCherrypick.$.restAPI,
+          'getRepoBranches').returns(Promise.resolve([]));
+      sandbox.stub(element.$.confirmMove.$.restAPI,
+          'getRepoBranches').returns(Promise.resolve([]));
 
       return element.reload();
     });
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 5f86514..cec91d5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -134,7 +134,7 @@
         max-width: var(--commit-message-max-width, 72ch);;
       }
       .commitMessage gr-linked-text {
-        word-break: break-all;
+        word-break: break-word;
       }
       #commitMessageEditor {
         min-width: 72ch;
@@ -374,7 +374,7 @@
         <div class="changeInfo-column changeMetadata hideOnMobileOverlay">
           <gr-change-metadata
               change="{{_change}}"
-              revision="[[_currentRevision]]"
+              revision="[[_selectedRevision]]"
               commit-info="[[_commitInfo]]"
               server-config="[[_serverConfig]]"
               missing-labels="[[_missingLabels]]"
@@ -510,7 +510,7 @@
       <gr-endpoint-decorator name="change-view-integration">
         <gr-endpoint-param name="change" value="[[_change]]">
         </gr-endpoint-param>
-        <gr-endpoint-param name="revision" value="[[_currentRevision]]">
+        <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
         </gr-endpoint-param>
       </gr-endpoint-decorator>
       <gr-messages-list id="messageList"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 1800175..0914fc8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -152,7 +152,8 @@
         type: Object,
       },
       _filesExpanded: String,
-      _currentRevision: Object,
+      _basePatchNum: String,
+      _selectedRevision: Object,
       _currentRevisionActions: Object,
       _allPatchSets: {
         type: Array,
@@ -236,6 +237,7 @@
     observers: [
       '_labelsChanged(_change.labels.*)',
       '_paramsAndChangeChanged(params, _change)',
+      '_patchNumChanged(_patchRange.patchNum)',
     ],
 
     keyBindings: {
@@ -1004,7 +1006,6 @@
                 parseInt(lineHeight.slice(0, lineHeight.length - 2), 10);
 
             this._change = change;
-            this._currentRevision = currentRevision;
             if (!this._patchRange || !this._patchRange.patchNum ||
                 this.patchNumEquals(this._patchRange.patchNum,
                     currentRevision._number)) {
@@ -1015,7 +1016,13 @@
               this._commitInfo = currentRevision.commit;
               this._currentRevisionActions =
                       this._updateRebaseAction(currentRevision.actions);
-                  // TODO: Fetch and process files.
+              this._selectedRevision = currentRevision;
+              // TODO: Fetch and process files.
+            } else {
+              this._selectedRevision =
+                Object.values(this._change.revisions).find(
+                    revision => revision._number ===
+                      parseInt(this._patchRange.patchNum, 10));
             }
           });
     },
@@ -1371,5 +1378,17 @@
     _computeCommitMessageKey(number, revision) {
       return `c${number}_rev${revision}`;
     },
+
+    _patchNumChanged(patchNumStr) {
+      if (!this._selectedRevision) {
+        return;
+      }
+      const patchNum = parseInt(patchNumStr, 10);
+      if (patchNum === this._selectedRevision._number) {
+        return;
+      }
+      this._selectedRevision = Object.values(this._change.revisions).find(
+          revision => revision._number === patchNum);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 7d4a54b..7555a5a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -1437,10 +1437,34 @@
       assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.called);
     });
 
+    test('_selectedRevision updates when patchNum is changed', () => {
+      const revision1 = {_number: 1, commit: {}};
+      const revision2 = {_number: 2, commit: {}};
+      sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
+          Promise.resolve({
+            revisions: {
+              aaa: revision1,
+              bbb: revision2,
+            },
+            labels: {},
+            actions: {},
+            current_revision: 'bbb',
+            change_id: 'loremipsumdolorsitamet',
+          }));
+      sandbox.stub(element, '_getEdit').returns(Promise.resolve());
+      element._patchRange = {patchNum: '2'};
+      return element._getChangeDetail().then(() => {
+        assert.strictEqual(element._selectedRevision, revision2);
+
+        element.set('_patchRange.patchNum', '1');
+        assert.strictEqual(element._selectedRevision, revision1);
+      });
+    });
+
     suite('plugin endpoints', () => {
       test('endpoint params', done => {
         element._change = {labels: {}};
-        element._currentRevision = {};
+        element._selectedRevision = {};
         let hookEl;
         let plugin;
         Gerrit.install(
@@ -1454,7 +1478,7 @@
         flush(() => {
           assert.strictEqual(hookEl.plugin, plugin);
           assert.strictEqual(hookEl.change, element._change);
-          assert.strictEqual(hookEl.revision, element._currentRevision);
+          assert.strictEqual(hookEl.revision, element._selectedRevision);
           done();
         });
       });
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index 1b5d6f9..6ce6a1e 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -55,11 +55,7 @@
       }
     </style>
     <template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file">
-      <div class="file">
-        <a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">
-          [[computeDisplayPath(file)]]
-        </a>:
-      </div>
+      <div class="file">[[computeDisplayPath(file)]]:</div>
       <template is="dom-repeat"
                 items="[[_computeCommentsForFile(comments, file)]]" as="comment">
         <div class="container">
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index 7fa8c7a..b089ae5 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -36,11 +36,6 @@
       return arr.sort(this.specialFilePathCompare);
     },
 
-    _computeFileDiffURL(file, changeNum, patchNum) {
-      return Gerrit.Nav.getUrlForDiffById(this.changeNum, this.projectName,
-          file, patchNum);
-    },
-
     _isOnParent(comment) {
       return comment.side === 'PARENT';
     },
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 8ce0e9d..b1d7b2e 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
@@ -58,6 +58,12 @@
       :host(.editLoaded) .showOnEdit {
         display: initial;
       }
+      .controlRow {
+        align-items: center;
+        display: flex;
+        height: 2.25em;
+        justify-content: center;
+      }
       .reviewed,
       .status {
         align-items: center;
@@ -114,7 +120,7 @@
         min-width: 7em;
       }
       .invisible {
-        visibility: hidden;
+        display: none;
       }
       .row:not(.header) .stats,
       .total-stats {
@@ -383,25 +389,25 @@
         </span>
       </div>
     </div>
-    <gr-button
-        class="fileListButton"
-        id="incrementButton"
-        hidden$="[[_computeFileListButtonHidden(numFilesShown, _files)]]"
-        link on-tap="_incrementNumFilesShown">
-      [[_computeIncrementText(numFilesShown, _files)]]
-    </gr-button>
-    <gr-tooltip-content
-        has-tooltip="[[_computeWarnShowAll(_files)]]"
-        show-icon="[[_computeWarnShowAll(_files)]]"
-        title$="[[_computeShowAllWarning(_files)]]">
+    <div class$="row controlRow [[_computeFileListControlClass(numFilesShown, _files)]]">
       <gr-button
           class="fileListButton"
-          id="showAllButton"
-          hidden$="[[_computeFileListButtonHidden(numFilesShown, _files)]]"
-          link on-tap="_showAllFiles">
-        [[_computeShowAllText(_files)]]
-      </gr-button><!--
- --></gr-tooltip-content>
+          id="incrementButton"
+          link on-tap="_incrementNumFilesShown">
+        [[_computeIncrementText(numFilesShown, _files)]]
+      </gr-button>
+      <gr-tooltip-content
+          has-tooltip="[[_computeWarnShowAll(_files)]]"
+          show-icon="[[_computeWarnShowAll(_files)]]"
+          title$="[[_computeShowAllWarning(_files)]]">
+        <gr-button
+            class="fileListButton"
+            id="showAllButton"
+            link on-tap="_showAllFiles">
+          [[_computeShowAllText(_files)]]
+        </gr-button><!--
+  --></gr-tooltip-content>
+    </div>
     <gr-diff-preferences
         id="diffPreferences"
         prefs="{{diffPrefs}}"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 66e3db7..70cafe9 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -747,8 +747,8 @@
       this.numFilesShown += this.fileListIncrement;
     },
 
-    _computeFileListButtonHidden(numFilesShown, files) {
-      return numFilesShown >= files.length;
+    _computeFileListControlClass(numFilesShown, files) {
+      return numFilesShown >= files.length ? 'invisible' : '';
     },
 
     _computeIncrementText(numFilesShown, files) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 381a304..d04098c 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -96,13 +96,28 @@
     });
 
     test('correct number of files are shown', () => {
+      element.fileListIncrement = 300;
       element._files = _.times(500, i => {
         return {__path: '/file' + i, lines_inserted: 9};
       });
       flushAsynchronousOperations();
+
       assert.equal(
           Polymer.dom(element.root).querySelectorAll('.file-row').length,
           element.numFilesShown);
+      const controlRow = element.$$('.controlRow');
+      assert.isFalse(controlRow.classList.contains('invisible'));
+      assert.equal(element.$.incrementButton.textContent.trim(),
+          'Show 300 more');
+      assert.equal(element.$.showAllButton.textContent.trim(),
+          'Show all 500 files');
+
+      MockInteractions.tap(element.$.showAllButton);
+      flushAsynchronousOperations();
+
+      assert.equal(element.numFilesShown, 500);
+      assert.equal(element._shownFiles.length, 500);
+      assert.isTrue(controlRow.classList.contains('invisible'));
     });
 
     test('calculate totals for patch number', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index 9ae8ff2..e45702c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -298,7 +298,6 @@
     _computeSaveDisabled(draft, comment, resolved) {
       // If resolved state has changed and a msg exists, save should be enabled.
       if (comment.unresolved === resolved && draft) { return false; }
-      if (comment.message) { return draft === comment.message; }
       return !draft || draft.trim() === '';
     },
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index 8a2886c..c09e289 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -520,9 +520,9 @@
       const msgComment = {message: 'test', unresolved: true};
       assert.equal(element._computeSaveDisabled('', comment, false), true);
       assert.equal(element._computeSaveDisabled('test', comment, false), false);
-      assert.equal(element._computeSaveDisabled('', msgComment, false), false);
+      assert.equal(element._computeSaveDisabled('', msgComment, false), true);
       assert.equal(
-          element._computeSaveDisabled('test', msgComment, false), true);
+          element._computeSaveDisabled('test', msgComment, false), false);
       assert.equal(
           element._computeSaveDisabled('test2', msgComment, false), false);
       assert.equal(element._computeSaveDisabled('test', comment, true), false);
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
index b43e994..847b49b 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
@@ -23,6 +23,8 @@
       <g id="search"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></g>
       <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
       <g id="settings"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/></g>
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <g id="create"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></g>
       <!-- This is a custom PolyGerrit SVG -->
       <g id="side-by-side"><path d="M17.1578947,10.8888889 L2.84210526,10.8888889 C2.37894737,10.8888889 2,11.2888889 2,11.7777778 L2,17.1111111 C2,17.6 2.37894737,18 2.84210526,18 L17.1578947,18 C17.6210526,18 18,17.6 18,17.1111111 L18,11.7777778 C18,11.2888889 17.6210526,10.8888889 17.1578947,10.8888889 Z M17.1578947,2 L2.84210526,2 C2.37894737,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.37894737,9.11111111 2.84210526,9.11111111 L17.1578947,9.11111111 C17.6210526,9.11111111 18,8.71111111 18,8.22222222 L18,2.88888889 C18,2.4 17.6210526,2 17.1578947,2 Z M16.1973628,2 L2.78874238,2 C2.35493407,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.35493407,9.11111111 2.78874238,9.11111111 L16.1973628,9.11111111 C16.6311711,9.11111111 16.9861052,8.71111111 16.9861052,8.22222222 L16.9861052,2.88888889 C16.9861052,2.4 16.6311711,2 16.1973628,2 Z" id="Shape" transform="scale(1.2) translate(10.000000, 10.000000) rotate(-90.000000) translate(-10.000000, -10.000000)"/></g>
       <!-- This is a custom PolyGerrit SVG -->