Merge "Update rebase dialog to give parent autocomplete suggestions"
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index bf9cb6d..a4f03d5 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1299,7 +1299,8 @@
 
 Allow to link:cmd-set-account.html[modify accounts over the ssh prompt].
 This capability allows the granted group members to modify any user account
-setting.
+setting. In addition this capability is required to view secondary emails
+of other accounts.
 
 [[capability_priority]]
 === Priority
@@ -1386,6 +1387,10 @@
 of link:config-gerrit.html#accounts.visibility[accounts.visibility]
 setting.
 
+This capability allows to view all accounts but not all account data.
+E.g. secondary emails of all accounts can only be viewed with the
+link:#capability_modifyAccount[Modify Account] capability.
+
 
 [[capability_viewCaches]]
 === View Caches
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 91a837d..fd8c3fe 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1252,6 +1252,12 @@
 +
 The default limit is 1024kB.
 
+[[change.disablePrivateChanges]]change.disablePrivateChanges::
++
+If set to true, users are not allowed to create private changes.
++
+The default is false.
+
 [[changeCleanup]]
 === Section changeCleanup
 
@@ -1380,10 +1386,13 @@
 example, to match the string `bug` in a case insensitive way the match
 pattern `[bB][uU][gG]` needs to be used.
 +
-The regular expression pattern is applied to the HTML form of the message
-in question, which means it needs to assume the data has been escaped.
-So `"` needs to be matched as `&amp;quot;`, `<` as `&amp;lt;`, and `'` as
-`&amp;#39;`.
+Between the GWT UI and PolyGerrit, the commentlink.name.match regular
+expressions are applied differently. Whereas in the GWT UI the
+expressions are applied to the formatted and escaped HTML result, the
+PolyGerrit UI applies them only to the raw, unformatted and unescaped
+text form. PolyGerrit does not support regex matching against HTML.
+Comment link patterns that are written in this style should be updated
+to match text formats.
 +
 A common pattern to match is `bug\\s+(\\d+)`.
 
@@ -3745,12 +3754,15 @@
 [[repository.name.defaultSubmitType]]repository.<name>.defaultSubmitType::
 +
 The default submit type for newly created projects. Supported values
-are `MERGE_IF_NECESSARY`, `FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`,
+are `INHERIT`, `MERGE_IF_NECESSARY`, `FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`,
 `REBASE_ALWAYS`, `MERGE_ALWAYS` and `CHERRY_PICK`.
 +
 For more details see link:project-configuration.html#submit_type[Submit Types].
 +
-By default, `MERGE_IF_NECESSARY`.
+This submit type is only applied at project creation time if a submit type is
+omitted from the link:rest-api-projects.html#project-input[ProjectInput]. If the
+submit type is unset in the project config at runtime, it defaults to
+link:project-configuration.html#merge_if_necessary[`MERGE_IF_NECESSARY`].
 
 [[repository.name.ownerGroup]]repository.<name>.ownerGroup::
 +
@@ -4396,8 +4408,6 @@
 * `diffie-hellman-group1-sha1`
 
 By default, all supported key exchange algorithms are available.
-Without Bouncy Castle, `diffie-hellman-group1-sha1` is the only
-available algorithm.
 
 It is strongly recommended to disable at least `diffie-hellman-group1-sha1`
 as it's known to be vulnerable (logjam attack). Additionally, if your setup
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 3644845..3b2b65f 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -236,6 +236,10 @@
 This option only takes effect in submit strategies which already modify the commit, i.e.
 Cherry Pick, Rebase Always, and (perhaps) Rebase If Necessary.
 
+- 'rejectEmptyCommit': Defines whether empty commits should be rejected when a change is merged.
+Changes might not seem empty at first but when attempting to merge, rebasing can lead to an empty
+commit. If this option is set to 'true' the merge would fail.
+
 Merge strategy
 
 
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index fc71d26..b482de1 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -1,13 +1,14 @@
 = Gerrit Code Review - Building with Bazel
 
 [[installation]]
-== Installation
+== Prerequisites
 
-You need to use Java 8 and Node.js for building gerrit.
+To build Gerrit from source, you need:
 
-You can install Bazel from the bazel.io:
-https://www.bazel.io/versions/master/docs/install.html
-
+* A Linux or macOS system (Windows is not supported at this time)
+* A JDK for Java 8
+* Node.js
+* link:https://www.bazel.io/versions/master/docs/install.html[Bazel]
 
 [[build]]
 == Building on the Command Line
diff --git a/Documentation/install.txt b/Documentation/install.txt
index c6977a4..0f121a0 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -175,8 +175,8 @@
 [[installation_on_windows]]
 == Installation on Windows
 
-If new site is going to be initialized with Bouncy Castle cryptography,
-ssh-keygen command must be available during the init phase. If you have
+The `ssh-keygen` command must be available during the init phase to
+generate SSH host keys. If you have
 link:https://git-for-windows.github.io/[Git for Windows] installed,
 start Command Prompt and temporary add directory with ssh-keygen to the
 PATH environment variable just before running init command:
diff --git a/Documentation/intro-gerrit-walkthrough.txt b/Documentation/intro-gerrit-walkthrough.txt
index 071267a..fcb4de2 100644
--- a/Documentation/intro-gerrit-walkthrough.txt
+++ b/Documentation/intro-gerrit-walkthrough.txt
@@ -182,7 +182,7 @@
 
 Later in the day, Max decides to check on his change and notices Hannah's
 feedback. He opens up the source file and incorporates her feedback. Because
-Max's change includes a change-id, all he has to is follow the typical git
+Max's change includes a change-id, all he has to do is follow the typical git
 workflow for updating a commit:
 
 * Check out the commit
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 6242fcf..b20c2c0 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -531,8 +531,10 @@
 [[private-changes]]
 == Private Changes
 
-Private changes are changes that are only visible to their owners and
-reviewers. Private changes are useful in a number of cases:
+Private changes are changes that are only visible to their owners, reviewers
+and users with the link:access-control.html#category_view_private_changes[
+View Private Changes] global capability. Private changes are useful in a number
+of cases:
 
 * You want to check what the change looks like before formal review starts.
   By marking the change private without reviewers, nobody can
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt
index 079ff49..6260f5b 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -57,6 +57,12 @@
 its dependencies are also submitted, with exceptions documented below.
 The following submit types are supported:
 
+[[submit_type_inherit]]
+* Inherit
++
+Inherit the submit type from the parent project. In `All-Projects`, this
+is equivalent to link:#merge_if_necessary[Merge If Necessary].
+
 [[fast_forward_only]]
 * Fast Forward Only
 +
@@ -70,7 +76,8 @@
 [[merge_if_necessary]]
 * Merge If Necessary
 +
-This is the default for a new project.
+This is the default for new projects, unless overridden by a global
+link:config-gerrit.html#repository.name.defaultSubmitType[`defaultSubmitType` option].
 +
 If the change being submitted is a strict superset of the destination
 branch, then the branch is fast-forwarded to the change.  If not,
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 8dc3b9d..5912d1f 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -63,7 +63,9 @@
 
 [[all-emails]]
 --
-* `ALL_EMAILS`: Includes all registered emails.
+* `ALL_EMAILS`: Includes all registered emails. Requires the caller
+to have the link:access-control.html#capability_modifyAccount[Modify
+Account] global capability.
 --
 
 [[suggest-account]]
@@ -79,6 +81,10 @@
   GET /accounts/?suggest&q=John HTTP/1.0
 ----
 
+Secondary emails are only included if the calling user has the
+link:access-control.html#capability_modifyAccount[Modify Account]
+capability.
+
 .Response
 ----
   HTTP/1.1 200 OK
@@ -2159,7 +2165,10 @@
 |`secondary_emails`|optional|
 A list of the secondary email addresses of the user. +
 Only set for account queries when the link:#all-emails[ALL_EMAILS]
-option is set.
+option or the link:#suggest-account[suggest] parameter is set. +
+Secondary emails are only included if the calling user has the
+link:access-control.html#capability_modifyAccount[Modify Account], and
+hence is allowed to see secondary emails of other users.
 |`username`        |optional|The username of the user. +
 Only set if detailed account information is requested. +
 See option link:rest-api-changes.html#detailed-accounts[
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 1606b8a..0c30a4b 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -301,6 +301,12 @@
     link:user-search.html#reviewedby[reviewedby:self].
 --
 
+[[skip_mergeable]]
+--
+* `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]]
 --
 * `SUBMITTABLE`: include the `submittable` field in link:#change-info[ChangeInfo],
@@ -5658,7 +5664,8 @@
 Not set for merged changes.
 |`mergeable`          |optional|
 Whether the change is mergeable. +
-Not set for merged changes, or if the change has not yet been tested.
+Not set for merged changes, if the change has not yet been tested, or
+if the link:#skip_mergeable[skip_mergeable] option is set.
 |`submittable`        |optional|
 Whether the change has been approved by the project submit rules. +
 Only set if link:#submittable[requested].
@@ -6709,12 +6716,11 @@
 Draft handling that defines how draft comments are handled that are
 already in the database but that were not also described in this
 input. +
-Allowed values are `DELETE`, `PUBLISH`, `PUBLISH_ALL_REVISIONS` and
-`KEEP`. All values except `PUBLISH_ALL_REVISIONS` operate only on drafts
-for a single revision. +
+Allowed values are `PUBLISH`, `PUBLISH_ALL_REVISIONS` and `KEEP`. All values
+except `PUBLISH_ALL_REVISIONS` operate only on drafts for a single revision. +
 Only `KEEP` is allowed when used in conjunction with `on_behalf_of`. +
-If not set, the default is `DELETE`, unless `on_behalf_of` is set, in
-which case the default is `KEEP` and any other value is disallowed.
+If not set, the default is `KEEP`. If `on_behalf_of` is set, then no other value
+besides `KEEP` is allowed.
 |`notify`                 |optional|
 Notify handling that defines to whom email notifications should be sent
 after the review is stored. +
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 6e02786..8aa1f42 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1398,6 +1398,8 @@
 |`submit_whole_topic` ||
 link:config-gerrit.html#change.submitWholeTopic[A configuration if
 the whole topic is submitted].
+|`disable_private_changes` |not set if `false`|
+Returns true if private changes are disabled.
 |=============================
 
 [[check-account-external-ids-input]]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index fec430f..34c0e72 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -494,7 +494,7 @@
 
   {
     "description": "This is a demo project.",
-    "submit_type": "CHERRY_PICK",
+    "submit_type": "INHERIT",
     "owners": [
       "MyProject-Owners"
     ]
@@ -821,7 +821,12 @@
       "configured_value": "15m",
       "inherited_value": "20m"
     },
-    "submit_type": "MERGE_IF_NECESSARY",
+    "submit_type": "INHERIT",
+    "default_submit_type": {
+      "value": "MERGE_IF_NECESSARY",
+      "configured_value": "INHERIT",
+      "inherited_value": "MERGE_IF_NECESSARY"
+    },
     "state": "ACTIVE",
     "commentlinks": {},
     "plugin_config": {
@@ -933,6 +938,11 @@
       "inherited_value": "20m"
     },
     "submit_type": "REBASE_IF_NECESSARY",
+    "default_submit_type": {
+      "value": "REBASE_IF_NECESSARY",
+      "configured_value": "INHERIT",
+      "inherited_value": "REBASE_IF_NECESSARY"
+    },
     "state": "ACTIVE",
     "commentlinks": {}
   }
@@ -1072,7 +1082,9 @@
 
 Lists the access rights for a single project.
 
-As result a link:#project-access-info[ProjectAccessInfo] entity is returned.
+As result a
+link:rest-api-access.html#project-access-info[ProjectAccessInfo]
+entity is returned.
 
 .Request
 ----
@@ -1156,7 +1168,9 @@
 
 After removals have been applied, additions will be applied.
 
-As result a link:#project-access-info[ProjectAccessInfo] entity is returned.
+As result a
+link:rest-api-access.html#project-access-info[ProjectAccessInfo]
+entity is returned.
 
 .Request
 ----
@@ -2846,10 +2860,12 @@
 The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
 limit] of this project as a link:#max-object-size-limit-info[
 MaxObjectSizeLimitInfo] entity.
+|`default_submit_type`     ||
+link:#submit-type-info[SubmitTypeInfo] that describes the default submit type of
+the project, when not overridden at the change level.
 |`submit_type`               ||
-The default submit type of the project, can be `MERGE_IF_NECESSARY`,
-`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `REBASE_ALWAYS`, `MERGE_ALWAYS` or
-`CHERRY_PICK`.
+Deprecated; equivalent to link:#submit-type-info[`value`] in
+`default_submit_type`.
 |`match_author_to_committer_date` |optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that indicates whether
 a change's author date will be changed to match its submitter date upon submit.
@@ -2872,6 +2888,9 @@
 |`actions`                                 |optional|
 Actions the caller might be able to perform on this project. The
 information is a map of view names to
+|`reject_empty_commit`                     |optional|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+empty commits should be rejected when a change is merged.
 link:rest-api-changes.html#action-info[ActionInfo] entities.
 |=======================================================
 
@@ -2936,6 +2955,10 @@
 |`plugin_config_values`                    |optional|
 Plugin configuration values as map which maps the plugin name to a map
 of parameter names to values.
+|`reject_empty_commit`                     |optional|
+Whether empty commits should be rejected when a change is merged.
+Can be `TRUE`, `FALSE` or `INHERIT`. +
+If not set, this setting is not updated.
 |======================================================
 
 [[config-parameter-info]]
@@ -3269,6 +3292,9 @@
 |`plugin_config_values`      |optional|
 Plugin configuration values as map which maps the plugin name to a map
 of parameter names to values.
+|`reject_empty_commit`       |optional|
+Whether empty commits should be rejected when a change is merged
+(`TRUE`, `FALSE`, `INHERIT`).
 |=========================================
 
 [[project-parent-input]]
@@ -3317,6 +3343,27 @@
 |`size_of_packed_objects`  |Size of packed objects in bytes.
 |======================================
 
+[[submit-type-info]]
+=== SubmitTypeInfo
+Information about the link:project-configuration.html#submit_type[default submit
+type of a project], taking into account project inheritance.
+
+Valid values for each field are `MERGE_IF_NECESSARY`, `FAST_FORWARD_ONLY`,
+`REBASE_IF_NECESSARY`, `REBASE_ALWAYS`, `MERGE_ALWAYS` or `CHERRY_PICK`, plus
+`INHERIT` where applicable.
+
+[options="header",cols="1,6"]
+|===============================
+|Field Name         |Description
+|`value`            |
+The effective submit type value. Never `INHERIT`.
+|`configured_value` |
+The configured value, can be one of the submit types, or `INHERIT` to inherit
+from the parent project.
+|`inherited_value`  |
+The effective value that would be inherited from the parent. Never `INHERIT`.
+|===============================
+
 [[tag-info]]
 === TagInfo
 The `TagInfo` entity contains information about a tag.
diff --git a/SUBMITTING_PATCHES b/SUBMITTING_PATCHES
index 553ab34..5a82fd9 100644
--- a/SUBMITTING_PATCHES
+++ b/SUBMITTING_PATCHES
@@ -3,6 +3,7 @@
  - Make small logical changes.
  - Provide a meaningful commit message.
  - Make sure all code is under the Apache License, 2.0.
+ - Make sure all commit messages have a Change-Id.
  - Publish your changes for review:
 
    git push https://gerrit.googlesource.com/gerrit HEAD:refs/for/master
@@ -67,6 +68,13 @@
 
   https://gerrit-review.googlesource.com/#/settings/http-password
 
+Ensure you have installed the commit-msg hook that automatically
+generates and inserts a Change-Id line during "git commit".  This can
+be done from the root directory of the local Git repository:
+
+   curl -Lo .git/hooks/commit-msg https://gerrit-review.googlesource.com/tools/hooks/commit-msg
+   chmod +x .git/hooks/commit-msg
+
 Push your patches over HTTPS to the review server, possibly through
 a remembered remote to make this easier in the future:
 
diff --git a/WORKSPACE b/WORKSPACE
index 21bbc8b..c16200b 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,14 +1,17 @@
 workspace(name = "gerrit")
 
-load("//:version.bzl", "check_version")
-
-check_version("0.5.3")
-
 load("//tools/bzl:maven_jar.bzl", "maven_jar", "GERRIT", "MAVEN_LOCAL")
 load("//lib/codemirror:cm.bzl", "CM_VERSION", "DIFF_MATCH_PATCH_VERSION")
 load("//plugins:external_plugin_deps.bzl", "external_plugin_deps")
 
 http_archive(
+    name = "bazel_skylib",
+    sha256 = "bbccf674aa441c266df9894182d80de104cabd19be98be002f6d478aaa31574d",
+    strip_prefix = "bazel-skylib-2169ae1c374aab4a09aa90e65efe1a3aad4e279b",
+    urls = ["https://github.com/bazelbuild/bazel-skylib/archive/2169ae1c374aab4a09aa90e65efe1a3aad4e279b.tar.gz"],
+)
+
+http_archive(
     name = "io_bazel_rules_closure",
     sha256 = "25f5399f18d8bf9ce435f85c6bbf671ec4820bc4396b3022cc5dc4bc66303609",
     strip_prefix = "rules_closure-0.4.2",
@@ -24,6 +27,10 @@
     url = "https://raw.githubusercontent.com/google/closure-compiler/775609aad61e14aef289ebec4bfc09ad88877f9e/contrib/externs/polymer-1.0.js",
 )
 
+load("@bazel_skylib//:lib.bzl", "versions")
+
+versions.check(minimum_bazel_version = "0.7.0")
+
 load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories")
 
 # Prevent redundant loading of dependencies.
@@ -278,6 +285,7 @@
     sha1 = "4b95f4897fa13f2cd904aee711aeafc0c5295cd8",
 )
 
+# When upgrading commons-compress, also upgrade tukaani-xz
 maven_jar(
     name = "commons_compress",
     artifact = "org.apache.commons:commons-compress:1.13",
@@ -418,10 +426,11 @@
     sha1 = "514df6a7c7938de35c7f68dc8b8f22df86037f38",
 )
 
+# Transitive dependency of commons-compress
 maven_jar(
     name = "tukaani_xz",
-    artifact = "org.tukaani:xz:1.6",
-    sha1 = "05b6f921f1810bdf90e25471968f741f87168b64",
+    artifact = "org.tukaani:xz:1.4",
+    sha1 = "18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3",
 )
 
 # When upgrading Lucene, make sure it's compatible with Elasticsearch
@@ -1077,8 +1086,8 @@
 bower_archive(
     name = "paper-button",
     package = "polymerelements/paper-button",
-    sha1 = "41a8fec68d93dad223ad2076d68515334b2c8d7b",
-    version = "1.0.11",
+    sha1 = "3b01774f58a8085d3c903fc5a32944b26ab7be72",
+    version = "2.0.0",
 )
 
 bower_archive(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index 7307264..74fcdc2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -83,17 +83,6 @@
     return createAccountFormatter().name(info);
   }
 
-  public static AccountInfo asInfo(com.google.gerrit.common.data.AccountInfo acct) {
-    if (acct == null) {
-      return AccountInfo.create(0, null, null, null);
-    }
-    return AccountInfo.create(
-        acct.getId() != null ? acct.getId().get() : 0,
-        acct.getFullName(),
-        acct.getPreferredEmail(),
-        acct.getUsername());
-  }
-
   private static AccountFormatter createAccountFormatter() {
     return new AccountFormatter(Gerrit.info().user().anonymousCowardName());
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index e46ba72..cb3e9f0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -89,6 +89,7 @@
     suggestions.add("ownerin:");
     suggestions.add("author:");
     suggestions.add("committer:");
+    suggestions.add("assignee:");
 
     suggestions.add("reviewer:");
     suggestions.add("reviewer:self");
@@ -136,6 +137,7 @@
     suggestions.add("is:mergeable");
     suggestions.add("is:ignored");
     suggestions.add("is:wip");
+    suggestions.add("is:assigned");
 
     suggestions.add("status:");
     suggestions.add("status:open");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index b556519..c0947a8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -133,6 +133,8 @@
 
   String headingProjectSubmitType();
 
+  String projectSubmitType_INHERIT();
+
   String projectSubmitType_FAST_FORWARD_ONLY();
 
   String projectSubmitType_MERGE_ALWAYS();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 62f3778..8d6878f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -57,6 +57,7 @@
 headingAuditLog = Audit Log
 
 headingProjectSubmitType = Submit Type
+projectSubmitType_INHERIT = Inherit
 projectSubmitType_FAST_FORWARD_ONLY = Fast Forward Only
 projectSubmitType_MERGE_IF_NECESSARY = Merge if Necessary
 projectSubmitType_REBASE_ALWAYS = Rebase Always
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 4e94250..64e147d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.client.projects.ConfigInfo.ConfigParameterInfo;
 import com.google.gerrit.client.projects.ConfigInfo.ConfigParameterValue;
 import com.google.gerrit.client.projects.ConfigInfo.InheritedBooleanInfo;
+import com.google.gerrit.client.projects.ConfigInfo.SubmitTypeInfo;
 import com.google.gerrit.client.projects.ProjectApi;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -335,13 +336,15 @@
     grid.addHtml(AdminConstants.I.useSignedOffBy(), signedOffBy);
   }
 
-  private void setSubmitType(SubmitType newSubmitType) {
+  private void setSubmitType(SubmitTypeInfo newSubmitType) {
     int index = -1;
-    if (submitType != null) {
+    if (newSubmitType != null) {
       for (int i = 0; i < submitType.getItemCount(); i++) {
-        if (newSubmitType.name().equals(submitType.getValue(i))) {
+        if (submitType.getValue(i).equals(SubmitType.INHERIT.name())) {
+          submitType.setItemText(i, getInheritString(newSubmitType));
+        }
+        if (newSubmitType.configuredValue().name().equals(submitType.getValue(i))) {
           index = i;
-          break;
         }
       }
       submitType.setSelectedIndex(index);
@@ -349,6 +352,13 @@
     }
   }
 
+  private static String getInheritString(SubmitTypeInfo submitType) {
+    return Util.toLongString(SubmitType.INHERIT)
+        + " ("
+        + Util.toLongString(submitType.inheritedValue())
+        + ")";
+  }
+
   private void setState(ProjectState newState) {
     if (state != null) {
       for (int i = 0; i < state.getItemCount(); i++) {
@@ -419,7 +429,7 @@
     setBool(privateByDefault, result.privateByDefault());
     setBool(enableReviewerByEmail, result.enableReviewerByEmail());
     setBool(matchAuthorToCommitterDate, result.matchAuthorToCommitterDate());
-    setSubmitType(result.submitType());
+    setSubmitType(result.defaultSubmitType());
     setState(result.state());
     maxObjectSizeLimit.setText(result.maxObjectSizeLimit().configuredValue());
     if (result.maxObjectSizeLimit().inheritedValue() != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
index 2e4926d..bbc8a1d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
@@ -35,6 +35,8 @@
       return "";
     }
     switch (type) {
+      case INHERIT:
+        return AdminConstants.I.projectSubmitType_INHERIT();
       case FAST_FORWARD_ONLY:
         return AdminConstants.I.projectSubmitType_FAST_FORWARD_ONLY();
       case MERGE_IF_NECESSARY:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index b8effdf..f670ac7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -70,6 +70,8 @@
     return SubmitType.valueOf(submitTypeRaw());
   }
 
+  public final native SubmitTypeInfo defaultSubmitType() /*-{ return this.default_submit_type; }-*/;
+
   public final native NativeMap<NativeMap<ConfigParameterInfo>> pluginConfig()
       /*-{ return this.plugin_config || {}; }-*/ ;
 
@@ -232,4 +234,26 @@
 
     protected ConfigParameterValue() {}
   }
+
+  public static class SubmitTypeInfo extends JavaScriptObject {
+    public final SubmitType value() {
+      return SubmitType.valueOf(valueRaw());
+    }
+
+    public final SubmitType configuredValue() {
+      return SubmitType.valueOf(configuredValueRaw());
+    }
+
+    public final SubmitType inheritedValue() {
+      return SubmitType.valueOf(inheritedValueRaw());
+    }
+
+    private final native String valueRaw() /*-{ return this.value; }-*/;
+
+    private final native String configuredValueRaw() /*-{ return this.configured_value; }-*/;
+
+    private final native String inheritedValueRaw() /*-{ return this.inherited_value; }-*/;
+
+    protected SubmitTypeInfo() {}
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index 3766dd9..66afdb2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -176,7 +176,9 @@
     in.setRejectImplicitMerges(rejectImplicitMerges);
     in.setPrivateByDefault(privateByDefault);
     in.setMaxObjectSizeLimit(maxObjectSizeLimit);
-    in.setSubmitType(submitType);
+    if (submitType != null) {
+      in.setSubmitType(submitType);
+    }
     in.setState(state);
     in.setPluginConfigValues(pluginConfigValues);
     in.setEnableReviewerByEmail(enableReviewerByEmail);
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index c3349f1..88e322a 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -436,6 +436,7 @@
       in.useContentMerge = ann.useContributorAgreements();
       in.useSignedOffBy = ann.useSignedOffBy();
       in.useContentMerge = ann.useContentMerge();
+      in.rejectEmptyCommit = ann.rejectEmptyCommit();
     } else {
       // Defaults should match TestProjectConfig, omitting nullable values.
       in.createEmptyCommit = true;
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 0b14cf1..1b9e8aa 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.pgm.Daemon;
 import com.google.gerrit.pgm.Init;
-import com.google.gerrit.pgm.init.InitSshd;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
 import com.google.gerrit.server.ssh.NoSshModule;
@@ -457,7 +456,8 @@
     URI uri = URI.create(url);
 
     String addr = cfg.getString("sshd", null, "listenAddress");
-    if (!InitSshd.isOff(addr)) {
+    // We do not use InitSshd.isOff to avoid coupling GerritServer to the SSH code.
+    if (!"off".equalsIgnoreCase(addr)) {
       sshdAddress = SocketUtil.resolve(cfg.getString("sshd", null, "listenAddress"), 0);
     }
     httpAddress = new InetSocketAddress(uri.getHost(), uri.getPort());
diff --git a/java/com/google/gerrit/acceptance/TestProjectInput.java b/java/com/google/gerrit/acceptance/TestProjectInput.java
index 739d4f5..eada6434 100644
--- a/java/com/google/gerrit/acceptance/TestProjectInput.java
+++ b/java/com/google/gerrit/acceptance/TestProjectInput.java
@@ -45,6 +45,8 @@
 
   InheritableBoolean requireChangeId() default InheritableBoolean.INHERIT;
 
+  InheritableBoolean rejectEmptyCommit() default InheritableBoolean.INHERIT;
+
   // Fields specific to acceptance test behavior.
 
   /** Username to use for initial clone, passed to {@link AccountCreator}. */
diff --git a/java/com/google/gerrit/common/data/AccountInfo.java b/java/com/google/gerrit/common/data/AccountInfo.java
deleted file mode 100644
index 788a26d..0000000
--- a/java/com/google/gerrit/common/data/AccountInfo.java
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.reviewdb.client.Account;
-
-/** Summary information about an {@link Account}, for simple tabular displays. */
-public class AccountInfo {
-  protected Account.Id id;
-  protected String fullName;
-  protected String preferredEmail;
-  protected String username;
-
-  protected AccountInfo() {}
-
-  /**
-   * Create an 'Anonymous Coward' account info, when only the id is known.
-   *
-   * <p>This constructor should only be a last-ditch effort, when the usual account lookup has
-   * failed and a stale account id has been discovered in the data store.
-   */
-  public AccountInfo(Account.Id id) {
-    this.id = id;
-  }
-
-  /**
-   * Create an account description from a real data store record.
-   *
-   * @param a the data store record holding the specific account details.
-   */
-  public AccountInfo(Account a) {
-    id = a.getId();
-    fullName = a.getFullName();
-    preferredEmail = a.getPreferredEmail();
-    username = a.getUserName();
-  }
-
-  /** @return the unique local id of the account */
-  public Account.Id getId() {
-    return id;
-  }
-
-  public void setFullName(String n) {
-    fullName = n;
-  }
-
-  /** @return the full name of the account holder; null if not supplied */
-  public String getFullName() {
-    return fullName;
-  }
-
-  /** @return the email address of the account holder; null if not supplied */
-  public String getPreferredEmail() {
-    return preferredEmail;
-  }
-
-  public void setPreferredEmail(String email) {
-    preferredEmail = email;
-  }
-
-  /** @return the username of the account holder */
-  public String getUsername() {
-    return username;
-  }
-}
diff --git a/java/com/google/gerrit/common/data/AgreementInfo.java b/java/com/google/gerrit/common/data/AgreementInfo.java
deleted file mode 100644
index 4fb4053..0000000
--- a/java/com/google/gerrit/common/data/AgreementInfo.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import java.util.List;
-import java.util.Map;
-
-public class AgreementInfo {
-  public List<String> accepted;
-  public Map<String, ContributorAgreement> agreements;
-
-  public AgreementInfo() {}
-
-  public void setAccepted(List<String> a) {
-    accepted = a;
-  }
-
-  public void setAgreements(Map<String, ContributorAgreement> a) {
-    agreements = a;
-  }
-}
diff --git a/java/com/google/gerrit/common/data/SubmitTypeRecord.java b/java/com/google/gerrit/common/data/SubmitTypeRecord.java
index a01d83d..d16da96 100644
--- a/java/com/google/gerrit/common/data/SubmitTypeRecord.java
+++ b/java/com/google/gerrit/common/data/SubmitTypeRecord.java
@@ -48,6 +48,9 @@
   public final String errorMessage;
 
   private SubmitTypeRecord(Status status, SubmitType type, String errorMessage) {
+    if (type == SubmitType.INHERIT) {
+      throw new IllegalArgumentException("Cannot output submit type " + type);
+    }
     this.status = status;
     this.type = type;
     this.errorMessage = errorMessage;
diff --git a/java/com/google/gerrit/common/data/testing/BUILD b/java/com/google/gerrit/common/data/testing/BUILD
new file mode 100644
index 0000000..83f1c06
--- /dev/null
+++ b/java/com/google/gerrit/common/data/testing/BUILD
@@ -0,0 +1,11 @@
+java_library(
+    name = "common-data-test-util",
+    testonly = 1,
+    srcs = glob(["**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//lib:truth",
+    ],
+)
diff --git a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
new file mode 100644
index 0000000..1988d66
--- /dev/null
+++ b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
@@ -0,0 +1,48 @@
+// 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.common.data.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.ComparableSubject;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.StringSubject;
+import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+public class GroupReferenceSubject extends Subject<GroupReferenceSubject, GroupReference> {
+
+  public static GroupReferenceSubject assertThat(GroupReference group) {
+    return assertAbout(GroupReferenceSubject::new).that(group);
+  }
+
+  private GroupReferenceSubject(FailureMetadata metadata, GroupReference group) {
+    super(metadata, group);
+  }
+
+  public ComparableSubject<?, AccountGroup.UUID> groupUuid() {
+    isNotNull();
+    GroupReference group = actual();
+    return Truth.assertThat(group.getUUID()).named("groupUuid");
+  }
+
+  public StringSubject name() {
+    isNotNull();
+    GroupReference group = actual();
+    return Truth.assertThat(group.getName()).named("name");
+  }
+}
diff --git a/java/com/google/gerrit/extensions/api/accounts/Accounts.java b/java/com/google/gerrit/extensions/api/accounts/Accounts.java
index e92d229..651e786 100644
--- a/java/com/google/gerrit/extensions/api/accounts/Accounts.java
+++ b/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -137,6 +137,7 @@
     private String query;
     private int limit;
     private int start;
+    private boolean suggest;
     private EnumSet<ListAccountsOption> options = EnumSet.noneOf(ListAccountsOption.class);
 
     /** Execute query and return a list of accounts. */
@@ -166,6 +167,11 @@
       return this;
     }
 
+    public QueryRequest withSuggest(boolean suggest) {
+      this.suggest = suggest;
+      return this;
+    }
+
     public QueryRequest withOption(ListAccountsOption options) {
       this.options.add(options);
       return this;
@@ -193,6 +199,10 @@
       return start;
     }
 
+    public boolean getSuggest() {
+      return suggest;
+    }
+
     public EnumSet<ListAccountsOption> getOptions() {
       return options;
     }
diff --git a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index 2c945be..a13fb75 100644
--- a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -48,8 +48,8 @@
    * How to process draft comments already in the database that were not also described in this
    * input request.
    *
-   * <p>Defaults to DELETE, unless {@link #onBehalfOf} is set, in which case it defaults to KEEP and
-   * any other value is disallowed.
+   * <p>If not set, the default is {@link DraftHandling#KEEP}. If {@link #onBehalfOf} is set, then
+   * no other value besides {@code KEEP} is allowed.
    */
   public DraftHandling drafts;
 
@@ -87,15 +87,12 @@
   public boolean ready;
 
   public enum DraftHandling {
-    /** Delete pending drafts on this revision only. */
-    DELETE,
+    /** Leave pending drafts alone. */
+    KEEP,
 
     /** Publish pending drafts on this revision only. */
     PUBLISH,
 
-    /** Leave pending drafts alone. */
-    KEEP,
-
     /** Publish pending drafts on all revisions. */
     PUBLISH_ALL_REVISIONS
   }
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
index 7fa65cf..80115aa 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -35,9 +35,12 @@
   public InheritedBooleanInfo privateByDefault;
   public InheritedBooleanInfo enableReviewerByEmail;
   public InheritedBooleanInfo matchAuthorToCommitterDate;
+  public InheritedBooleanInfo rejectEmptyCommit;
 
   public MaxObjectSizeLimitInfo maxObjectSizeLimit;
+  @Deprecated // Equivalent to defaultSubmitType.value
   public SubmitType submitType;
+  public SubmitTypeInfo defaultSubmitType;
   public ProjectState state;
   public Map<String, Map<String, ConfigParameterInfo>> pluginConfig;
   public Map<String, ActionInfo> actions;
@@ -72,4 +75,10 @@
     public List<String> permittedValues;
     public List<String> values;
   }
+
+  public static class SubmitTypeInfo {
+    public SubmitType value;
+    public SubmitType configuredValue;
+    public SubmitType inheritedValue;
+  }
 }
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
index 0c1cec4..37a2e8b 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
@@ -32,6 +32,7 @@
   public InheritableBoolean privateByDefault;
   public InheritableBoolean enableReviewerByEmail;
   public InheritableBoolean matchAuthorToCommitterDate;
+  public InheritableBoolean rejectEmptyCommit;
   public String maxObjectSizeLimit;
   public SubmitType submitType;
   public ProjectState state;
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectInput.java b/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
index 612c49c..b7079ae 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
@@ -33,6 +33,7 @@
   public InheritableBoolean useContentMerge;
   public InheritableBoolean requireChangeId;
   public InheritableBoolean createNewChangeForAllNotInTarget;
+  public InheritableBoolean rejectEmptyCommit;
   public String maxObjectSizeLimit;
   public Map<String, Map<String, ConfigValue>> pluginConfigValues;
 }
diff --git a/java/com/google/gerrit/extensions/client/ListChangesOption.java b/java/com/google/gerrit/extensions/client/ListChangesOption.java
index ee7d039..ffc5029 100644
--- a/java/com/google/gerrit/extensions/client/ListChangesOption.java
+++ b/java/com/google/gerrit/extensions/client/ListChangesOption.java
@@ -75,7 +75,10 @@
   SUBMITTABLE(20),
 
   /** If tracking Ids are included, include detailed tracking Ids info. */
-  TRACKING_IDS(21);
+  TRACKING_IDS(21),
+
+  /** Skip mergeability data */
+  SKIP_MERGEABLE(22);
 
   private final int value;
 
diff --git a/java/com/google/gerrit/extensions/client/ProjectState.java b/java/com/google/gerrit/extensions/client/ProjectState.java
index e5bc194..4aee69c 100644
--- a/java/com/google/gerrit/extensions/client/ProjectState.java
+++ b/java/com/google/gerrit/extensions/client/ProjectState.java
@@ -15,8 +15,14 @@
 package com.google.gerrit.extensions.client;
 
 public enum ProjectState {
+  /** Permits reading project state and contents as well as mutating data. */
   ACTIVE(true, true),
+  /** Permits reading project state and contents. Does not permit any modifications. */
   READ_ONLY(true, false),
+  /**
+   * Hides the project as if it was deleted, but makes requests fail with an error message that
+   * reveals the project's existence.
+   */
   HIDDEN(false, false);
 
   private final boolean permitsRead;
diff --git a/java/com/google/gerrit/extensions/client/SubmitType.java b/java/com/google/gerrit/extensions/client/SubmitType.java
index b52e89a..0e2f362 100644
--- a/java/com/google/gerrit/extensions/client/SubmitType.java
+++ b/java/com/google/gerrit/extensions/client/SubmitType.java
@@ -15,10 +15,11 @@
 package com.google.gerrit.extensions.client;
 
 public enum SubmitType {
+  INHERIT,
   FAST_FORWARD_ONLY,
   MERGE_IF_NECESSARY,
   REBASE_IF_NECESSARY,
   REBASE_ALWAYS,
   MERGE_ALWAYS,
-  CHERRY_PICK
+  CHERRY_PICK;
 }
diff --git a/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java b/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
index b710121..9e02ae5 100644
--- a/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
@@ -18,6 +18,7 @@
   public Boolean allowBlame;
   public Boolean showAssigneeInChangesTable;
   public Boolean allowDrafts;
+  public Boolean disablePrivateChanges;
   public int largeChange;
   public String replyLabel;
   public String replyTooltip;
diff --git a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
index b9d89ee..6d132c8 100644
--- a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
+++ b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -24,8 +24,9 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -41,16 +42,19 @@
 
   private final Provider<PersonIdent> serverIdent;
   private final Provider<PublicKeyStore> storeProvider;
-  private final ExternalIdsUpdate.User externalIdsUpdateFactory;
+  private final AccountsUpdate.User accountsUpdateFactory;
+  private final ExternalIds externalIds;
 
   @Inject
   DeleteGpgKey(
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
       Provider<PublicKeyStore> storeProvider,
-      ExternalIdsUpdate.User externalIdsUpdateFactory) {
+      AccountsUpdate.User accountsUpdateFactory,
+      ExternalIds externalIds) {
     this.serverIdent = serverIdent;
     this.storeProvider = storeProvider;
-    this.externalIdsUpdateFactory = externalIdsUpdateFactory;
+    this.accountsUpdateFactory = accountsUpdateFactory;
+    this.externalIds = externalIds;
   }
 
   @Override
@@ -58,12 +62,16 @@
       throws ResourceConflictException, PGPException, OrmException, IOException,
           ConfigInvalidException {
     PGPPublicKey key = rsrc.getKeyRing().getPublicKey();
-    externalIdsUpdateFactory
-        .create()
-        .delete(
-            rsrc.getUser().getAccountId(),
+    ExternalId extId =
+        externalIds.get(
             ExternalId.Key.create(
                 SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint())));
+    accountsUpdateFactory
+        .create()
+        .update(
+            "Delete GPG Key via API",
+            rsrc.getUser().getAccountId(),
+            u -> u.deleteExternalId(extId));
 
     try (PublicKeyStore store = storeProvider.get()) {
       store.remove(rsrc.getKeyRing().getPublicKey().getFingerprint());
diff --git a/java/com/google/gerrit/gpg/server/GpgKeys.java b/java/com/google/gerrit/gpg/server/GpgKeys.java
index 63c0476..c4e35e0 100644
--- a/java/com/google/gerrit/gpg/server/GpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -92,7 +92,8 @@
       throws ResourceNotFoundException, PGPException, OrmException, IOException {
     checkVisible(self, parent);
 
-    byte[] fp = parseFingerprint(id.get(), getGpgExtIds(parent));
+    ExternalId gpgKeyExtId = findGpgKey(id.get(), getGpgExtIds(parent));
+    byte[] fp = parseFingerprint(gpgKeyExtId);
     try (PublicKeyStore store = storeProvider.get()) {
       long keyId = keyId(fp);
       for (PGPPublicKeyRing keyRing : store.get(keyId)) {
@@ -106,30 +107,34 @@
     throw new ResourceNotFoundException(id);
   }
 
-  static byte[] parseFingerprint(String str, Iterable<ExternalId> existingExtIds)
+  static ExternalId findGpgKey(String str, Iterable<ExternalId> existingExtIds)
       throws ResourceNotFoundException {
     str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
     if ((str.length() != 8 && str.length() != 40)
         || !CharMatcher.anyOf("0123456789ABCDEF").matchesAllOf(str)) {
       throw new ResourceNotFoundException(str);
     }
-    byte[] fp = null;
+    ExternalId gpgKeyExtId = null;
     for (ExternalId extId : existingExtIds) {
       String fpStr = extId.key().id();
       if (!fpStr.endsWith(str)) {
         continue;
-      } else if (fp != null) {
+      } else if (gpgKeyExtId != null) {
         throw new ResourceNotFoundException("Multiple keys found for " + str);
       }
-      fp = BaseEncoding.base16().decode(fpStr);
+      gpgKeyExtId = extId;
       if (str.length() == 40) {
         break;
       }
     }
-    if (fp == null) {
+    if (gpgKeyExtId == null) {
       throw new ResourceNotFoundException(str);
     }
-    return fp;
+    return gpgKeyExtId;
+  }
+
+  static byte[] parseFingerprint(ExternalId gpgKeyExtId) {
+    return BaseEncoding.base16().decode(gpgKeyExtId.key().id());
   }
 
   @Override
@@ -145,8 +150,7 @@
       Map<String, GpgKeyInfo> keys = new HashMap<>();
       try (PublicKeyStore store = storeProvider.get()) {
         for (ExternalId extId : getGpgExtIds(rsrc)) {
-          String fpStr = extId.key().id();
-          byte[] fp = BaseEncoding.base16().decode(fpStr);
+          byte[] fp = parseFingerprint(extId);
           boolean found = false;
           for (PGPPublicKeyRing keyRing : store.get(keyId(fp))) {
             if (Arrays.equals(keyRing.getPublicKey().getFingerprint(), fp)) {
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index d8ed855..4996e0e 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -18,14 +18,12 @@
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.accounts.GpgKeysInput;
@@ -45,9 +43,9 @@
 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.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
-import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
 import com.google.gerrit.server.mail.send.AddKeySender;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gwtorm.server.OrmException;
@@ -61,7 +59,6 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import org.bouncycastle.bcpg.ArmoredInputStream;
 import org.bouncycastle.openpgp.PGPException;
 import org.bouncycastle.openpgp.PGPPublicKey;
@@ -85,7 +82,7 @@
   private final AddKeySender.Factory addKeyFactory;
   private final Provider<InternalAccountQuery> accountQueryProvider;
   private final ExternalIds externalIds;
-  private final ExternalIdsUpdate.User externalIdsUpdateFactory;
+  private final AccountsUpdate.User accountsUpdateFactory;
 
   @Inject
   PostGpgKeys(
@@ -96,7 +93,7 @@
       AddKeySender.Factory addKeyFactory,
       Provider<InternalAccountQuery> accountQueryProvider,
       ExternalIds externalIds,
-      ExternalIdsUpdate.User externalIdsUpdateFactory) {
+      AccountsUpdate.User accountsUpdateFactory) {
     this.serverIdent = serverIdent;
     this.self = self;
     this.storeProvider = storeProvider;
@@ -104,7 +101,7 @@
     this.addKeyFactory = addKeyFactory;
     this.accountQueryProvider = accountQueryProvider;
     this.externalIds = externalIds;
-    this.externalIdsUpdateFactory = externalIdsUpdateFactory;
+    this.accountsUpdateFactory = accountsUpdateFactory;
   }
 
   @Override
@@ -116,8 +113,9 @@
     Collection<ExternalId> existingExtIds =
         externalIds.byAccount(rsrc.getUser().getAccountId(), SCHEME_GPGKEY);
     try (PublicKeyStore store = storeProvider.get()) {
-      Set<Fingerprint> toRemove = readKeysToRemove(input, existingExtIds);
-      List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, toRemove);
+      Map<ExternalId, Fingerprint> toRemove = readKeysToRemove(input, existingExtIds);
+      Collection<Fingerprint> fingerprintsToRemove = toRemove.values();
+      List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, fingerprintsToRemove);
       List<ExternalId> newExtIds = new ArrayList<>(existingExtIds.size());
 
       for (PGPPublicKeyRing keyRing : newKeys) {
@@ -133,26 +131,29 @@
         }
       }
 
-      storeKeys(rsrc, newKeys, toRemove);
+      storeKeys(rsrc, newKeys, fingerprintsToRemove);
 
-      List<ExternalId.Key> extIdKeysToRemove =
-          toRemove.stream().map(fp -> toExtIdKey(fp.get())).collect(toList());
-      externalIdsUpdateFactory
+      accountsUpdateFactory
           .create()
-          .replace(rsrc.getUser().getAccountId(), extIdKeysToRemove, newExtIds);
-      return toJson(newKeys, toRemove, store, rsrc.getUser());
+          .update(
+              "Update GPG Keys via API",
+              rsrc.getUser().getAccountId(),
+              u -> u.replaceExternalIds(toRemove.keySet(), newExtIds));
+      return toJson(newKeys, fingerprintsToRemove, store, rsrc.getUser());
     }
   }
 
-  private Set<Fingerprint> readKeysToRemove(
+  private Map<ExternalId, Fingerprint> readKeysToRemove(
       GpgKeysInput input, Collection<ExternalId> existingExtIds) {
     if (input.delete == null || input.delete.isEmpty()) {
-      return ImmutableSet.of();
+      return ImmutableMap.of();
     }
-    Set<Fingerprint> fingerprints = Sets.newHashSetWithExpectedSize(input.delete.size());
+    Map<ExternalId, Fingerprint> fingerprints =
+        Maps.newHashMapWithExpectedSize(input.delete.size());
     for (String id : input.delete) {
       try {
-        fingerprints.add(new Fingerprint(GpgKeys.parseFingerprint(id, existingExtIds)));
+        ExternalId gpgKeyExtId = GpgKeys.findGpgKey(id, existingExtIds);
+        fingerprints.put(gpgKeyExtId, new Fingerprint(GpgKeys.parseFingerprint(gpgKeyExtId)));
       } catch (ResourceNotFoundException e) {
         // Skip removal.
       }
@@ -160,7 +161,7 @@
     return fingerprints;
   }
 
-  private List<PGPPublicKeyRing> readKeysToAdd(GpgKeysInput input, Set<Fingerprint> toRemove)
+  private List<PGPPublicKeyRing> readKeysToAdd(GpgKeysInput input, Collection<Fingerprint> toRemove)
       throws BadRequestException, IOException {
     if (input.add == null || input.add.isEmpty()) {
       return ImmutableList.of();
@@ -188,7 +189,7 @@
   }
 
   private void storeKeys(
-      AccountResource rsrc, List<PGPPublicKeyRing> keyRings, Set<Fingerprint> toRemove)
+      AccountResource rsrc, List<PGPPublicKeyRing> keyRings, Collection<Fingerprint> toRemove)
       throws BadRequestException, ResourceConflictException, PGPException, IOException {
     try (PublicKeyStore store = storeProvider.get()) {
       List<String> addedKeys = new ArrayList<>();
@@ -269,7 +270,7 @@
 
   private Map<String, GpgKeyInfo> toJson(
       Collection<PGPPublicKeyRing> keys,
-      Set<Fingerprint> deleted,
+      Collection<Fingerprint> deleted,
       PublicKeyStore store,
       IdentifiedUser user)
       throws IOException {
diff --git a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 1f095e0..853d173 100644
--- a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -183,9 +183,9 @@
     return HtmlDomUtil.toUTF8(doc);
   }
 
-  private AuthResult auth(Account account) {
+  private AuthResult auth(AccountState account) {
     if (account != null) {
-      return new AuthResult(account.getId(), null, false);
+      return new AuthResult(account.getAccount().getId(), null, false);
     }
     return null;
   }
@@ -218,13 +218,8 @@
 
   private AuthResult byPreferredEmail(String email) {
     try (ReviewDb db = schema.open()) {
-      Optional<Account> match =
-          queryProvider
-              .get()
-              .byPreferredEmail(email)
-              .stream()
-              .map(AccountState::getAccount)
-              .findFirst();
+      Optional<AccountState> match =
+          queryProvider.get().byPreferredEmail(email).stream().findFirst();
       return match.isPresent() ? auth(match.get()) : null;
     } catch (OrmException e) {
       getServletContext().log("cannot query database", e);
diff --git a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index 4c8918d..88154b0 100644
--- a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -224,7 +224,9 @@
     detail.setOwnerOf(ownerOf);
     detail.setCanUpload(
         canWriteProjectConfig
-            || (checkReadConfig && perm.ref(RefNames.REFS_CONFIG).testOrFalse(CREATE_CHANGE)));
+            || (checkReadConfig
+                && perm.ref(RefNames.REFS_CONFIG).testOrFalse(CREATE_CHANGE)
+                && projectState.statePermitsWrite()));
     detail.setConfigVisible(canWriteProjectConfig || checkReadConfig);
     detail.setGroupInfo(buildGroupInfo(local));
     detail.setLabelTypes(projectState.getLabelTypes());
diff --git a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
index 0240e2e..e44d680 100644
--- a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
+++ b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -110,7 +110,7 @@
   public final T call()
       throws NoSuchProjectException, IOException, ConfigInvalidException, InvalidNameException,
           NoSuchGroupException, OrmException, UpdateParentFailedException,
-          PermissionDeniedException, PermissionBackendException {
+          PermissionDeniedException, PermissionBackendException, ResourceConflictException {
     try {
       contributorAgreements.check(projectName, user);
     } catch (AuthException e) {
@@ -195,7 +195,7 @@
   protected abstract T updateProjectConfig(
       ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
       throws IOException, NoSuchProjectException, ConfigInvalidException, OrmException,
-          PermissionDeniedException, PermissionBackendException;
+          PermissionDeniedException, PermissionBackendException, ResourceConflictException;
 
   private void replace(ProjectConfig config, Set<String> toDelete, AccessSection section)
       throws NoSuchGroupException {
diff --git a/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index 81957d6..497354d 100644
--- a/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.errors.PermissionDeniedException;
 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;
@@ -134,7 +135,7 @@
   protected Change.Id updateProjectConfig(
       ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
       throws IOException, OrmException, PermissionDeniedException, PermissionBackendException,
-          ConfigInvalidException {
+          ConfigInvalidException, ResourceConflictException {
     PermissionBackend.ForProject perm = permissionBackend.user(user).project(config.getName());
     if (!check(perm, ProjectPermission.READ_CONFIG)) {
       throw new PermissionDeniedException(RefNames.REFS_CONFIG + " not visible");
@@ -145,6 +146,8 @@
       throw new PermissionDeniedException("cannot create change for " + RefNames.REFS_CONFIG);
     }
 
+    projectCache.checkedGet(config.getName()).checkStatePermitsWrite();
+
     md.setInsertChangeId(true);
     Change.Id changeId = new Change.Id(seq.nextChangeId());
     RevCommit commit =
diff --git a/java/com/google/gerrit/pgm/init/AccountsOnInit.java b/java/com/google/gerrit/pgm/init/AccountsOnInit.java
index fbe9b62..0a94b42 100644
--- a/java/com/google/gerrit/pgm/init/AccountsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/AccountsOnInit.java
@@ -70,7 +70,7 @@
                 new GerritPersonIdentProvider(flags.cfg).get(), account.getRegisteredOn());
 
         Config accountConfig = new Config();
-        AccountConfig.writeToConfig(
+        AccountConfig.writeToAccountConfig(
             InternalAccountUpdate.builder()
                 .setActive(account.isActive())
                 .setFullName(account.getFullName())
diff --git a/java/com/google/gerrit/pgm/init/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index 3385244..7fe227d 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -150,7 +150,7 @@
       File allUsersRepoPath = getPathToAllUsersRepository();
       if (allUsersRepoPath != null) {
         try (Repository allUsersRepo = new FileRepository(allUsersRepoPath)) {
-          return GroupNameNotes.loadAllGroupReferences(allUsersRepo).stream();
+          return GroupNameNotes.loadAllGroups(allUsersRepo).stream();
         }
       }
       return Stream.empty();
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 3251c01..e9f5cd5 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -21,6 +21,7 @@
 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;
@@ -151,7 +152,12 @@
           }
 
           AccountState as =
-              new AccountState(new AllUsersName(allUsers.get()), a, extIds, new HashMap<>());
+              new AccountState(
+                  new AllUsersName(allUsers.get()),
+                  a,
+                  extIds,
+                  new HashMap<>(),
+                  GeneralPreferencesInfo.defaults());
           for (AccountIndex accountIndex : accountIndexCollection.getWriteIndexes()) {
             accountIndex.replace(as);
           }
diff --git a/java/com/google/gerrit/pgm/init/InitSshd.java b/java/com/google/gerrit/pgm/init/InitSshd.java
index 043f3ee..d2e280d 100644
--- a/java/com/google/gerrit/pgm/init/InitSshd.java
+++ b/java/com/google/gerrit/pgm/init/InitSshd.java
@@ -73,7 +73,7 @@
     remover.remove("bc(pg|pkix|prov)-.*[.]jar");
   }
 
-  public static boolean isOff(String listenHostname) {
+  static boolean isOff(String listenHostname) {
     return "off".equalsIgnoreCase(listenHostname)
         || "none".equalsIgnoreCase(listenHostname)
         || "no".equalsIgnoreCase(listenHostname);
@@ -82,7 +82,6 @@
   private void generateSshHostKeys() throws InterruptedException, IOException {
     if (!exists(site.ssh_key)
         && (!exists(site.ssh_rsa)
-            || !exists(site.ssh_dsa)
             || !exists(site.ssh_ed25519)
             || !exists(site.ssh_ecdsa_256)
             || !exists(site.ssh_ecdsa_384)
@@ -116,26 +115,6 @@
             .waitFor();
       }
 
-      if (!exists(site.ssh_dsa)) {
-        System.err.print(" dsa...");
-        System.err.flush();
-        new ProcessBuilder(
-                "ssh-keygen",
-                "-q" /* quiet */,
-                "-t",
-                "dsa",
-                "-P",
-                emptyPassphraseArg,
-                "-C",
-                comment,
-                "-f",
-                site.ssh_dsa.toAbsolutePath().toString())
-            .redirectError(Redirect.INHERIT)
-            .redirectOutput(Redirect.INHERIT)
-            .start()
-            .waitFor();
-      }
-
       if (!exists(site.ssh_ed25519)) {
         System.err.print(" ed25519...");
         System.err.flush();
diff --git a/java/com/google/gerrit/reviewdb/client/Account.java b/java/com/google/gerrit/reviewdb/client/Account.java
index bce07aa..1f9ae0e 100644
--- a/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/java/com/google/gerrit/reviewdb/client/Account.java
@@ -19,7 +19,6 @@
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS;
 
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
 import java.sql.Timestamp;
@@ -180,9 +179,6 @@
   /** <i>computed</i> the username selected from the identities. */
   protected String userName;
 
-  /** <i>stored in git, used for caching</i> the user's preferences. */
-  private GeneralPreferencesInfo generalPreferences;
-
   /**
    * ID of the user branch from which the account was read, {@code null} if the account was read
    * from ReviewDb.
@@ -286,14 +282,6 @@
     return registeredOn;
   }
 
-  public GeneralPreferencesInfo getGeneralPreferencesInfo() {
-    return generalPreferences;
-  }
-
-  public void setGeneralPreferences(GeneralPreferencesInfo p) {
-    generalPreferences = p;
-  }
-
   public String getMetaId() {
     return metaId;
   }
diff --git a/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java b/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
index ef8156b..765e38c 100644
--- a/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
+++ b/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
@@ -39,7 +39,8 @@
   REJECT_IMPLICIT_MERGES("receive", "rejectImplicitMerges"),
   PRIVATE_BY_DEFAULT("change", "privateByDefault"),
   ENABLE_REVIEWER_BY_EMAIL("reviewer", "enableByEmail"),
-  MATCH_AUTHOR_TO_COMMITTER_DATE("project", "matchAuthorToCommitterDate");
+  MATCH_AUTHOR_TO_COMMITTER_DATE("submit", "matchAuthorToCommitterDate"),
+  REJECT_EMPTY_COMMIT("submit", "rejectEmptyCommit");
 
   // Git config
   private final String section;
diff --git a/java/com/google/gerrit/reviewdb/client/Project.java b/java/com/google/gerrit/reviewdb/client/Project.java
index c66b646..921667e 100644
--- a/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/java/com/google/gerrit/reviewdb/client/Project.java
@@ -25,6 +25,12 @@
 
 /** Projects match a source code repository managed by Gerrit */
 public final class Project {
+  /** Default submit type for new projects. */
+  public static final SubmitType DEFAULT_SUBMIT_TYPE = SubmitType.MERGE_IF_NECESSARY;
+
+  /** Default submit type for root project (All-Projects). */
+  public static final SubmitType DEFAULT_ALL_PROJECTS_SUBMIT_TYPE = SubmitType.MERGE_IF_NECESSARY;
+
   /** Project name key */
   public static class NameKey extends StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
@@ -137,7 +143,14 @@
     maxObjectSizeLimit = limit;
   }
 
-  public SubmitType getSubmitType() {
+  /**
+   * Submit type as configured in {@code project.config}.
+   *
+   * <p>Does not take inheritance into account, i.e. may return {@link SubmitType#INHERIT}.
+   *
+   * @return submit type.
+   */
+  public SubmitType getConfiguredSubmitType() {
     return submitType;
   }
 
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 9894751..963da62 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -130,50 +130,25 @@
   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<>());
+    return new AccountState(
+        allUsersName,
+        account,
+        Collections.emptySet(),
+        new HashMap<>(),
+        GeneralPreferencesInfo.defaults());
   }
 
   static class ByIdLoader extends CacheLoader<Account.Id, Optional<AccountState>> {
-    private final AllUsersName allUsersName;
     private final Accounts accounts;
-    private final GeneralPreferencesLoader loader;
-    private final Provider<WatchConfig.Accessor> watchConfig;
-    private final ExternalIds externalIds;
 
     @Inject
-    ByIdLoader(
-        AllUsersName allUsersName,
-        Accounts accounts,
-        GeneralPreferencesLoader loader,
-        Provider<WatchConfig.Accessor> watchConfig,
-        ExternalIds externalIds) {
-      this.allUsersName = allUsersName;
+    ByIdLoader(Accounts accounts) {
       this.accounts = accounts;
-      this.loader = loader;
-      this.watchConfig = watchConfig;
-      this.externalIds = externalIds;
     }
 
     @Override
     public Optional<AccountState> load(Account.Id who) throws Exception {
-      Account account = accounts.get(who);
-      if (account == null) {
-        return Optional.empty();
-      }
-
-      try {
-        account.setGeneralPreferences(loader.load(who));
-      } catch (IOException | ConfigInvalidException e) {
-        log.warn("Cannot load GeneralPreferences for " + who + " (using default)", e);
-        account.setGeneralPreferences(GeneralPreferencesInfo.defaults());
-      }
-
-      return Optional.of(
-          new AccountState(
-              allUsersName,
-              account,
-              externalIds.byAccount(who),
-              watchConfig.get().getProjectWatches(who)));
+      return Optional.ofNullable(accounts.get(who));
     }
   }
 }
diff --git a/java/com/google/gerrit/server/account/AccountConfig.java b/java/com/google/gerrit/server/account/AccountConfig.java
index 1283270..b20aa0b 100644
--- a/java/com/google/gerrit/server/account/AccountConfig.java
+++ b/java/com/google/gerrit/server/account/AccountConfig.java
@@ -19,24 +19,31 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.account.WatchConfig.NotifyType;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
+import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.VersionedMetaData;
-import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevSort;
 
@@ -75,26 +82,50 @@
   public static final String KEY_PREFERRED_EMAIL = "preferredEmail";
   public static final String KEY_STATUS = "status";
 
-  @Nullable private final OutgoingEmailValidator emailValidator;
   private final Account.Id accountId;
+  private final Repository repo;
   private final String ref;
 
   private Optional<Account> loadedAccount;
+  private Optional<ObjectId> externalIdsRev;
+  private WatchConfig watchConfig;
+  private PreferencesConfig prefConfig;
   private Optional<InternalAccountUpdate> accountUpdate = Optional.empty();
   private Timestamp registeredOn;
+  private boolean eagerParsing;
   private List<ValidationError> validationErrors;
 
-  public AccountConfig(@Nullable OutgoingEmailValidator emailValidator, Account.Id accountId) {
-    this.emailValidator = emailValidator;
-    this.accountId = checkNotNull(accountId);
+  public AccountConfig(Account.Id accountId, Repository allUsersRepo) {
+    this.accountId = checkNotNull(accountId, "accountId");
+    this.repo = checkNotNull(allUsersRepo, "allUsersRepo");
     this.ref = RefNames.refsUsers(accountId);
   }
 
+  /**
+   * Sets whether all account data should be eagerly parsed.
+   *
+   * <p>Eager parsing should only be used if the caller is interested in validation errors for all
+   * account data (see {@link #getValidationErrors()}.
+   *
+   * @param eagerParsing whether all account data should be eagerly parsed
+   * @return this AccountConfig instance for chaining
+   */
+  public AccountConfig setEagerParsing(boolean eagerParsing) {
+    checkState(loadedAccount == null, "Account %s already loaded", accountId.get());
+    this.eagerParsing = eagerParsing;
+    return this;
+  }
+
   @Override
   protected String getRefName() {
     return ref;
   }
 
+  public AccountConfig load() throws IOException, ConfigInvalidException {
+    load(repo);
+    return this;
+  }
+
   /**
    * Get the loaded account.
    *
@@ -108,6 +139,40 @@
   }
 
   /**
+   * Returns the revision of the {@code refs/meta/external-ids} branch.
+   *
+   * <p>This revision can be used to load the external IDs of the loaded account lazily via {@link
+   * ExternalIds#byAccount(com.google.gerrit.reviewdb.client.Account.Id, ObjectId)}.
+   *
+   * @return revision of the {@code refs/meta/external-ids} branch, {@link Optional#empty()} if no
+   *     {@code refs/meta/external-ids} branch exists
+   */
+  public Optional<ObjectId> getExternalIdsRev() {
+    checkLoaded();
+    return externalIdsRev;
+  }
+
+  /**
+   * Get the project watches of the loaded account.
+   *
+   * @return the project watches of the loaded account
+   */
+  public Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches() {
+    checkLoaded();
+    return watchConfig.getProjectWatches();
+  }
+
+  /**
+   * Get the general preferences of the loaded account.
+   *
+   * @return the general preferences of the loaded account
+   */
+  public GeneralPreferencesInfo getGeneralPreferences() {
+    checkLoaded();
+    return prefConfig.getGeneralPreferences();
+  }
+
+  /**
    * 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.
@@ -115,7 +180,7 @@
    * @param account account that should be set
    * @throws IllegalStateException if the account was not loaded yet
    */
-  public void setAccount(Account account) {
+  public AccountConfig setAccount(Account account) {
     checkLoaded();
     this.loadedAccount = Optional.of(account);
     this.accountUpdate =
@@ -127,6 +192,7 @@
                 .setStatus(account.getStatus())
                 .build());
     this.registeredOn = account.getRegisteredOn();
+    return this;
   }
 
   /**
@@ -155,8 +221,9 @@
     return loadedAccount.get();
   }
 
-  public void setAccountUpdate(InternalAccountUpdate accountUpdate) {
+  public AccountConfig setAccountUpdate(InternalAccountUpdate accountUpdate) {
     this.accountUpdate = Optional.of(accountUpdate);
+    return this;
   }
 
   @Override
@@ -167,9 +234,26 @@
       rw.sort(RevSort.REVERSE);
       registeredOn = new Timestamp(rw.next().getCommitTime() * 1000L);
 
-      Config cfg = readConfig(ACCOUNT_CONFIG);
+      Config accountConfig = readConfig(ACCOUNT_CONFIG);
+      loadedAccount = Optional.of(parse(accountConfig, revision.name()));
 
-      loadedAccount = Optional.of(parse(cfg, 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 =
+          new PreferencesConfig(
+              accountId,
+              readConfig(PreferencesConfig.PREFERENCES_CONFIG),
+              PreferencesConfig.readDefaultConfig(repo),
+              this);
+
+      if (eagerParsing) {
+        watchConfig.parse();
+        prefConfig.parse();
+      }
     } else {
       loadedAccount = Optional.empty();
     }
@@ -182,11 +266,6 @@
 
     String preferredEmail = get(cfg, KEY_PREFERRED_EMAIL);
     account.setPreferredEmail(preferredEmail);
-    if (emailValidator != null && !emailValidator.isValid(preferredEmail)) {
-      error(
-          new ValidationError(
-              ACCOUNT_CONFIG, String.format("Invalid preferred email: %s", preferredEmail)));
-    }
 
     account.setStatus(get(cfg, KEY_STATUS));
     account.setMetaId(metaId);
@@ -221,21 +300,28 @@
       commit.setCommitter(new PersonIdent(commit.getCommitter(), registeredOn));
     }
 
-    Config cfg = readConfig(ACCOUNT_CONFIG);
-    if (accountUpdate.isPresent()) {
-      writeToConfig(accountUpdate.get(), cfg);
-    }
-    saveConfig(ACCOUNT_CONFIG, cfg);
+    Config accountConfig = saveAccount();
+    saveProjectWatches();
+    saveGeneralPreferences();
 
     // metaId is set in the commit(MetaDataUpdate) method after the commit is created
-    loadedAccount = Optional.of(parse(cfg, null));
+    loadedAccount = Optional.of(parse(accountConfig, null));
 
     accountUpdate = Optional.empty();
 
     return true;
   }
 
-  public static void writeToConfig(InternalAccountUpdate accountUpdate, Config cfg) {
+  private Config saveAccount() throws IOException, ConfigInvalidException {
+    Config accountConfig = readConfig(ACCOUNT_CONFIG);
+    if (accountUpdate.isPresent()) {
+      writeToAccountConfig(accountUpdate.get(), accountConfig);
+    }
+    saveConfig(ACCOUNT_CONFIG, accountConfig);
+    return accountConfig;
+  }
+
+  public static void writeToAccountConfig(InternalAccountUpdate accountUpdate, Config cfg) {
     accountUpdate.getActive().ifPresent(active -> setActive(cfg, active));
     accountUpdate.getFullName().ifPresent(fullName -> set(cfg, KEY_FULL_NAME, fullName));
     accountUpdate
@@ -244,6 +330,28 @@
     accountUpdate.getStatus().ifPresent(status -> set(cfg, KEY_STATUS, status));
   }
 
+  private void saveProjectWatches() throws IOException {
+    if (accountUpdate.isPresent()
+        && (!accountUpdate.get().getDeletedProjectWatches().isEmpty()
+            || !accountUpdate.get().getUpdatedProjectWatches().isEmpty())) {
+      Map<ProjectWatchKey, Set<NotifyType>> projectWatches = watchConfig.getProjectWatches();
+      accountUpdate.get().getDeletedProjectWatches().forEach(pw -> projectWatches.remove(pw));
+      accountUpdate
+          .get()
+          .getUpdatedProjectWatches()
+          .forEach((pw, nt) -> projectWatches.put(pw, nt));
+      saveConfig(WatchConfig.WATCH_CONFIG, watchConfig.save(projectWatches));
+    }
+  }
+
+  private void saveGeneralPreferences() throws IOException, ConfigInvalidException {
+    if (accountUpdate.isPresent() && accountUpdate.get().getGeneralPreferences().isPresent()) {
+      saveConfig(
+          PreferencesConfig.PREFERENCES_CONFIG,
+          prefConfig.saveGeneralPreferences(accountUpdate.get().getGeneralPreferences().get()));
+    }
+  }
+
   /**
    * Sets/Unsets {@code account.active} in the given config.
    *
@@ -298,7 +406,10 @@
   }
 
   /**
-   * Get the validation errors, if any were discovered during load.
+   * Get the validation errors, if any were discovered during parsing the account data.
+   *
+   * <p>To get validation errors for all account data request eager parsing before loading the
+   * account (see {@link #setEagerParsing(boolean)}).
    *
    * @return list of errors; empty list if there are no errors.
    */
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index 3f87e5f..aac515d 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -21,7 +24,6 @@
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.Account;
@@ -33,12 +35,12 @@
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
-import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
 import com.google.gerrit.server.auth.NoSuchUserException;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -67,11 +69,10 @@
   private final AccountCache byIdCache;
   private final Realm realm;
   private final IdentifiedUser.GenericFactory userFactory;
-  private final ChangeUserName.Factory changeUserNameFactory;
+  private final SshKeyCache sshKeyCache;
   private final ProjectCache projectCache;
   private final AtomicBoolean awaitsFirstAccountCheck;
   private final ExternalIds externalIds;
-  private final ExternalIdsUpdate.Server externalIdsUpdateFactory;
   private final GroupsUpdate.Factory groupsUpdateFactory;
   private final boolean autoUpdateAccountActiveStatus;
   private final SetInactiveFlag setInactiveFlag;
@@ -86,10 +87,9 @@
       AccountCache byIdCache,
       Realm accountMapper,
       IdentifiedUser.GenericFactory userFactory,
-      ChangeUserName.Factory changeUserNameFactory,
+      SshKeyCache sshKeyCache,
       ProjectCache projectCache,
       ExternalIds externalIds,
-      ExternalIdsUpdate.Server externalIdsUpdateFactory,
       GroupsUpdate.Factory groupsUpdateFactory,
       SetInactiveFlag setInactiveFlag) {
     this.schema = schema;
@@ -99,12 +99,11 @@
     this.byIdCache = byIdCache;
     this.realm = accountMapper;
     this.userFactory = userFactory;
-    this.changeUserNameFactory = changeUserNameFactory;
+    this.sshKeyCache = sshKeyCache;
     this.projectCache = projectCache;
     this.awaitsFirstAccountCheck =
         new AtomicBoolean(cfg.getBoolean("capability", "makeFirstUserAdmin", true));
     this.externalIds = externalIds;
-    this.externalIdsUpdateFactory = externalIdsUpdateFactory;
     this.groupsUpdateFactory = groupsUpdateFactory;
     this.autoUpdateAccountActiveStatus =
         cfg.getBoolean("auth", "autoUpdateAccountActiveStatus", false);
@@ -262,6 +261,8 @@
 
     ExternalId extId =
         ExternalId.createWithEmail(who.getExternalIdKey(), newId, who.getEmailAddress());
+    ExternalId userNameExtId =
+        !Strings.isNullOrEmpty(who.getUserName()) ? createUsername(newId, who.getUserName()) : null;
 
     boolean isFirstAccount = awaitsFirstAccountCheck.getAndSet(false) && !accounts.hasAnyAccount();
 
@@ -273,10 +274,14 @@
               .insert(
                   "Create Account on First Login",
                   newId,
-                  u ->
-                      u.setFullName(who.getDisplayName())
-                          .setPreferredEmail(extId.email())
-                          .addExternalId(extId));
+                  u -> {
+                    u.setFullName(who.getDisplayName())
+                        .setPreferredEmail(extId.email())
+                        .addExternalId(extId);
+                    if (userNameExtId != null) {
+                      u.addExternalId(userNameExtId);
+                    }
+                  });
     } catch (DuplicateExternalIdKeyException e) {
       throw new AccountException(
           "Cannot assign external ID \""
@@ -291,6 +296,10 @@
       awaitsFirstAccountCheck.set(isFirstAccount);
     }
 
+    if (userNameExtId != null) {
+      sshKeyCache.evict(who.getUserName());
+    }
+
     IdentifiedUser user = userFactory.create(newId);
 
     if (isFirstAccount) {
@@ -309,37 +318,23 @@
       addGroupMember(db, adminGroupUuid, user);
     }
 
-    if (who.getUserName() != null) {
-      // Only set if the name hasn't been used yet, but was given to us.
-      //
-      try {
-        changeUserNameFactory.create("Set Username on Login", user, who.getUserName()).call();
-      } catch (NameAlreadyUsedException e) {
-        String message =
-            "Cannot assign user name \""
-                + who.getUserName()
-                + "\" to account "
-                + newId
-                + "; name already in use.";
-        handleSettingUserNameFailure(account, extId, message, e, false);
-      } catch (InvalidUserNameException e) {
-        String message =
-            "Cannot assign user name \""
-                + who.getUserName()
-                + "\" to account "
-                + newId
-                + "; name does not conform.";
-        handleSettingUserNameFailure(account, extId, message, e, false);
-      } catch (OrmException e) {
-        String message = "Cannot assign user name";
-        handleSettingUserNameFailure(account, extId, message, e, true);
-      }
-    }
-
     realm.onCreateAccount(who, account);
     return new AuthResult(newId, extId.key(), true);
   }
 
+  private ExternalId createUsername(Account.Id accountId, String username)
+      throws AccountUserNameException {
+    checkArgument(!Strings.isNullOrEmpty(username));
+
+    if (!ExternalId.isValidUsername(username)) {
+      throw new AccountUserNameException(
+          String.format(
+              "Cannot assign user name \"%s\" to account %s; name does not conform.",
+              username, accountId));
+    }
+    return ExternalId.create(SCHEME_USERNAME, username, accountId);
+  }
+
   private void addGroupMember(ReviewDb db, AccountGroup.UUID groupUuid, IdentifiedUser user)
       throws OrmException, IOException, ConfigInvalidException, AccountException {
     // The user initiated this request by logging in. -> Attribute all modifications to that user.
@@ -357,43 +352,6 @@
   }
 
   /**
-   * This method handles an exception that occurred during the setting of the user name for a newly
-   * created account. If the realm does not allow the user to set a user name manually this method
-   * deletes the newly created account and throws an {@link AccountUserNameException}. In any case
-   * the error message is logged.
-   *
-   * @param account the newly created account
-   * @param extId the newly created external id
-   * @param errorMessage the error message
-   * @param e the exception that occurred during the setting of the user name for the new account
-   * @param logException flag that decides whether the exception should be included into the log
-   * @throws AccountUserNameException thrown if the realm does not allow the user to manually set
-   *     the user name
-   * @throws OrmException thrown if cleaning the database failed
-   */
-  private void handleSettingUserNameFailure(
-      Account account, ExternalId extId, String errorMessage, Exception e, boolean logException)
-      throws AccountUserNameException, OrmException, IOException, ConfigInvalidException {
-    if (logException) {
-      log.error(errorMessage, e);
-    } else {
-      log.error(errorMessage);
-    }
-    if (!realm.allowsEdit(AccountFieldName.USER_NAME)) {
-      // setting the given user name has failed, but the realm does not
-      // allow the user to manually set a user name,
-      // this means we would end with an account without user name
-      // (without 'username:<USERNAME>' external ID),
-      // such an account cannot be used for uploading changes,
-      // this is why the best we can do here is to fail early and cleanup
-      // the database
-      accountsUpdateFactory.create().delete(account);
-      externalIdsUpdateFactory.create().delete(extId);
-      throw new AccountUserNameException(errorMessage, e);
-    }
-  }
-
-  /**
    * Link another authentication identity to an existing account.
    *
    * @param to account to link the identity onto.
@@ -453,7 +411,12 @@
                 .filter(e -> e.key().equals(who.getExternalIdKey()))
                 .findAny()
                 .isPresent())) {
-      externalIdsUpdateFactory.create().delete(filteredExtIdsByScheme);
+      accountsUpdateFactory
+          .create()
+          .update(
+              "Delete External IDs on Update Link",
+              to,
+              u -> u.deleteExternalIds(filteredExtIdsByScheme));
     }
     return link(to, who);
   }
@@ -498,27 +461,17 @@
       }
     }
 
-    externalIdsUpdateFactory.create().delete(extIds);
-
-    if (extIds.stream().anyMatch(e -> e.email() != null)) {
-      accountsUpdateFactory
-          .create()
-          .update(
-              "Clear Preferred Email on Unlinking External ID\n"
-                  + "\n"
-                  + "The preferred email is cleared because the corresponding external ID\n"
-                  + "was removed.",
-              from,
-              (a, u) -> {
-                if (a.getPreferredEmail() != null) {
-                  for (ExternalId extId : extIds) {
-                    if (a.getPreferredEmail().equals(extId.email())) {
-                      u.setPreferredEmail(null);
-                      break;
-                    }
-                  }
-                }
-              });
-    }
+    accountsUpdateFactory
+        .create()
+        .update(
+            "Unlink External ID" + (extIds.size() > 1 ? "s" : ""),
+            from,
+            (a, u) -> {
+              u.deleteExternalIds(extIds);
+              if (a.getPreferredEmail() != null
+                  && extIds.stream().anyMatch(e -> a.getPreferredEmail().equals(e.email()))) {
+                u.setPreferredEmail(null);
+              }
+            });
   }
 }
diff --git a/java/com/google/gerrit/server/account/AccountState.java b/java/com/google/gerrit/server/account/AccountState.java
index 01d31c9..6601df7 100644
--- a/java/com/google/gerrit/server/account/AccountState.java
+++ b/java/com/google/gerrit/server/account/AccountState.java
@@ -22,6 +22,7 @@
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser.PropertyKey;
 import com.google.gerrit.server.IdentifiedUser;
@@ -51,17 +52,20 @@
   private final Account account;
   private final Collection<ExternalId> externalIds;
   private final Map<ProjectWatchKey, Set<NotifyType>> projectWatches;
+  private final GeneralPreferencesInfo generalPreferences;
   private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
 
   public AccountState(
       AllUsersName allUsersName,
       Account account,
       Collection<ExternalId> externalIds,
-      Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
+      Map<ProjectWatchKey, Set<NotifyType>> projectWatches,
+      GeneralPreferencesInfo generalPreferences) {
     this.allUsersName = allUsersName;
     this.account = account;
     this.externalIds = externalIds;
     this.projectWatches = projectWatches;
+    this.generalPreferences = generalPreferences;
     this.account.setUserName(getUserName(externalIds));
   }
 
@@ -117,6 +121,11 @@
     return projectWatches;
   }
 
+  /** The general preferences of the account. */
+  public GeneralPreferencesInfo getGeneralPreferences() {
+    return generalPreferences;
+  }
+
   public static String getUserName(Collection<ExternalId> ids) {
     for (ExternalId extId : ids) {
       if (extId.isScheme(SCHEME_USERNAME)) {
diff --git a/java/com/google/gerrit/server/account/AccountUserNameException.java b/java/com/google/gerrit/server/account/AccountUserNameException.java
index f1a2555..a1f1df2 100644
--- a/java/com/google/gerrit/server/account/AccountUserNameException.java
+++ b/java/com/google/gerrit/server/account/AccountUserNameException.java
@@ -21,6 +21,10 @@
 public class AccountUserNameException extends AccountException {
   private static final long serialVersionUID = 1L;
 
+  public AccountUserNameException(String message) {
+    super(message);
+  }
+
   public AccountUserNameException(String message, Throwable why) {
     super(message, why);
   }
diff --git a/java/com/google/gerrit/server/account/Accounts.java b/java/com/google/gerrit/server/account/Accounts.java
index 7b04610..45831ae 100644
--- a/java/com/google/gerrit/server/account/Accounts.java
+++ b/java/com/google/gerrit/server/account/Accounts.java
@@ -18,12 +18,13 @@
 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;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -46,28 +47,25 @@
 
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
-  private final OutgoingEmailValidator emailValidator;
+  private final ExternalIds externalIds;
 
   @Inject
-  Accounts(
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      OutgoingEmailValidator emailValidator) {
+  Accounts(GitRepositoryManager repoManager, AllUsersName allUsersName, ExternalIds externalIds) {
     this.repoManager = repoManager;
     this.allUsersName = allUsersName;
-    this.emailValidator = emailValidator;
+    this.externalIds = externalIds;
   }
 
   @Nullable
-  public Account get(Account.Id accountId) throws IOException, ConfigInvalidException {
+  public AccountState get(Account.Id accountId) throws IOException, ConfigInvalidException {
     try (Repository repo = repoManager.openRepository(allUsersName)) {
       return read(repo, accountId).orElse(null);
     }
   }
 
-  public List<Account> get(Collection<Account.Id> accountIds)
+  public List<AccountState> get(Collection<Account.Id> accountIds)
       throws IOException, ConfigInvalidException {
-    List<Account> accounts = new ArrayList<>(accountIds.size());
+    List<AccountState> accounts = new ArrayList<>(accountIds.size());
     try (Repository repo = repoManager.openRepository(allUsersName)) {
       for (Account.Id accountId : accountIds) {
         read(repo, accountId).ifPresent(accounts::add);
@@ -81,9 +79,9 @@
    *
    * @return all accounts
    */
-  public List<Account> all() throws IOException {
+  public List<AccountState> all() throws IOException {
     Set<Account.Id> accountIds = allIds();
-    List<Account> accounts = new ArrayList<>(accountIds.size());
+    List<AccountState> accounts = new ArrayList<>(accountIds.size());
     try (Repository repo = repoManager.openRepository(allUsersName)) {
       for (Account.Id accountId : accountIds) {
         try {
@@ -136,11 +134,22 @@
     }
   }
 
-  private Optional<Account> read(Repository allUsersRepository, Account.Id accountId)
+  private Optional<AccountState> read(Repository allUsersRepository, Account.Id accountId)
       throws IOException, ConfigInvalidException {
-    AccountConfig accountConfig = new AccountConfig(emailValidator, accountId);
-    accountConfig.load(allUsersRepository);
-    return accountConfig.getLoadedAccount();
+    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()));
   }
 
   public static Stream<Account.Id> readUserRefs(Repository repo) throws IOException {
diff --git a/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java b/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java
index 0085303..0b63927 100644
--- a/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -26,21 +25,20 @@
 @Singleton
 public class AccountsConsistencyChecker {
   private final Accounts accounts;
-  private final ExternalIds externalIds;
 
   @Inject
-  AccountsConsistencyChecker(Accounts accounts, ExternalIds externalIds) {
+  AccountsConsistencyChecker(Accounts accounts) {
     this.accounts = accounts;
-    this.externalIds = externalIds;
   }
 
   public List<ConsistencyProblemInfo> check() throws IOException {
     List<ConsistencyProblemInfo> problems = new ArrayList<>();
 
-    for (Account account : accounts.all()) {
+    for (AccountState accountState : accounts.all()) {
+      Account account = accountState.getAccount();
       if (account.getPreferredEmail() != null) {
-        if (!externalIds
-            .byAccount(account.getId())
+        if (!accountState
+            .getExternalIds()
             .stream()
             .anyMatch(e -> account.getPreferredEmail().equals(e.email()))) {
           addError(
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index ee3cf87f..fb8067c 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -24,8 +24,6 @@
 import com.google.common.util.concurrent.Runnables;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
@@ -35,7 +33,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
-import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryHelper.ActionType;
@@ -51,11 +48,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.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 
 /**
@@ -82,7 +75,7 @@
    * <p>Allows to read the current state of an account and to prepare updates to it.
    */
   @FunctionalInterface
-  public static interface AccountUpdater {
+  public interface AccountUpdater {
     /**
      * Prepare updates to an account.
      *
@@ -94,12 +87,11 @@
      */
     void update(Account account, InternalAccountUpdate.Builder update);
 
-    public static AccountUpdater join(List<AccountUpdater> updaters) {
+    static AccountUpdater join(List<AccountUpdater> updaters) {
       return (a, u) -> updaters.stream().forEach(updater -> updater.update(a, u));
     }
 
-    public static AccountUpdater joinConsumers(
-        List<Consumer<InternalAccountUpdate.Builder>> consumers) {
+    static AccountUpdater joinConsumers(List<Consumer<InternalAccountUpdate.Builder>> consumers) {
       return join(Lists.transform(consumers, AccountUpdater::fromConsumer));
     }
 
@@ -119,7 +111,6 @@
     private final GitRepositoryManager repoManager;
     private final GitReferenceUpdated gitRefUpdated;
     private final AllUsersName allUsersName;
-    private final OutgoingEmailValidator emailValidator;
     private final Provider<PersonIdent> serverIdentProvider;
     private final Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
     private final RetryHelper retryHelper;
@@ -130,7 +121,6 @@
         GitRepositoryManager repoManager,
         GitReferenceUpdated gitRefUpdated,
         AllUsersName allUsersName,
-        OutgoingEmailValidator emailValidator,
         @GerritPersonIdent Provider<PersonIdent> serverIdentProvider,
         Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
         RetryHelper retryHelper,
@@ -138,7 +128,6 @@
       this.repoManager = repoManager;
       this.gitRefUpdated = gitRefUpdated;
       this.allUsersName = allUsersName;
-      this.emailValidator = emailValidator;
       this.serverIdentProvider = serverIdentProvider;
       this.metaDataUpdateInternalFactory = metaDataUpdateInternalFactory;
       this.retryHelper = retryHelper;
@@ -152,7 +141,6 @@
           gitRefUpdated,
           null,
           allUsersName,
-          emailValidator,
           metaDataUpdateInternalFactory,
           retryHelper,
           extIdNotesFactory,
@@ -175,7 +163,6 @@
     private final GitRepositoryManager repoManager;
     private final GitReferenceUpdated gitRefUpdated;
     private final AllUsersName allUsersName;
-    private final OutgoingEmailValidator emailValidator;
     private final Provider<PersonIdent> serverIdentProvider;
     private final Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
     private final RetryHelper retryHelper;
@@ -186,7 +173,6 @@
         GitRepositoryManager repoManager,
         GitReferenceUpdated gitRefUpdated,
         AllUsersName allUsersName,
-        OutgoingEmailValidator emailValidator,
         @GerritPersonIdent Provider<PersonIdent> serverIdentProvider,
         Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
         RetryHelper retryHelper,
@@ -194,7 +180,6 @@
       this.repoManager = repoManager;
       this.gitRefUpdated = gitRefUpdated;
       this.allUsersName = allUsersName;
-      this.emailValidator = emailValidator;
       this.serverIdentProvider = serverIdentProvider;
       this.metaDataUpdateInternalFactory = metaDataUpdateInternalFactory;
       this.retryHelper = retryHelper;
@@ -208,7 +193,6 @@
           gitRefUpdated,
           null,
           allUsersName,
-          emailValidator,
           metaDataUpdateInternalFactory,
           retryHelper,
           extIdNotesFactory,
@@ -228,7 +212,6 @@
     private final GitRepositoryManager repoManager;
     private final GitReferenceUpdated gitRefUpdated;
     private final AllUsersName allUsersName;
-    private final OutgoingEmailValidator emailValidator;
     private final Provider<PersonIdent> serverIdentProvider;
     private final Provider<IdentifiedUser> identifiedUser;
     private final Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
@@ -240,7 +223,6 @@
         GitRepositoryManager repoManager,
         GitReferenceUpdated gitRefUpdated,
         AllUsersName allUsersName,
-        OutgoingEmailValidator emailValidator,
         @GerritPersonIdent Provider<PersonIdent> serverIdentProvider,
         Provider<IdentifiedUser> identifiedUser,
         Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
@@ -250,7 +232,6 @@
       this.gitRefUpdated = gitRefUpdated;
       this.allUsersName = allUsersName;
       this.serverIdentProvider = serverIdentProvider;
-      this.emailValidator = emailValidator;
       this.identifiedUser = identifiedUser;
       this.metaDataUpdateInternalFactory = metaDataUpdateInternalFactory;
       this.retryHelper = retryHelper;
@@ -266,7 +247,6 @@
           gitRefUpdated,
           user,
           allUsersName,
-          emailValidator,
           metaDataUpdateInternalFactory,
           retryHelper,
           extIdNotesFactory,
@@ -283,7 +263,6 @@
   private final GitReferenceUpdated gitRefUpdated;
   @Nullable private final IdentifiedUser currentUser;
   private final AllUsersName allUsersName;
-  private final OutgoingEmailValidator emailValidator;
   private final Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
   private final RetryHelper retryHelper;
   private final ExternalIdNotesLoader extIdNotesLoader;
@@ -296,7 +275,6 @@
       GitReferenceUpdated gitRefUpdated,
       @Nullable IdentifiedUser currentUser,
       AllUsersName allUsersName,
-      OutgoingEmailValidator emailValidator,
       Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
       RetryHelper retryHelper,
       ExternalIdNotesLoader extIdNotesLoader,
@@ -307,7 +285,6 @@
         gitRefUpdated,
         currentUser,
         allUsersName,
-        emailValidator,
         metaDataUpdateInternalFactory,
         retryHelper,
         extIdNotesLoader,
@@ -322,7 +299,6 @@
       GitReferenceUpdated gitRefUpdated,
       @Nullable IdentifiedUser currentUser,
       AllUsersName allUsersName,
-      OutgoingEmailValidator emailValidator,
       Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
       RetryHelper retryHelper,
       ExternalIdNotesLoader extIdNotesLoader,
@@ -333,7 +309,6 @@
     this.gitRefUpdated = checkNotNull(gitRefUpdated, "gitRefUpdated");
     this.currentUser = currentUser;
     this.allUsersName = checkNotNull(allUsersName, "allUsersName");
-    this.emailValidator = checkNotNull(emailValidator, "emailValidator");
     this.metaDataUpdateInternalFactory =
         checkNotNull(metaDataUpdateInternalFactory, "metaDataUpdateInternalFactory");
     this.retryHelper = checkNotNull(retryHelper, "retryHelper");
@@ -446,81 +421,10 @@
         });
   }
 
-  /**
-   * Deletes the account.
-   *
-   * @param account the account that should be deleted
-   * @throws IOException if deleting the user branch fails due to an IO error
-   * @throws OrmException if deleting the user branch fails
-   * @throws ConfigInvalidException
-   */
-  public void delete(Account account) throws IOException, OrmException, ConfigInvalidException {
-    deleteByKey(account.getId());
-  }
-
-  /**
-   * Deletes the account.
-   *
-   * @param accountId the ID of the account that should be deleted
-   * @throws IOException if deleting the user branch fails due to an IO error
-   * @throws OrmException if deleting the user branch fails
-   * @throws ConfigInvalidException
-   */
-  public void deleteByKey(Account.Id accountId)
-      throws IOException, OrmException, ConfigInvalidException {
-    deleteAccount(accountId);
-  }
-
-  private Account deleteAccount(Account.Id accountId)
-      throws IOException, OrmException, ConfigInvalidException {
-    return retryHelper.execute(
-        ActionType.ACCOUNT_UPDATE,
-        () -> {
-          deleteUserBranch(accountId);
-          return null;
-        });
-  }
-
-  private void deleteUserBranch(Account.Id accountId) throws IOException {
-    try (Repository repo = repoManager.openRepository(allUsersName)) {
-      deleteUserBranch(repo, allUsersName, gitRefUpdated, currentUser, authorIdent, accountId);
-    }
-  }
-
-  public static void deleteUserBranch(
-      Repository repo,
-      Project.NameKey project,
-      GitReferenceUpdated gitRefUpdated,
-      @Nullable IdentifiedUser user,
-      PersonIdent refLogIdent,
-      Account.Id accountId)
-      throws IOException {
-    String refName = RefNames.refsUsers(accountId);
-    Ref ref = repo.exactRef(refName);
-    if (ref == null) {
-      return;
-    }
-
-    RefUpdate ru = repo.updateRef(refName);
-    ru.setExpectedOldObjectId(ref.getObjectId());
-    ru.setNewObjectId(ObjectId.zeroId());
-    ru.setForceUpdate(true);
-    ru.setRefLogIdent(refLogIdent);
-    ru.setRefLogMessage("Delete Account", true);
-    Result result = ru.delete();
-    if (result != Result.FORCED) {
-      throw new IOException(String.format("Failed to delete ref %s: %s", refName, result.name()));
-    }
-    gitRefUpdated.fire(project, ru, user != null ? user.getAccount() : null);
-  }
-
   private AccountConfig read(Repository allUsersRepo, Account.Id accountId)
       throws IOException, ConfigInvalidException {
-    AccountConfig accountConfig = new AccountConfig(emailValidator, accountId);
-    accountConfig.load(allUsersRepo);
-
+    AccountConfig accountConfig = new AccountConfig(accountId, allUsersRepo).load();
     afterReadRevision.run();
-
     return accountConfig;
   }
 
diff --git a/java/com/google/gerrit/server/account/ChangeUserName.java b/java/com/google/gerrit/server/account/ChangeUserName.java
deleted file mode 100644
index b332b75..0000000
--- a/java/com/google/gerrit/server/account/ChangeUserName.java
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.account;
-
-import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.NameAlreadyUsedException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIds;
-import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
-import java.util.concurrent.Callable;
-import java.util.regex.Pattern;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-
-/** Operation to change the username of an account. */
-public class ChangeUserName implements Callable<VoidResult> {
-  public static final String USERNAME_CANNOT_BE_CHANGED = "Username cannot be changed.";
-
-  private static final Pattern USER_NAME_PATTERN = Pattern.compile(Account.USER_NAME_PATTERN);
-
-  /** Generic factory to change any user's username. */
-  public interface Factory {
-    ChangeUserName create(
-        @Assisted("message") String message,
-        IdentifiedUser user,
-        @Assisted("newUsername") String newUsername);
-  }
-
-  private final SshKeyCache sshKeyCache;
-  private final ExternalIds externalIds;
-  private final AccountsUpdate.Server accountsUpdate;
-
-  private final String message;
-  private final IdentifiedUser user;
-  private final String newUsername;
-
-  @Inject
-  ChangeUserName(
-      SshKeyCache sshKeyCache,
-      ExternalIds externalIds,
-      AccountsUpdate.Server accountsUpdate,
-      @Assisted("message") String message,
-      @Assisted IdentifiedUser user,
-      @Nullable @Assisted("newUsername") String newUsername) {
-    this.sshKeyCache = sshKeyCache;
-    this.externalIds = externalIds;
-    this.accountsUpdate = accountsUpdate;
-    this.message = message;
-    this.user = user;
-    this.newUsername = newUsername;
-  }
-
-  @Override
-  public VoidResult call()
-      throws OrmException, NameAlreadyUsedException, InvalidUserNameException, IOException,
-          ConfigInvalidException {
-    if (!externalIds.byAccount(user.getAccountId(), SCHEME_USERNAME).isEmpty()) {
-      throw new IllegalStateException(USERNAME_CANNOT_BE_CHANGED);
-    }
-
-    if (newUsername != null && !newUsername.isEmpty()) {
-      if (!USER_NAME_PATTERN.matcher(newUsername).matches()) {
-        throw new InvalidUserNameException();
-      }
-
-      ExternalId.Key key = ExternalId.Key.create(SCHEME_USERNAME, newUsername);
-      try {
-        accountsUpdate
-            .create()
-            .update(
-                message,
-                user.getAccountId(),
-                u -> u.addExternalId(ExternalId.create(key, user.getAccountId(), null, null)));
-      } catch (OrmDuplicateKeyException dupeErr) {
-        // If we are using this identity, don't report the exception.
-        //
-        ExternalId other = externalIds.get(key);
-        if (other != null && other.accountId().equals(user.getAccountId())) {
-          return VoidResult.INSTANCE;
-        }
-
-        // Otherwise, someone else has this identity.
-        //
-        throw new NameAlreadyUsedException(newUsername);
-      }
-    }
-
-    sshKeyCache.evict(newUsername);
-    return VoidResult.INSTANCE;
-  }
-}
diff --git a/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java b/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
deleted file mode 100644
index 8043773..0000000
--- a/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
+++ /dev/null
@@ -1,198 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.account;
-
-import static com.google.gerrit.server.config.ConfigUtil.loadSection;
-import static com.google.gerrit.server.config.ConfigUtil.skipField;
-import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
-import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_ID;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_MATCH;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_TOKEN;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
-import static com.google.gerrit.server.git.UserConfigSections.URL_ALIAS;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
-import com.google.gerrit.extensions.client.MenuItem;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.config.AllUsersName;
-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 java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@Singleton
-public class GeneralPreferencesLoader {
-  private static final Logger log = LoggerFactory.getLogger(GeneralPreferencesLoader.class);
-
-  private final GitRepositoryManager gitMgr;
-  private final AllUsersName allUsersName;
-
-  @Inject
-  public GeneralPreferencesLoader(GitRepositoryManager gitMgr, AllUsersName allUsersName) {
-    this.gitMgr = gitMgr;
-    this.allUsersName = allUsersName;
-  }
-
-  public GeneralPreferencesInfo load(Account.Id id)
-      throws IOException, ConfigInvalidException, RepositoryNotFoundException {
-    return read(id, null);
-  }
-
-  public GeneralPreferencesInfo merge(Account.Id id, GeneralPreferencesInfo in)
-      throws IOException, ConfigInvalidException, RepositoryNotFoundException {
-    return read(id, in);
-  }
-
-  private GeneralPreferencesInfo read(Account.Id id, GeneralPreferencesInfo in)
-      throws IOException, ConfigInvalidException, RepositoryNotFoundException {
-    try (Repository allUsers = gitMgr.openRepository(allUsersName)) {
-      // Load all users default prefs
-      VersionedAccountPreferences dp = VersionedAccountPreferences.forDefault();
-      dp.load(allUsers);
-
-      // Load user prefs
-      VersionedAccountPreferences p = VersionedAccountPreferences.forUser(id);
-      p.load(allUsers);
-      GeneralPreferencesInfo r =
-          loadSection(
-              p.getConfig(),
-              UserConfigSections.GENERAL,
-              null,
-              new GeneralPreferencesInfo(),
-              readDefaultsFromGit(dp.getConfig(), in),
-              in);
-      loadChangeTableColumns(r, p, dp);
-      return loadMyMenusAndUrlAliases(r, p, dp);
-    }
-  }
-
-  public GeneralPreferencesInfo readDefaultsFromGit(Repository git, GeneralPreferencesInfo in)
-      throws ConfigInvalidException, IOException {
-    VersionedAccountPreferences dp = VersionedAccountPreferences.forDefault();
-    dp.load(git);
-    return readDefaultsFromGit(dp.getConfig(), in);
-  }
-
-  private GeneralPreferencesInfo readDefaultsFromGit(Config config, GeneralPreferencesInfo in)
-      throws ConfigInvalidException {
-    GeneralPreferencesInfo allUserPrefs = new GeneralPreferencesInfo();
-    loadSection(
-        config,
-        UserConfigSections.GENERAL,
-        null,
-        allUserPrefs,
-        GeneralPreferencesInfo.defaults(),
-        in);
-    return updateDefaults(allUserPrefs);
-  }
-
-  private GeneralPreferencesInfo updateDefaults(GeneralPreferencesInfo input) {
-    GeneralPreferencesInfo result = GeneralPreferencesInfo.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("Cannot get default general preferences from " + allUsersName.get(), e);
-      return GeneralPreferencesInfo.defaults();
-    }
-    return result;
-  }
-
-  public GeneralPreferencesInfo loadMyMenusAndUrlAliases(
-      GeneralPreferencesInfo r, VersionedAccountPreferences v, VersionedAccountPreferences d) {
-    r.my = my(v);
-    if (r.my.isEmpty() && !v.isDefaults()) {
-      r.my = my(d);
-    }
-    if (r.my.isEmpty()) {
-      r.my.add(new MenuItem("Changes", "#/dashboard/self", null));
-      r.my.add(new MenuItem("Draft Comments", "#/q/has:draft", null));
-      r.my.add(new MenuItem("Edits", "#/q/has:edit", null));
-      r.my.add(new MenuItem("Watched Changes", "#/q/is:watched+is:open", null));
-      r.my.add(new MenuItem("Starred Changes", "#/q/is:starred", null));
-      r.my.add(new MenuItem("Groups", "#/groups/self", null));
-    }
-
-    r.urlAliases = urlAliases(v);
-    if (r.urlAliases == null && !v.isDefaults()) {
-      r.urlAliases = urlAliases(d);
-    }
-    return r;
-  }
-
-  private static List<MenuItem> my(VersionedAccountPreferences v) {
-    List<MenuItem> my = new ArrayList<>();
-    Config cfg = v.getConfig();
-    for (String subsection : cfg.getSubsections(UserConfigSections.MY)) {
-      String url = my(cfg, subsection, KEY_URL, "#/");
-      String target = my(cfg, subsection, KEY_TARGET, url.startsWith("#") ? null : "_blank");
-      my.add(new MenuItem(subsection, url, target, my(cfg, subsection, KEY_ID, null)));
-    }
-    return my;
-  }
-
-  private static String my(Config cfg, String subsection, String key, String defaultValue) {
-    String val = cfg.getString(UserConfigSections.MY, subsection, key);
-    return !Strings.isNullOrEmpty(val) ? val : defaultValue;
-  }
-
-  public GeneralPreferencesInfo loadChangeTableColumns(
-      GeneralPreferencesInfo r, VersionedAccountPreferences v, VersionedAccountPreferences d) {
-    r.changeTable = changeTable(v);
-
-    if (r.changeTable.isEmpty() && !v.isDefaults()) {
-      r.changeTable = changeTable(d);
-    }
-    return r;
-  }
-
-  private static List<String> changeTable(VersionedAccountPreferences v) {
-    return Lists.newArrayList(v.getConfig().getStringList(CHANGE_TABLE, null, CHANGE_TABLE_COLUMN));
-  }
-
-  private static Map<String, String> urlAliases(VersionedAccountPreferences v) {
-    HashMap<String, String> urlAliases = new HashMap<>();
-    Config cfg = v.getConfig();
-    for (String subsection : cfg.getSubsections(URL_ALIAS)) {
-      urlAliases.put(
-          cfg.getString(URL_ALIAS, subsection, KEY_MATCH),
-          cfg.getString(URL_ALIAS, subsection, KEY_TOKEN));
-    }
-    return !urlAliases.isEmpty() ? urlAliases : null;
-  }
-}
diff --git a/java/com/google/gerrit/server/account/InternalAccountUpdate.java b/java/com/google/gerrit/server/account/InternalAccountUpdate.java
index ea778ca..05c431e 100644
--- a/java/com/google/gerrit/server/account/InternalAccountUpdate.java
+++ b/java/com/google/gerrit/server/account/InternalAccountUpdate.java
@@ -16,12 +16,18 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.WatchConfig.NotifyType;
+import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import java.util.Collection;
+import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 
 /**
  * Class to prepare updates to an account.
@@ -93,6 +99,30 @@
   public abstract ImmutableSet<ExternalId> getDeletedExternalIds();
 
   /**
+   * Returns external IDs that should be updated for the account.
+   *
+   * @return external IDs that should be updated for the account
+   */
+  public abstract ImmutableMap<ProjectWatchKey, Set<NotifyType>> getUpdatedProjectWatches();
+
+  /**
+   * Returns project watches that should be deleted for the account.
+   *
+   * @return project watches that should be deleted for the account
+   */
+  public abstract ImmutableSet<ProjectWatchKey> getDeletedProjectWatches();
+
+  /**
+   * Returns the new value for the general preferences.
+   *
+   * <p>Only preferences that are non-null in the returned GeneralPreferencesInfo should be updated.
+   *
+   * @return the new value for the general preferences, {@code Optional#empty()} if the general
+   *     preferences are not being updated, the wrapped value is never {@code null}
+   */
+  public abstract Optional<GeneralPreferencesInfo> getGeneralPreferences();
+
+  /**
    * Class to build an account update.
    *
    * <p>Account data is only updated if the corresponding setter is invoked. If a setter is not
@@ -239,7 +269,7 @@
     }
 
     /**
-     * Delete external IDs for the account.
+     * Deletes external IDs for the account.
      *
      * <p>The account IDs of the external IDs must match the account ID of the account that is
      * updated.
@@ -278,6 +308,83 @@
     }
 
     /**
+     * Returns a builder for the map of updated project watches.
+     *
+     * @return builder for the map of updated project watches.
+     */
+    abstract ImmutableMap.Builder<ProjectWatchKey, Set<NotifyType>> updatedProjectWatchesBuilder();
+
+    /**
+     * Updates a project watch for the account.
+     *
+     * <p>If no project watch with the key exists the project watch is created.
+     *
+     * @param projectWatchKey key of the project watch that should be updated
+     * @param notifyTypes the notify types that should be set for the project watch
+     * @return the builder
+     */
+    public Builder updateProjectWatch(
+        ProjectWatchKey projectWatchKey, Set<NotifyType> notifyTypes) {
+      return updateProjectWatches(ImmutableMap.of(projectWatchKey, notifyTypes));
+    }
+
+    /**
+     * Updates project watches for the account.
+     *
+     * <p>If any of the project watches already exists, it is overwritten. New project watches are
+     * inserted.
+     *
+     * @param projectWatches project watches that should be updated
+     * @return the builder
+     */
+    public Builder updateProjectWatches(Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
+      updatedProjectWatchesBuilder().putAll(projectWatches);
+      return this;
+    }
+
+    /**
+     * Returns a builder for the set of deleted project watches.
+     *
+     * @return builder for the set of deleted project watches.
+     */
+    abstract ImmutableSet.Builder<ProjectWatchKey> deletedProjectWatchesBuilder();
+
+    /**
+     * Deletes a project watch for the account.
+     *
+     * <p>If no project watch with the ID exists this is a no-op.
+     *
+     * @param projectWatch project watch that should be deleted
+     * @return the builder
+     */
+    public Builder deleteProjectWatch(ProjectWatchKey projectWatch) {
+      return deleteProjectWatches(ImmutableSet.of(projectWatch));
+    }
+
+    /**
+     * Deletes project watches for the account.
+     *
+     * <p>For non-existing project watches this is a no-op.
+     *
+     * @param projectWatches project watches that should be deleted
+     * @return the builder
+     */
+    public Builder deleteProjectWatches(Collection<ProjectWatchKey> projectWatches) {
+      deletedProjectWatchesBuilder().addAll(projectWatches);
+      return this;
+    }
+
+    /**
+     * Sets the general preferences for the account.
+     *
+     * <p>Updates any preference that is non-null in the provided GeneralPreferencesInfo.
+     *
+     * @param generalPreferences the general preferences that should be set
+     * @return the builder
+     */
+    public abstract Builder setGeneralPreferences(GeneralPreferencesInfo generalPreferences);
+
+    /**
      * Builds the account update.
      *
      * @return the account update
@@ -385,6 +492,34 @@
         delegate.deleteExternalIds(extIds);
         return this;
       }
+
+      @Override
+      ImmutableMap.Builder<ProjectWatchKey, Set<NotifyType>> updatedProjectWatchesBuilder() {
+        return delegate.updatedProjectWatchesBuilder();
+      }
+
+      @Override
+      public Builder updateProjectWatches(Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
+        delegate.updateProjectWatches(projectWatches);
+        return this;
+      }
+
+      @Override
+      ImmutableSet.Builder<ProjectWatchKey> deletedProjectWatchesBuilder() {
+        return delegate.deletedProjectWatchesBuilder();
+      }
+
+      @Override
+      public Builder deleteProjectWatches(Collection<ProjectWatchKey> projectWatches) {
+        delegate.deleteProjectWatches(projectWatches);
+        return this;
+      }
+
+      @Override
+      public Builder setGeneralPreferences(GeneralPreferencesInfo generalPreferences) {
+        delegate.setGeneralPreferences(generalPreferences);
+        return this;
+      }
     }
   }
 }
diff --git a/java/com/google/gerrit/server/account/PreferencesConfig.java b/java/com/google/gerrit/server/account/PreferencesConfig.java
new file mode 100644
index 0000000..32df659
--- /dev/null
+++ b/java/com/google/gerrit/server/account/PreferencesConfig.java
@@ -0,0 +1,368 @@
+// 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.account;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+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 static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_ID;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_MATCH;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_TOKEN;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
+import static com.google.gerrit.server.git.UserConfigSections.URL_ALIAS;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.MenuItem;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.UserConfigSections;
+import com.google.gerrit.server.git.ValidationError;
+import com.google.gerrit.server.git.VersionedMetaData;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PreferencesConfig {
+  private static final Logger log = LoggerFactory.getLogger(PreferencesConfig.class);
+
+  public static final String PREFERENCES_CONFIG = "preferences.config";
+
+  private final Account.Id accountId;
+  private final Config cfg;
+  private final Config defaultCfg;
+  private final ValidationError.Sink validationErrorSink;
+
+  private GeneralPreferencesInfo generalPreferences;
+
+  public PreferencesConfig(
+      Account.Id accountId,
+      Config cfg,
+      Config defaultCfg,
+      ValidationError.Sink validationErrorSink) {
+    this.accountId = checkNotNull(accountId, "accountId");
+    this.cfg = checkNotNull(cfg, "cfg");
+    this.defaultCfg = checkNotNull(defaultCfg, "defaultCfg");
+    this.validationErrorSink = checkNotNull(validationErrorSink, "validationErrorSink");
+  }
+
+  public GeneralPreferencesInfo getGeneralPreferences() {
+    if (generalPreferences == null) {
+      parse();
+    }
+    return generalPreferences;
+  }
+
+  public void parse() {
+    generalPreferences = parse(null);
+  }
+
+  public Config saveGeneralPreferences(GeneralPreferencesInfo input) throws ConfigInvalidException {
+    // merge configs
+    input = parse(input);
+
+    storeSection(
+        cfg, UserConfigSections.GENERAL, null, input, parseDefaultPreferences(defaultCfg, null));
+    setChangeTable(cfg, input.changeTable);
+    setMy(cfg, input.my);
+    setUrlAliases(cfg, input.urlAliases);
+
+    // evict the cached general preferences
+    this.generalPreferences = null;
+
+    return cfg;
+  }
+
+  private GeneralPreferencesInfo parse(@Nullable GeneralPreferencesInfo input) {
+    try {
+      return parse(cfg, defaultCfg, input);
+    } catch (ConfigInvalidException e) {
+      validationErrorSink.error(
+          new ValidationError(
+              PREFERENCES_CONFIG,
+              String.format(
+                  "Invalid general preferences for account %d: %s",
+                  accountId.get(), e.getMessage())));
+      return new GeneralPreferencesInfo();
+    }
+  }
+
+  private static GeneralPreferencesInfo parse(
+      Config cfg, @Nullable Config defaultCfg, @Nullable GeneralPreferencesInfo input)
+      throws ConfigInvalidException {
+    GeneralPreferencesInfo r =
+        loadSection(
+            cfg,
+            UserConfigSections.GENERAL,
+            null,
+            new GeneralPreferencesInfo(),
+            defaultCfg != null
+                ? parseDefaultPreferences(defaultCfg, input)
+                : GeneralPreferencesInfo.defaults(),
+            input);
+    if (input != null) {
+      r.changeTable = input.changeTable;
+      r.my = input.my;
+      r.urlAliases = input.urlAliases;
+    } else {
+      r.changeTable = parseChangeTableColumns(cfg, defaultCfg);
+      r.my = parseMyMenus(cfg, defaultCfg);
+      r.urlAliases = parseUrlAliases(cfg, defaultCfg);
+    }
+    return r;
+  }
+
+  private static GeneralPreferencesInfo parseDefaultPreferences(
+      Config defaultCfg, GeneralPreferencesInfo input) throws ConfigInvalidException {
+    GeneralPreferencesInfo allUserPrefs = new GeneralPreferencesInfo();
+    loadSection(
+        defaultCfg,
+        UserConfigSections.GENERAL,
+        null,
+        allUserPrefs,
+        GeneralPreferencesInfo.defaults(),
+        input);
+    return updateDefaults(allUserPrefs);
+  }
+
+  private static GeneralPreferencesInfo updateDefaults(GeneralPreferencesInfo input) {
+    GeneralPreferencesInfo result = GeneralPreferencesInfo.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 general preferences", e);
+      return GeneralPreferencesInfo.defaults();
+    }
+    return result;
+  }
+
+  private static List<String> parseChangeTableColumns(Config cfg, @Nullable Config defaultCfg) {
+    List<String> changeTable = changeTable(cfg);
+    if (changeTable == null && defaultCfg != null) {
+      changeTable = changeTable(defaultCfg);
+    }
+    return changeTable;
+  }
+
+  private static List<MenuItem> parseMyMenus(Config cfg, @Nullable Config defaultCfg) {
+    List<MenuItem> my = my(cfg);
+    if (my.isEmpty() && defaultCfg != null) {
+      my = my(defaultCfg);
+    }
+    if (my.isEmpty()) {
+      my.add(new MenuItem("Changes", "#/dashboard/self", null));
+      my.add(new MenuItem("Draft Comments", "#/q/has:draft", null));
+      my.add(new MenuItem("Edits", "#/q/has:edit", null));
+      my.add(new MenuItem("Watched Changes", "#/q/is:watched+is:open", null));
+      my.add(new MenuItem("Starred Changes", "#/q/is:starred", null));
+      my.add(new MenuItem("Groups", "#/groups/self", null));
+    }
+    return my;
+  }
+
+  private static Map<String, String> parseUrlAliases(Config cfg, @Nullable Config defaultCfg) {
+    Map<String, String> urlAliases = urlAliases(cfg);
+    if (urlAliases == null && defaultCfg != null) {
+      urlAliases = urlAliases(defaultCfg);
+    }
+    return urlAliases;
+  }
+
+  public static GeneralPreferencesInfo readDefaultPreferences(Repository allUsersRepo)
+      throws IOException, ConfigInvalidException {
+    return parse(readDefaultConfig(allUsersRepo), null, null);
+  }
+
+  static Config readDefaultConfig(Repository allUsersRepo)
+      throws IOException, ConfigInvalidException {
+    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
+    defaultPrefs.load(allUsersRepo);
+    return defaultPrefs.getConfig();
+  }
+
+  public static GeneralPreferencesInfo updateDefaultPreferences(
+      MetaDataUpdate md, GeneralPreferencesInfo input) throws IOException, ConfigInvalidException {
+    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
+    defaultPrefs.load(md);
+    storeSection(
+        defaultPrefs.getConfig(),
+        UserConfigSections.GENERAL,
+        null,
+        input,
+        GeneralPreferencesInfo.defaults());
+    setMy(defaultPrefs.getConfig(), input.my);
+    setChangeTable(defaultPrefs.getConfig(), input.changeTable);
+    setUrlAliases(defaultPrefs.getConfig(), input.urlAliases);
+    defaultPrefs.commit(md);
+
+    return parse(defaultPrefs.getConfig(), null, null);
+  }
+
+  private static List<String> changeTable(Config cfg) {
+    return Lists.newArrayList(cfg.getStringList(CHANGE_TABLE, null, CHANGE_TABLE_COLUMN));
+  }
+
+  private static void setChangeTable(Config cfg, List<String> changeTable) {
+    if (changeTable != null) {
+      unsetSection(cfg, UserConfigSections.CHANGE_TABLE);
+      cfg.setStringList(UserConfigSections.CHANGE_TABLE, null, CHANGE_TABLE_COLUMN, changeTable);
+    }
+  }
+
+  private static List<MenuItem> my(Config cfg) {
+    List<MenuItem> my = new ArrayList<>();
+    for (String subsection : cfg.getSubsections(UserConfigSections.MY)) {
+      String url = my(cfg, subsection, KEY_URL, "#/");
+      String target = my(cfg, subsection, KEY_TARGET, url.startsWith("#") ? null : "_blank");
+      my.add(new MenuItem(subsection, url, target, my(cfg, subsection, KEY_ID, null)));
+    }
+    return my;
+  }
+
+  private static String my(Config cfg, String subsection, String key, String defaultValue) {
+    String val = cfg.getString(UserConfigSections.MY, subsection, key);
+    return !Strings.isNullOrEmpty(val) ? val : defaultValue;
+  }
+
+  private static void setMy(Config cfg, List<MenuItem> my) {
+    if (my != null) {
+      unsetSection(cfg, UserConfigSections.MY);
+      for (MenuItem item : my) {
+        checkState(!isNullOrEmpty(item.name), "MenuItem.name must not be null or empty");
+        checkState(!isNullOrEmpty(item.url), "MenuItem.url must not be null or empty");
+
+        setMy(cfg, item.name, KEY_URL, item.url);
+        setMy(cfg, item.name, KEY_TARGET, item.target);
+        setMy(cfg, item.name, KEY_ID, item.id);
+      }
+    }
+  }
+
+  public static void validateMy(List<MenuItem> my) throws BadRequestException {
+    if (my == null) {
+      return;
+    }
+    for (MenuItem item : my) {
+      checkRequiredMenuItemField(item.name, "name");
+      checkRequiredMenuItemField(item.url, "URL");
+    }
+  }
+
+  private static void checkRequiredMenuItemField(String value, String name)
+      throws BadRequestException {
+    if (isNullOrEmpty(value)) {
+      throw new BadRequestException(name + " for menu item is required");
+    }
+  }
+
+  private static boolean isNullOrEmpty(String value) {
+    return value == null || value.trim().isEmpty();
+  }
+
+  private static void setMy(Config cfg, String section, String key, @Nullable String val) {
+    if (val == null || val.trim().isEmpty()) {
+      cfg.unset(UserConfigSections.MY, section.trim(), key);
+    } else {
+      cfg.setString(UserConfigSections.MY, section.trim(), key, val.trim());
+    }
+  }
+
+  private static Map<String, String> urlAliases(Config cfg) {
+    HashMap<String, String> urlAliases = new HashMap<>();
+    for (String subsection : cfg.getSubsections(URL_ALIAS)) {
+      urlAliases.put(
+          cfg.getString(URL_ALIAS, subsection, KEY_MATCH),
+          cfg.getString(URL_ALIAS, subsection, KEY_TOKEN));
+    }
+    return !urlAliases.isEmpty() ? urlAliases : null;
+  }
+
+  private static void setUrlAliases(Config cfg, Map<String, String> urlAliases) {
+    if (urlAliases != null) {
+      for (String subsection : cfg.getSubsections(URL_ALIAS)) {
+        cfg.unsetSection(URL_ALIAS, subsection);
+      }
+
+      int i = 1;
+      for (Entry<String, String> e : urlAliases.entrySet()) {
+        cfg.setString(URL_ALIAS, URL_ALIAS + i, KEY_MATCH, e.getKey());
+        cfg.setString(URL_ALIAS, URL_ALIAS + i, KEY_TOKEN, e.getValue());
+        i++;
+      }
+    }
+  }
+
+  private static void unsetSection(Config cfg, String section) {
+    cfg.unsetSection(section, null);
+    for (String subsection : cfg.getSubsections(section)) {
+      cfg.unsetSection(section, subsection);
+    }
+  }
+
+  private static class VersionedDefaultPreferences extends VersionedMetaData {
+    private Config cfg;
+
+    @Override
+    protected String getRefName() {
+      return RefNames.REFS_USERS_DEFAULT;
+    }
+
+    private Config getConfig() {
+      checkState(cfg != null, "Default preferences not loaded yet.");
+      return cfg;
+    }
+
+    @Override
+    protected void onLoad() throws IOException, ConfigInvalidException {
+      cfg = readConfig(PREFERENCES_CONFIG);
+    }
+
+    @Override
+    protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
+      if (Strings.isNullOrEmpty(commit.getMessage())) {
+        commit.setMessage("Update default preferences\n");
+      }
+      saveConfig(PREFERENCES_CONFIG, cfg);
+      return true;
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/account/WatchConfig.java b/java/com/google/gerrit/server/account/WatchConfig.java
index 667ca37..7adadf7 100644
--- a/java/com/google/gerrit/server/account/WatchConfig.java
+++ b/java/com/google/gerrit/server/account/WatchConfig.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.account;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
@@ -23,7 +23,6 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
@@ -31,17 +30,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ValidationError;
-import com.google.gerrit.server.git.VersionedMetaData;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.EnumSet;
@@ -49,10 +38,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Repository;
 
 /**
  * ‘watch.config’ file in the user branch in the All-Users repository that contains the watch
@@ -85,91 +71,7 @@
  *
  * <p>Unknown notify types are ignored and removed on save.
  */
-public class WatchConfig extends VersionedMetaData implements ValidationError.Sink {
-  @Singleton
-  public static class Accessor {
-    private final GitRepositoryManager repoManager;
-    private final AllUsersName allUsersName;
-    private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
-    private final IdentifiedUser.GenericFactory userFactory;
-
-    @Inject
-    Accessor(
-        GitRepositoryManager repoManager,
-        AllUsersName allUsersName,
-        Provider<MetaDataUpdate.User> metaDataUpdateFactory,
-        IdentifiedUser.GenericFactory userFactory) {
-      this.repoManager = repoManager;
-      this.allUsersName = allUsersName;
-      this.metaDataUpdateFactory = metaDataUpdateFactory;
-      this.userFactory = userFactory;
-    }
-
-    public Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches(Account.Id accountId)
-        throws IOException, ConfigInvalidException {
-      try (Repository git = repoManager.openRepository(allUsersName)) {
-        WatchConfig watchConfig = new WatchConfig(accountId);
-        watchConfig.load(git);
-        return watchConfig.getProjectWatches();
-      }
-    }
-
-    public synchronized void upsertProjectWatches(
-        Account.Id accountId, Map<ProjectWatchKey, Set<NotifyType>> newProjectWatches)
-        throws IOException, ConfigInvalidException {
-      WatchConfig watchConfig = read(accountId);
-      Map<ProjectWatchKey, Set<NotifyType>> projectWatches = watchConfig.getProjectWatches();
-      projectWatches.putAll(newProjectWatches);
-      commit(watchConfig);
-    }
-
-    public synchronized void deleteProjectWatches(
-        Account.Id accountId, Collection<ProjectWatchKey> projectWatchKeys)
-        throws IOException, ConfigInvalidException {
-      WatchConfig watchConfig = read(accountId);
-      Map<ProjectWatchKey, Set<NotifyType>> projectWatches = watchConfig.getProjectWatches();
-      boolean commit = false;
-      for (ProjectWatchKey key : projectWatchKeys) {
-        if (projectWatches.remove(key) != null) {
-          commit = true;
-        }
-      }
-      if (commit) {
-        commit(watchConfig);
-      }
-    }
-
-    public synchronized void deleteAllProjectWatches(Account.Id accountId)
-        throws IOException, ConfigInvalidException {
-      WatchConfig watchConfig = read(accountId);
-      boolean commit = false;
-      if (!watchConfig.getProjectWatches().isEmpty()) {
-        watchConfig.getProjectWatches().clear();
-        commit = true;
-      }
-      if (commit) {
-        commit(watchConfig);
-      }
-    }
-
-    private WatchConfig read(Account.Id accountId) throws IOException, ConfigInvalidException {
-      try (Repository git = repoManager.openRepository(allUsersName)) {
-        WatchConfig watchConfig = new WatchConfig(accountId);
-        watchConfig.load(git);
-        return watchConfig;
-      }
-    }
-
-    private void commit(WatchConfig watchConfig) throws IOException {
-      try (MetaDataUpdate md =
-          metaDataUpdateFactory
-              .get()
-              .create(allUsersName, userFactory.create(watchConfig.accountId))) {
-        watchConfig.commit(md);
-      }
-    }
-  }
-
+public class WatchConfig {
   @AutoValue
   public abstract static class ProjectWatchKey {
     public static ProjectWatchKey create(Project.NameKey project, @Nullable String filter) {
@@ -199,25 +101,26 @@
   public static final String KEY_NOTIFY = "notify";
 
   private final Account.Id accountId;
-  private final String ref;
+  private final Config cfg;
+  private final ValidationError.Sink validationErrorSink;
 
   private Map<ProjectWatchKey, Set<NotifyType>> projectWatches;
-  private List<ValidationError> validationErrors;
 
-  public WatchConfig(Account.Id accountId) {
-    this.accountId = accountId;
-    this.ref = RefNames.refsUsers(accountId);
+  public WatchConfig(Account.Id accountId, Config cfg, ValidationError.Sink validationErrorSink) {
+    this.accountId = checkNotNull(accountId, "accountId");
+    this.cfg = checkNotNull(cfg, "cfg");
+    this.validationErrorSink = checkNotNull(validationErrorSink, "validationErrorSink");
   }
 
-  @Override
-  protected String getRefName() {
-    return ref;
+  public Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches() {
+    if (projectWatches == null) {
+      parse();
+    }
+    return projectWatches;
   }
 
-  @Override
-  protected void onLoad() throws IOException, ConfigInvalidException {
-    Config cfg = readConfig(WATCH_CONFIG);
-    projectWatches = parse(accountId, cfg, this);
+  public void parse() {
+    projectWatches = parse(accountId, cfg, validationErrorSink);
   }
 
   @VisibleForTesting
@@ -248,24 +151,8 @@
     return projectWatches;
   }
 
-  Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches() {
-    checkLoaded();
-    return projectWatches;
-  }
-
-  public void setProjectWatches(Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
+  public Config save(Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
     this.projectWatches = projectWatches;
-  }
-
-  @Override
-  protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
-    checkLoaded();
-
-    if (Strings.isNullOrEmpty(commit.getMessage())) {
-      commit.setMessage("Updated watch configuration\n");
-    }
-
-    Config cfg = readConfig(WATCH_CONFIG);
 
     for (String projectName : cfg.getSubsections(PROJECT)) {
       cfg.unsetSection(PROJECT, projectName);
@@ -282,32 +169,7 @@
       cfg.setStringList(PROJECT, e.getKey(), KEY_NOTIFY, new ArrayList<>(e.getValue()));
     }
 
-    saveConfig(WATCH_CONFIG, cfg);
-    return true;
-  }
-
-  private void checkLoaded() {
-    checkState(projectWatches != null, "project watches not loaded yet");
-  }
-
-  @Override
-  public void error(ValidationError error) {
-    if (validationErrors == null) {
-      validationErrors = new ArrayList<>(4);
-    }
-    validationErrors.add(error);
-  }
-
-  /**
-   * Get the validation errors, if any were discovered during load.
-   *
-   * @return list of errors; empty list if there are no errors.
-   */
-  public List<ValidationError> getValidationErrors() {
-    if (validationErrors != null) {
-      return ImmutableList.copyOf(validationErrors);
-    }
-    return ImmutableList.of();
+    return cfg;
   }
 
   @AutoValue
@@ -356,7 +218,7 @@
       return create(filter, notifyTypes);
     }
 
-    public static NotifyValue create(@Nullable String filter, Set<NotifyType> notifyTypes) {
+    public static NotifyValue create(@Nullable String filter, Collection<NotifyType> notifyTypes) {
       return new AutoValue_WatchConfig_NotifyValue(
           Strings.emptyToNull(filter), Sets.immutableEnumSet(notifyTypes));
     }
diff --git a/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java b/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
index 1033641..5894051 100644
--- a/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
+++ b/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
@@ -35,20 +35,6 @@
   }
 
   @Override
-  public void onCreate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId) {}
-
-  @Override
-  public void onUpdate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId) {}
-
-  @Override
-  public void onReplace(
-      ObjectId oldNotesRev,
-      ObjectId newNotesRev,
-      Account.Id accountId,
-      Collection<ExternalId> toRemove,
-      Collection<ExternalId> toAdd) {}
-
-  @Override
   public void onReplace(
       ObjectId oldNotesRev,
       ObjectId newNotesRev,
@@ -56,14 +42,16 @@
       Collection<ExternalId> toAdd) {}
 
   @Override
-  public void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId) {}
-
-  @Override
   public ImmutableSet<ExternalId> byAccount(Account.Id accountId) {
     throw new UnsupportedOperationException();
   }
 
   @Override
+  public ImmutableSet<ExternalId> byAccount(Account.Id accountId, ObjectId rev) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
   public ImmutableSetMultimap<Account.Id, ExternalId> allByAccount() throws IOException {
     throw new UnsupportedOperationException();
   }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 1ac8f3d..8be7092 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -33,6 +33,7 @@
 import java.util.Collection;
 import java.util.Objects;
 import java.util.Set;
+import java.util.regex.Pattern;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
@@ -40,6 +41,12 @@
 
 @AutoValue
 public abstract class ExternalId implements Serializable {
+  private static final Pattern USER_NAME_PATTERN = Pattern.compile(Account.USER_NAME_PATTERN);
+
+  public static boolean isValidUsername(String username) {
+    return USER_NAME_PATTERN.matcher(username).matches();
+  }
+
   private static final long serialVersionUID = 1L;
 
   private static final String EXTERNAL_ID_SECTION = "externalId";
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
index d928e15..ffb4e76 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import java.io.IOException;
 import java.util.Collection;
-import java.util.Collections;
 import org.eclipse.jgit.lib.ObjectId;
 
 /**
@@ -29,20 +28,6 @@
  * cache is up to date.
  */
 interface ExternalIdCache {
-  void onCreate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId)
-      throws IOException;
-
-  void onUpdate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId)
-      throws IOException;
-
-  void onReplace(
-      ObjectId oldNotesRev,
-      ObjectId newNotesRev,
-      Account.Id accountId,
-      Collection<ExternalId> toRemove,
-      Collection<ExternalId> toAdd)
-      throws IOException;
-
   void onReplace(
       ObjectId oldNotesRev,
       ObjectId newNotesRev,
@@ -50,11 +35,10 @@
       Collection<ExternalId> toAdd)
       throws IOException;
 
-  void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId)
-      throws IOException;
-
   ImmutableSet<ExternalId> byAccount(Account.Id accountId) throws IOException;
 
+  ImmutableSet<ExternalId> byAccount(Account.Id accountId, ObjectId rev) throws IOException;
+
   ImmutableSetMultimap<Account.Id, ExternalId> allByAccount() throws IOException;
 
   ImmutableSetMultimap<String, ExternalId> byEmails(String... emails) throws IOException;
@@ -64,19 +48,4 @@
   default ImmutableSet<ExternalId> byEmail(String email) throws IOException {
     return byEmails(email).get(email);
   }
-
-  default void onCreate(ObjectId oldNotesRev, ObjectId newNotesRev, ExternalId extId)
-      throws IOException {
-    onCreate(oldNotesRev, newNotesRev, Collections.singleton(extId));
-  }
-
-  default void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, ExternalId extId)
-      throws IOException {
-    onRemove(oldNotesRev, newNotesRev, Collections.singleton(extId));
-  }
-
-  default void onUpdate(ObjectId oldNotesRev, ObjectId newNotesRev, ExternalId updatedExtId)
-      throws IOException {
-    onUpdate(oldNotesRev, newNotesRev, Collections.singleton(updatedExtId));
-  }
 }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
index 6f80713..25789a1 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
@@ -15,17 +15,14 @@
 package com.google.gerrit.server.account.externalids;
 
 import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
-import static java.util.stream.Collectors.toSet;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
-import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.MultimapBuilder;
@@ -77,73 +74,6 @@
   }
 
   @Override
-  public void onCreate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extIds)
-      throws IOException {
-    updateCache(
-        oldNotesRev,
-        newNotesRev,
-        m -> {
-          for (ExternalId extId : extIds) {
-            extId.checkThatBlobIdIsSet();
-            m.put(extId.accountId(), extId);
-          }
-        });
-  }
-
-  @Override
-  public void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extIds)
-      throws IOException {
-    updateCache(
-        oldNotesRev,
-        newNotesRev,
-        m -> {
-          for (ExternalId extId : extIds) {
-            m.remove(extId.accountId(), extId);
-          }
-        });
-  }
-
-  @Override
-  public void onUpdate(
-      ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> updatedExtIds)
-      throws IOException {
-    updateCache(
-        oldNotesRev,
-        newNotesRev,
-        m -> {
-          removeKeys(m.values(), updatedExtIds.stream().map(e -> e.key()).collect(toSet()));
-          for (ExternalId updatedExtId : updatedExtIds) {
-            updatedExtId.checkThatBlobIdIsSet();
-            m.put(updatedExtId.accountId(), updatedExtId);
-          }
-        });
-  }
-
-  @Override
-  public void onReplace(
-      ObjectId oldNotesRev,
-      ObjectId newNotesRev,
-      Account.Id accountId,
-      Collection<ExternalId> toRemove,
-      Collection<ExternalId> toAdd)
-      throws IOException {
-    ExternalIdNotes.checkSameAccount(Iterables.concat(toRemove, toAdd), accountId);
-
-    updateCache(
-        oldNotesRev,
-        newNotesRev,
-        m -> {
-          for (ExternalId extId : toRemove) {
-            m.remove(extId.accountId(), extId);
-          }
-          for (ExternalId extId : toAdd) {
-            extId.checkThatBlobIdIsSet();
-            m.put(extId.accountId(), extId);
-          }
-        });
-  }
-
-  @Override
   public void onReplace(
       ObjectId oldNotesRev,
       ObjectId newNotesRev,
@@ -170,6 +100,11 @@
   }
 
   @Override
+  public ImmutableSet<ExternalId> byAccount(Account.Id accountId, ObjectId rev) throws IOException {
+    return get(rev).byAccount().get(accountId);
+  }
+
+  @Override
   public ImmutableSetMultimap<Account.Id, ExternalId> allByAccount() throws IOException {
     return get().byAccount();
   }
@@ -190,8 +125,12 @@
   }
 
   private AllExternalIds get() throws IOException {
+    return get(externalIdReader.readRevision());
+  }
+
+  private AllExternalIds get(ObjectId rev) throws IOException {
     try {
-      return extIdsByAccount.get(externalIdReader.readRevision());
+      return extIdsByAccount.get(rev);
     } catch (ExecutionException e) {
       throw new IOException("Cannot load external ids", e);
     }
@@ -221,10 +160,6 @@
     }
   }
 
-  private static void removeKeys(Collection<ExternalId> ids, Collection<ExternalId.Key> toRemove) {
-    Collections2.transform(ids, e -> e.key()).removeAll(toRemove);
-  }
-
   private static class Loader extends CacheLoader<ObjectId, AllExternalIds> {
     private final ExternalIdReader externalIdReader;
 
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index f663247..8e69f2e 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -171,7 +171,7 @@
   private Runnable afterReadRevision;
   private boolean readOnly = false;
 
-  ExternalIdNotes(
+  private ExternalIdNotes(
       ExternalIdCache externalIdCache,
       @Nullable AccountCache accountCache,
       Repository allUsersRepo) {
@@ -205,7 +205,7 @@
    *
    * @return {@link ExternalIdNotes} instance for chaining
    */
-  ExternalIdNotes load() throws IOException, ConfigInvalidException {
+  private ExternalIdNotes load() throws IOException, ConfigInvalidException {
     load(repo);
     return this;
   }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIds.java b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
index 5732bce..a8ecc40 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
@@ -75,6 +75,20 @@
     return byAccount(accountId).stream().filter(e -> e.key().isScheme(scheme)).collect(toSet());
   }
 
+  /** Returns the external IDs of the specified account. */
+  public Set<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)
+      throws IOException {
+    return byAccount(accountId, rev)
+        .stream()
+        .filter(e -> e.key().isScheme(scheme))
+        .collect(toSet());
+  }
+
   /** Returns all external IDs by account. */
   public ImmutableSetMultimap<Account.Id, ExternalId> allByAccount() throws IOException {
     return externalIdCache.allByAccount();
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java b/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
deleted file mode 100644
index 028fd8d..0000000
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
+++ /dev/null
@@ -1,460 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.account.externalids;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.util.concurrent.Runnables;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.metrics.Counter0;
-import com.google.gerrit.metrics.Description;
-import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.account.AccountCache;
-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.update.RetryHelper;
-import com.google.gerrit.server.update.RetryHelper.ActionType;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-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.Collection;
-import java.util.Collections;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Repository;
-
-/**
- * Updates externalIds in ReviewDb and NoteDb.
- *
- * <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called
- * refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content
- * is a git config file that contains an external ID. It has exactly one externalId subsection with
- * an accountId and optionally email and password:
- *
- * <pre>
- * [externalId "username:jdoe"]
- *   accountId = 1003407
- *   email = jdoe@example.com
- *   password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
- * </pre>
- *
- * For NoteDb each method call results in one commit on refs/meta/external-ids branch.
- *
- * <p>On updating external IDs this class takes care to evict affected accounts from the account
- * cache and thus triggers reindex for them.
- */
-public class ExternalIdsUpdate {
-  /**
-   * Factory to create an ExternalIdsUpdate instance for updating external IDs by the Gerrit server.
-   *
-   * <p>The Gerrit server identity will be used as author and committer for all commits that update
-   * the external IDs.
-   */
-  @Singleton
-  public static class Server {
-    private final GitRepositoryManager repoManager;
-    private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
-    private final AccountCache accountCache;
-    private final AllUsersName allUsersName;
-    private final MetricMaker metricMaker;
-    private final ExternalIds externalIds;
-    private final ExternalIdCache externalIdCache;
-    private final RetryHelper retryHelper;
-
-    @Inject
-    public Server(
-        GitRepositoryManager repoManager,
-        Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory,
-        AccountCache accountCache,
-        AllUsersName allUsersName,
-        MetricMaker metricMaker,
-        ExternalIds externalIds,
-        ExternalIdCache externalIdCache,
-        RetryHelper retryHelper) {
-      this.repoManager = repoManager;
-      this.metaDataUpdateServerFactory = metaDataUpdateServerFactory;
-      this.accountCache = accountCache;
-      this.allUsersName = allUsersName;
-      this.metricMaker = metricMaker;
-      this.externalIds = externalIds;
-      this.externalIdCache = externalIdCache;
-      this.retryHelper = retryHelper;
-    }
-
-    public ExternalIdsUpdate create() {
-      return new ExternalIdsUpdate(
-          repoManager,
-          () -> metaDataUpdateServerFactory.get().create(allUsersName),
-          accountCache,
-          allUsersName,
-          metricMaker,
-          externalIds,
-          externalIdCache,
-          retryHelper);
-    }
-  }
-
-  /**
-   * Factory to create an ExternalIdsUpdate instance for updating external IDs by the Gerrit server.
-   *
-   * <p>Using this class no reindex will be performed for the affected accounts and they will also
-   * not be evicted from the account cache.
-   *
-   * <p>The Gerrit server identity will be used as author and committer for all commits that update
-   * the external IDs.
-   */
-  @Singleton
-  public static class ServerNoReindex {
-    private final GitRepositoryManager repoManager;
-    private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
-    private final AllUsersName allUsersName;
-    private final MetricMaker metricMaker;
-    private final ExternalIds externalIds;
-    private final ExternalIdCache externalIdCache;
-    private final RetryHelper retryHelper;
-
-    @Inject
-    public ServerNoReindex(
-        GitRepositoryManager repoManager,
-        Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory,
-        AllUsersName allUsersName,
-        MetricMaker metricMaker,
-        ExternalIds externalIds,
-        ExternalIdCache externalIdCache,
-        RetryHelper retryHelper) {
-      this.repoManager = repoManager;
-      this.metaDataUpdateServerFactory = metaDataUpdateServerFactory;
-      this.allUsersName = allUsersName;
-      this.metricMaker = metricMaker;
-      this.externalIds = externalIds;
-      this.externalIdCache = externalIdCache;
-      this.retryHelper = retryHelper;
-    }
-
-    public ExternalIdsUpdate create() {
-      return new ExternalIdsUpdate(
-          repoManager,
-          () -> metaDataUpdateServerFactory.get().create(allUsersName),
-          null,
-          allUsersName,
-          metricMaker,
-          externalIds,
-          externalIdCache,
-          retryHelper);
-    }
-  }
-
-  /**
-   * Factory to create an ExternalIdsUpdate instance for updating external IDs by the current user.
-   *
-   * <p>The identity of the current user will be used as author for all commits that update the
-   * external IDs. The Gerrit server identity will be used as committer.
-   */
-  @Singleton
-  public static class User {
-    private final GitRepositoryManager repoManager;
-    private final Provider<MetaDataUpdate.User> metaDataUpdateUserFactory;
-    private final AccountCache accountCache;
-    private final AllUsersName allUsersName;
-    private final MetricMaker metricMaker;
-    private final ExternalIds externalIds;
-    private final ExternalIdCache externalIdCache;
-    private final RetryHelper retryHelper;
-
-    @Inject
-    public User(
-        GitRepositoryManager repoManager,
-        Provider<MetaDataUpdate.User> metaDataUpdateUserFactory,
-        AccountCache accountCache,
-        AllUsersName allUsersName,
-        MetricMaker metricMaker,
-        ExternalIds externalIds,
-        ExternalIdCache externalIdCache,
-        RetryHelper retryHelper) {
-      this.repoManager = repoManager;
-      this.metaDataUpdateUserFactory = metaDataUpdateUserFactory;
-      this.accountCache = accountCache;
-      this.allUsersName = allUsersName;
-      this.metricMaker = metricMaker;
-      this.externalIds = externalIds;
-      this.externalIdCache = externalIdCache;
-      this.retryHelper = retryHelper;
-    }
-
-    public ExternalIdsUpdate create() {
-      return new ExternalIdsUpdate(
-          repoManager,
-          () -> metaDataUpdateUserFactory.get().create(allUsersName),
-          accountCache,
-          allUsersName,
-          metricMaker,
-          externalIds,
-          externalIdCache,
-          retryHelper);
-    }
-  }
-
-  private final GitRepositoryManager repoManager;
-  private final MetaDataUpdateFactory metaDataUpdateFactory;
-  @Nullable private final AccountCache accountCache;
-  private final AllUsersName allUsersName;
-  private final ExternalIds externalIds;
-  private final ExternalIdCache externalIdCache;
-  private final RetryHelper retryHelper;
-  private final Runnable afterReadRevision;
-  private final Counter0 updateCount;
-
-  private ExternalIdsUpdate(
-      GitRepositoryManager repoManager,
-      MetaDataUpdateFactory metaDataUpdateFactory,
-      @Nullable AccountCache accountCache,
-      AllUsersName allUsersName,
-      MetricMaker metricMaker,
-      ExternalIds externalIds,
-      ExternalIdCache externalIdCache,
-      RetryHelper retryHelper) {
-    this(
-        repoManager,
-        metaDataUpdateFactory,
-        accountCache,
-        allUsersName,
-        metricMaker,
-        externalIds,
-        externalIdCache,
-        retryHelper,
-        Runnables.doNothing());
-  }
-
-  @VisibleForTesting
-  public ExternalIdsUpdate(
-      GitRepositoryManager repoManager,
-      MetaDataUpdateFactory metaDataUpdateFactory,
-      @Nullable AccountCache accountCache,
-      AllUsersName allUsersName,
-      MetricMaker metricMaker,
-      ExternalIds externalIds,
-      ExternalIdCache externalIdCache,
-      RetryHelper retryHelper,
-      Runnable afterReadRevision) {
-    this.repoManager = checkNotNull(repoManager, "repoManager");
-    this.metaDataUpdateFactory = checkNotNull(metaDataUpdateFactory, "metaDataUpdateFactory");
-    this.accountCache = accountCache;
-    this.allUsersName = checkNotNull(allUsersName, "allUsersName");
-    this.externalIds = checkNotNull(externalIds, "externalIds");
-    this.externalIdCache = checkNotNull(externalIdCache, "externalIdCache");
-    this.retryHelper = checkNotNull(retryHelper, "retryHelper");
-    this.afterReadRevision = checkNotNull(afterReadRevision, "afterReadRevision");
-    this.updateCount =
-        metricMaker.newCounter(
-            "notedb/external_id_update_count",
-            new Description("Total number of external ID updates.").setRate().setUnit("updates"));
-  }
-
-  /**
-   * Inserts a new external ID.
-   *
-   * <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}.
-   */
-  public void insert(ExternalId extId) throws IOException, ConfigInvalidException, OrmException {
-    insert(Collections.singleton(extId));
-  }
-
-  /**
-   * Inserts new external IDs.
-   *
-   * <p>If any of the external ID already exists, the insert fails with {@link
-   * OrmDuplicateKeyException}.
-   */
-  public void insert(Collection<ExternalId> extIds)
-      throws IOException, ConfigInvalidException, OrmException {
-    updateNoteMap(n -> n.insert(extIds));
-  }
-
-  /**
-   * Inserts or updates an external ID.
-   *
-   * <p>If the external ID already exists, it is overwritten, otherwise it is inserted.
-   */
-  public void upsert(ExternalId extId) throws IOException, ConfigInvalidException, OrmException {
-    upsert(Collections.singleton(extId));
-  }
-
-  /**
-   * Inserts or updates external IDs.
-   *
-   * <p>If any of the external IDs already exists, it is overwritten. New external IDs are inserted.
-   */
-  public void upsert(Collection<ExternalId> extIds)
-      throws IOException, ConfigInvalidException, OrmException {
-    updateNoteMap(n -> n.upsert(extIds));
-  }
-
-  /**
-   * Deletes an external ID.
-   *
-   * @throws IllegalStateException is thrown if there is an existing external ID that has the same
-   *     key, but otherwise doesn't match the specified external ID.
-   */
-  public void delete(ExternalId extId) throws IOException, ConfigInvalidException, OrmException {
-    delete(Collections.singleton(extId));
-  }
-
-  /**
-   * Deletes external IDs.
-   *
-   * @throws IllegalStateException is thrown if there is an existing external ID that has the same
-   *     key as any of the external IDs that should be deleted, but otherwise doesn't match the that
-   *     external ID.
-   */
-  public void delete(Collection<ExternalId> extIds)
-      throws IOException, ConfigInvalidException, OrmException {
-    updateNoteMap(n -> n.delete(extIds));
-  }
-
-  /**
-   * Delete an external ID by key.
-   *
-   * @throws IllegalStateException is thrown if the external ID does not belong to the specified
-   *     account.
-   */
-  public void delete(Account.Id accountId, ExternalId.Key extIdKey)
-      throws IOException, ConfigInvalidException, OrmException {
-    delete(accountId, Collections.singleton(extIdKey));
-  }
-
-  /**
-   * Delete external IDs by external ID key.
-   *
-   * @throws IllegalStateException is thrown if any of the external IDs does not belong to the
-   *     specified account.
-   */
-  public void delete(Account.Id accountId, Collection<ExternalId.Key> extIdKeys)
-      throws IOException, ConfigInvalidException, OrmException {
-    updateNoteMap(n -> n.delete(accountId, extIdKeys));
-  }
-
-  /**
-   * Delete external IDs by external ID key.
-   *
-   * <p>The external IDs are deleted regardless of which account they belong to.
-   */
-  public void deleteByKeys(Collection<ExternalId.Key> extIdKeys)
-      throws IOException, ConfigInvalidException, OrmException {
-    updateNoteMap(n -> n.deleteByKeys(extIdKeys));
-  }
-
-  /** Deletes all external IDs of the specified account. */
-  public void deleteAll(Account.Id accountId)
-      throws IOException, ConfigInvalidException, OrmException {
-    delete(externalIds.byAccount(accountId));
-  }
-
-  /**
-   * Replaces external IDs for an account by external ID keys.
-   *
-   * <p>Deletion of external IDs is done before adding the new external IDs. This means if an
-   * external ID key is specified for deletion and an external ID with the same key is specified to
-   * be added, the old external ID with that key is deleted first and then the new external ID is
-   * added (so the external ID for that key is replaced).
-   *
-   * @throws IllegalStateException is thrown if any of the specified external IDs does not belong to
-   *     the specified account.
-   */
-  public void replace(
-      Account.Id accountId, Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
-      throws IOException, ConfigInvalidException, OrmException {
-    updateNoteMap(n -> n.replace(accountId, toDelete, toAdd));
-  }
-
-  /**
-   * Replaces external IDs for an account by external ID keys.
-   *
-   * <p>Deletion of external IDs is done before adding the new external IDs. This means if an
-   * external ID key is specified for deletion and an external ID with the same key is specified to
-   * be added, the old external ID with that key is deleted first and then the new external ID is
-   * added (so the external ID for that key is replaced).
-   *
-   * <p>The external IDs are replaced regardless of which account they belong to.
-   */
-  public void replaceByKeys(Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
-      throws IOException, ConfigInvalidException, OrmException {
-    updateNoteMap(n -> n.replaceByKeys(toDelete, toAdd));
-  }
-
-  /**
-   * Replaces an external ID.
-   *
-   * @throws IllegalStateException is thrown if the specified external IDs belong to different
-   *     accounts.
-   */
-  public void replace(ExternalId toDelete, ExternalId toAdd)
-      throws IOException, ConfigInvalidException, OrmException {
-    replace(Collections.singleton(toDelete), Collections.singleton(toAdd));
-  }
-
-  /**
-   * Replaces external IDs.
-   *
-   * <p>Deletion of external IDs is done before adding the new external IDs. This means if an
-   * external ID is specified for deletion and an external ID with the same key is specified to be
-   * added, the old external ID with that key is deleted first and then the new external ID is added
-   * (so the external ID for that key is replaced).
-   *
-   * @throws IllegalStateException is thrown if the specified external IDs belong to different
-   *     accounts.
-   */
-  public void replace(Collection<ExternalId> toDelete, Collection<ExternalId> toAdd)
-      throws IOException, ConfigInvalidException, OrmException {
-    updateNoteMap(n -> n.replace(toDelete, toAdd));
-  }
-
-  private void updateNoteMap(ExternalIdUpdater updater)
-      throws IOException, ConfigInvalidException, OrmException {
-    retryHelper.execute(
-        ActionType.ACCOUNT_UPDATE,
-        () -> {
-          try (Repository repo = repoManager.openRepository(allUsersName)) {
-            ExternalIdNotes extIdNotes =
-                new ExternalIdNotes(externalIdCache, accountCache, repo)
-                    .setAfterReadRevision(afterReadRevision)
-                    .load();
-            updater.update(extIdNotes);
-            try (MetaDataUpdate metaDataUpdate = metaDataUpdateFactory.create()) {
-              extIdNotes.commit(metaDataUpdate);
-            }
-            extIdNotes.updateCaches();
-            updateCount.increment();
-            return null;
-          }
-        });
-  }
-
-  @FunctionalInterface
-  private static interface ExternalIdUpdater {
-    void update(ExternalIdNotes extIdsNotes)
-        throws IOException, ConfigInvalidException, OrmException;
-  }
-
-  @VisibleForTesting
-  @FunctionalInterface
-  public static interface MetaDataUpdateFactory {
-    MetaDataUpdate create() throws IOException;
-  }
-}
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 232cc63..366ebfb 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -382,8 +382,12 @@
   }
 
   @Override
-  public List<EmailInfo> getEmails() {
-    return getEmails.apply(account);
+  public List<EmailInfo> getEmails() throws RestApiException {
+    try {
+      return getEmails.apply(account);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get emails", e);
+    }
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/api/accounts/AccountsImpl.java b/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
index fac16dc..f5f1a34 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -156,6 +156,7 @@
       myQueryAccounts.setQuery(r.getQuery());
       myQueryAccounts.setLimit(r.getLimit());
       myQueryAccounts.setStart(r.getStart());
+      myQueryAccounts.setSuggest(r.getSuggest());
       for (ListAccountsOption option : r.getOptions()) {
         myQueryAccounts.addOption(option);
       }
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 8ed1ef0..5e60906 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -569,7 +569,8 @@
                 new Branch.NameKey(ctx.getProject(), refName),
                 ctx.getIdentifiedUser(),
                 new NoSshInfo(),
-                ctx.getRevWalk())
+                ctx.getRevWalk(),
+                change)
             .validate(event);
       }
     } catch (CommitValidationException e) {
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 81558e3..8ba755f 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -33,6 +33,7 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWER_UPDATES;
+import static com.google.gerrit.extensions.client.ListChangesOption.SKIP_MERGEABLE;
 import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
 import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS;
 import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS;
@@ -521,7 +522,9 @@
       if (str.isOk()) {
         out.submitType = str.type;
       }
-      out.mergeable = cd.isMergeable();
+      if (!has(SKIP_MERGEABLE)) {
+        out.mergeable = cd.isMergeable();
+      }
       if (has(SUBMITTABLE)) {
         out.submittable = submittable(cd);
       }
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index 4166bf7..8c40ad1 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -103,7 +103,7 @@
   }
 
   public PermissionBackend.ForChange permissions() {
-    return permissionBackend.user(user).change(notes);
+    return permissionBackend.user(user).database(db).change(notes);
   }
 
   public CurrentUser getUser() {
diff --git a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index 119051e..7ba18e8 100644
--- a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
@@ -59,6 +60,7 @@
 
   public static final ImmutableBiMap<SubmitType, Character> SUBMIT_TYPES =
       new ImmutableBiMap.Builder<SubmitType, Character>()
+          .put(SubmitType.INHERIT, 'I')
           .put(SubmitType.FAST_FORWARD_ONLY, 'F')
           .put(SubmitType.MERGE_IF_NECESSARY, 'M')
           .put(SubmitType.REBASE_ALWAYS, 'P')
@@ -98,6 +100,11 @@
     private String mergeStrategy;
 
     public EntryKey(ObjectId commit, ObjectId into, SubmitType submitType, String mergeStrategy) {
+      checkArgument(
+          submitType != SubmitType.INHERIT,
+          "Cannot cache %s.%s",
+          SubmitType.class.getSimpleName(),
+          submitType);
       this.commit = checkNotNull(commit, "commit");
       this.into = checkNotNull(into, "into");
       this.submitType = checkNotNull(submitType, "submitType");
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index d298730..3a32f8f 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -319,6 +319,7 @@
           .change(origNotes)
           .check(ChangePermission.ADD_PATCH_SET);
     }
+    projectCache.checkedGet(ctx.getProject()).checkStatePermitsWrite();
     if (!validate) {
       return;
     }
@@ -344,7 +345,8 @@
               origNotes.getChange().getDest(),
               ctx.getIdentifiedUser(),
               new NoSshInfo(),
-              ctx.getRevWalk())
+              ctx.getRevWalk(),
+              origNotes.getChange())
           .validate(event);
     } catch (CommitValidationException e) {
       throw new ResourceConflictException(e.getFullMessage());
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 1dda0e5..74e19b6 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -86,7 +86,6 @@
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.AccountVisibilityProvider;
 import com.google.gerrit.server.account.CapabilityCollection;
-import com.google.gerrit.server.account.ChangeUserName;
 import com.google.gerrit.server.account.EmailExpander;
 import com.google.gerrit.server.account.GroupCacheImpl;
 import com.google.gerrit.server.account.GroupControl;
@@ -166,7 +165,6 @@
 import com.google.gerrit.server.project.PermissionCollection;
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.ProjectNameLockManager;
-import com.google.gerrit.server.project.ProjectNode;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SectionSortCache;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
@@ -264,7 +262,6 @@
     factory(MergeUtil.Factory.class);
     factory(PatchScriptFactory.Factory.class);
     factory(PluginUser.Factory.class);
-    factory(ProjectNode.Factory.class);
     factory(ProjectState.Factory.class);
     factory(RegisterNewEmailSender.Factory.class);
     factory(ReplacePatchSetSender.Factory.class);
@@ -421,7 +418,6 @@
     factory(VersionedAuthorizedKeys.Factory.class);
 
     bind(AccountManager.class);
-    factory(ChangeUserName.Factory.class);
 
     bind(new TypeLiteral<List<CommentLinkInfo>>() {})
         .toProvider(CommentLinkProvider.class)
diff --git a/java/com/google/gerrit/server/config/SitePaths.java b/java/com/google/gerrit/server/config/SitePaths.java
index 3748bfd..11ec50c 100644
--- a/java/com/google/gerrit/server/config/SitePaths.java
+++ b/java/com/google/gerrit/server/config/SitePaths.java
@@ -58,7 +58,6 @@
   public final Path ssl_keystore;
   public final Path ssh_key;
   public final Path ssh_rsa;
-  public final Path ssh_dsa;
   public final Path ssh_ecdsa_256;
   public final Path ssh_ecdsa_384;
   public final Path ssh_ecdsa_521;
@@ -106,7 +105,6 @@
     ssl_keystore = etc_dir.resolve("keystore");
     ssh_key = etc_dir.resolve("ssh_host_key");
     ssh_rsa = etc_dir.resolve("ssh_host_rsa_key");
-    ssh_dsa = etc_dir.resolve("ssh_host_dsa_key");
     ssh_ecdsa_256 = etc_dir.resolve("ssh_host_ecdsa_key");
     ssh_ecdsa_384 = etc_dir.resolve("ssh_host_ecdsa_384_key");
     ssh_ecdsa_521 = etc_dir.resolve("ssh_host_ecdsa_521_key");
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 82fa596..64f5ae7 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.RawInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -40,6 +41,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -83,6 +85,7 @@
   private final PermissionBackend permissionBackend;
   private final ChangeEditUtil changeEditUtil;
   private final PatchSetUtil patchSetUtil;
+  private final ProjectCache projectCache;
 
   @Inject
   ChangeEditModifier(
@@ -92,7 +95,8 @@
       Provider<CurrentUser> currentUser,
       PermissionBackend permissionBackend,
       ChangeEditUtil changeEditUtil,
-      PatchSetUtil patchSetUtil) {
+      PatchSetUtil patchSetUtil,
+      ProjectCache projectCache) {
     this.indexer = indexer;
     this.reviewDb = reviewDb;
     this.currentUser = currentUser;
@@ -100,6 +104,7 @@
     this.tz = gerritIdent.getTimeZone();
     this.changeEditUtil = changeEditUtil;
     this.patchSetUtil = patchSetUtil;
+    this.projectCache = projectCache;
   }
 
   /**
@@ -113,7 +118,7 @@
    */
   public void createEdit(Repository repository, ChangeNotes notes)
       throws AuthException, IOException, InvalidChangeOperationException, OrmException,
-          PermissionBackendException {
+          PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
     Optional<ChangeEdit> changeEdit = lookupChangeEdit(notes);
@@ -141,7 +146,7 @@
    */
   public void rebaseEdit(Repository repository, ChangeNotes notes)
       throws AuthException, InvalidChangeOperationException, IOException, OrmException,
-          MergeConflictException, PermissionBackendException {
+          MergeConflictException, PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
     Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
@@ -206,7 +211,7 @@
    */
   public void modifyMessage(Repository repository, ChangeNotes notes, String newCommitMessage)
       throws AuthException, IOException, UnchangedCommitMessageException, OrmException,
-          PermissionBackendException, BadRequestException {
+          PermissionBackendException, BadRequestException, ResourceConflictException {
     assertCanEdit(notes);
     newCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(newCommitMessage);
 
@@ -244,11 +249,12 @@
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
    * @throws InvalidChangeOperationException if the file already had the specified content
    * @throws PermissionBackendException
+   * @throws ResourceConflictException if the project state does not permit the operation
    */
   public void modifyFile(
       Repository repository, ChangeNotes notes, String filePath, RawInput newContent)
       throws AuthException, InvalidChangeOperationException, IOException, OrmException,
-          PermissionBackendException {
+          PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new ChangeFileContentModification(filePath, newContent));
   }
 
@@ -262,10 +268,11 @@
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
    * @throws InvalidChangeOperationException if the file does not exist
    * @throws PermissionBackendException
+   * @throws ResourceConflictException if the project state does not permit the operation
    */
   public void deleteFile(Repository repository, ChangeNotes notes, String file)
       throws AuthException, InvalidChangeOperationException, IOException, OrmException,
-          PermissionBackendException {
+          PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new DeleteFileModification(file));
   }
 
@@ -281,11 +288,12 @@
    * @throws InvalidChangeOperationException if the file was already renamed to the specified new
    *     name
    * @throws PermissionBackendException
+   * @throws ResourceConflictException if the project state does not permit the operation
    */
   public void renameFile(
       Repository repository, ChangeNotes notes, String currentFilePath, String newFilePath)
       throws AuthException, InvalidChangeOperationException, IOException, OrmException,
-          PermissionBackendException {
+          PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new RenameFileModification(currentFilePath, newFilePath));
   }
 
@@ -303,14 +311,14 @@
    */
   public void restoreFile(Repository repository, ChangeNotes notes, String file)
       throws AuthException, InvalidChangeOperationException, IOException, OrmException,
-          PermissionBackendException {
+          PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new RestoreFileModification(file));
   }
 
   private void modifyTree(
       Repository repository, ChangeNotes notes, TreeModification treeModification)
       throws AuthException, IOException, OrmException, InvalidChangeOperationException,
-          PermissionBackendException {
+          PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
     Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
@@ -355,7 +363,7 @@
       PatchSet patchSet,
       List<TreeModification> treeModifications)
       throws AuthException, IOException, InvalidChangeOperationException, MergeConflictException,
-          OrmException, PermissionBackendException {
+          OrmException, PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
     Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
@@ -385,7 +393,8 @@
     return createEdit(repository, notes, patchSet, newEditCommit, nowTimestamp);
   }
 
-  private void assertCanEdit(ChangeNotes notes) throws AuthException, PermissionBackendException {
+  private void assertCanEdit(ChangeNotes notes)
+      throws AuthException, PermissionBackendException, IOException, ResourceConflictException {
     if (!currentUser.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
@@ -395,6 +404,7 @@
           .database(reviewDb)
           .change(notes)
           .check(ChangePermission.ADD_PATCH_SET);
+      projectCache.checkedGet(notes.getProjectName()).checkStatePermitsWrite();
     } catch (AuthException denied) {
       throw new AuthException("edit not permitted", denied);
     }
diff --git a/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java b/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
index df6f5bb..6fafe4e 100644
--- a/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
+++ b/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
-//Licensed under the Apache License, Version 2.0 (the "License");
-//you may not use this file except in compliance with the License.
-//You may obtain a copy of the License at
+// 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
+// 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.
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
 
 package com.google.gerrit.server.git;
 
diff --git a/java/com/google/gerrit/server/git/ProjectConfig.java b/java/com/google/gerrit/server/git/ProjectConfig.java
index e5b4a17e..fec1ae3 100644
--- a/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.common.data.Permission.isPermission;
+import static com.google.gerrit.reviewdb.client.Project.DEFAULT_SUBMIT_TYPE;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Joiner;
@@ -42,7 +43,6 @@
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
-import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -151,7 +151,6 @@
 
   private static final String PLUGIN = "plugin";
 
-  private static final SubmitType DEFAULT_SUBMIT_ACTION = SubmitType.MERGE_IF_NECESSARY;
   private static final ProjectState DEFAULT_STATE_VALUE = ProjectState.ACTIVE;
 
   private static final String EXTENSION_PANELS = "extension-panels";
@@ -519,7 +518,7 @@
 
     p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
 
-    p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, DEFAULT_SUBMIT_ACTION));
+    p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, DEFAULT_SUBMIT_TYPE));
     p.setState(getEnum(rc, PROJECT, null, KEY_STATE, DEFAULT_STATE_VALUE));
 
     p.setDefaultDashboard(rc.getString(DASHBOARD, null, KEY_DEFAULT));
@@ -1043,7 +1042,7 @@
         KEY_MAX_OBJECT_SIZE_LIMIT,
         validMaxObjectSizeLimit(p.getMaxObjectSizeLimit()));
 
-    set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), DEFAULT_SUBMIT_ACTION);
+    set(rc, SUBMIT, null, KEY_ACTION, p.getConfiguredSubmitType(), DEFAULT_SUBMIT_TYPE);
 
     set(rc, PROJECT, null, KEY_STATE, p.getState(), DEFAULT_STATE_VALUE);
 
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index a058aec..292fa7a 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -376,6 +376,7 @@
   private MagicBranchInput magicBranch;
   private boolean newChangeForAllNotInTarget;
   private String setFullNameTo;
+  private boolean setChangeAsPrivate;
 
   // Handles for outputting back over the wire to the end user.
   private Task newProgress;
@@ -1036,7 +1037,7 @@
       // Must pass explicit user instead of injecting a provider into CreateRefControl, since
       // Provider<CurrentUser> within ReceiveCommits will always return anonymous.
       createRefControl.checkCreateRef(Providers.of(user), rp.getRepository(), branch, obj);
-    } catch (AuthException denied) {
+    } catch (AuthException | ResourceConflictException denied) {
       reject(cmd, "prohibited by Gerrit: " + denied.getMessage());
       return;
     }
@@ -1059,6 +1060,9 @@
     } catch (AuthException err) {
       ok = false;
     }
+    if (!projectState.statePermitsWrite()) {
+      reject(cmd, "prohibited by Gerrit: project state does not permit write");
+    }
     if (ok) {
       if (isHead(cmd) && !isCommit(cmd)) {
         return;
@@ -1116,7 +1120,7 @@
   private boolean canDelete(ReceiveCommand cmd) throws PermissionBackendException {
     try {
       permissions.ref(cmd.getRefName()).check(RefPermission.DELETE);
-      return true;
+      return projectState.statePermitsWrite();
     } catch (AuthException e) {
       return false;
     }
@@ -1155,6 +1159,9 @@
       if (!validRefOperation(cmd)) {
         return;
       }
+      if (!projectState.statePermitsWrite()) {
+        cmd.setResult(REJECTED_NONFASTFORWARD, " project state does not permit write.");
+      }
       actualCommands.add(cmd);
     } else {
       cmd.setResult(
@@ -1333,11 +1340,10 @@
       this.publish = cmd.getRefName().startsWith(MagicBranch.NEW_PUBLISH_CHANGE);
       this.labelTypes = labelTypes;
       this.notesMigration = notesMigration;
-      GeneralPreferencesInfo prefs = user.getAccount().getGeneralPreferencesInfo();
+      GeneralPreferencesInfo prefs = user.state().getGeneralPreferences();
       this.defaultPublishComments =
           prefs != null
-              ? firstNonNull(
-                  user.getAccount().getGeneralPreferencesInfo().publishCommentsOnPush, false)
+              ? firstNonNull(user.state().getGeneralPreferences().publishCommentsOnPush, false)
               : false;
     }
 
@@ -1515,12 +1521,28 @@
       reject(cmd, denied.getMessage());
       return;
     }
+    if (!projectState.statePermitsWrite()) {
+      reject(cmd, "project state does not permit write");
+      return;
+    }
 
     if (magicBranch.isPrivate && magicBranch.removePrivate) {
       reject(cmd, "the options 'private' and 'remove-private' are mutually exclusive");
       return;
     }
 
+    boolean privateByDefault =
+        projectCache.get(project.getNameKey()).is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
+    setChangeAsPrivate =
+        magicBranch.draft
+            || magicBranch.isPrivate
+            || (privateByDefault && !magicBranch.removePrivate);
+
+    if (receiveConfig.disablePrivateChanges && setChangeAsPrivate) {
+      reject(cmd, "private changes are disabled");
+      return;
+    }
+
     if (magicBranch.workInProgress && magicBranch.ready) {
       reject(cmd, "the options 'wip' and 'ready' are mutually exclusive");
       return;
@@ -1538,6 +1560,10 @@
         reject(cmd, e.getMessage());
         return;
       }
+      if (!projectState.statePermitsWrite()) {
+        reject(cmd, "project state does not permit write");
+        return;
+      }
     }
 
     RevWalk walk = rp.getRevWalk();
@@ -1830,7 +1856,8 @@
           logDebug("Creating new change for {} even though it is already tracked", name);
         }
 
-        if (!validCommit(rp.getRevWalk(), magicBranch.perm, magicBranch.dest, magicBranch.cmd, c)) {
+        if (!validCommit(
+            rp.getRevWalk(), magicBranch.perm, magicBranch.dest, magicBranch.cmd, c, null)) {
           // Not a change the user can propose? Abort as early as possible.
           newChanges = Collections.emptyList();
           logDebug("Aborting early due to invalid commit");
@@ -2133,18 +2160,13 @@
     }
 
     private void setChangeId(int id) {
-      boolean privateByDefault =
-          projectCache.get(project.getNameKey()).is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
 
       changeId = new Change.Id(id);
       ins =
           changeInserterFactory
               .create(changeId, commit, refName)
               .setTopic(magicBranch.topic)
-              .setPrivate(
-                  magicBranch.draft
-                      || magicBranch.isPrivate
-                      || (privateByDefault && !magicBranch.removePrivate))
+              .setPrivate(setChangeAsPrivate)
               .setWorkInProgress(magicBranch.workInProgress)
               // Changes already validated in validateNewCommits.
               .setValidate(false);
@@ -2383,6 +2405,10 @@
         return false;
       }
 
+      if (!projectState.statePermitsWrite()) {
+        reject(inputCommand, "cannot add patch set to " + ontoChange + ".");
+        return false;
+      }
       if (change.getStatus().isClosed()) {
         reject(inputCommand, "change " + ontoChange + " closed");
         return false;
@@ -2409,7 +2435,7 @@
       }
 
       PermissionBackend.ForRef perm = permissions.ref(change.getDest().get());
-      if (!validCommit(rp.getRevWalk(), perm, change.getDest(), inputCommand, newCommit)) {
+      if (!validCommit(rp.getRevWalk(), perm, change.getDest(), inputCommand, newCommit, change)) {
         return false;
       }
       rp.getRevWalk().parseBody(priorCommit);
@@ -2773,7 +2799,7 @@
         }
         if (existing.keySet().contains(c)) {
           continue;
-        } else if (!validCommit(walk, perm, branch, cmd, c)) {
+        } else if (!validCommit(walk, perm, branch, cmd, c, null)) {
           break;
         }
 
@@ -2795,7 +2821,8 @@
       PermissionBackend.ForRef perm,
       Branch.NameKey branch,
       ReceiveCommand cmd,
-      ObjectId id)
+      ObjectId id,
+      @Nullable Change change)
       throws IOException {
 
     if (validCommits.contains(id)) {
@@ -2813,9 +2840,10 @@
               && magicBranch.merged;
       CommitValidators validators =
           isMerged
-              ? commitValidatorsFactory.forMergedCommits(perm, user.asIdentifiedUser())
+              ? commitValidatorsFactory.forMergedCommits(
+                  project.getNameKey(), perm, user.asIdentifiedUser())
               : commitValidatorsFactory.forReceiveCommits(
-                  perm, branch, user.asIdentifiedUser(), sshInfo, repo, rw);
+                  perm, branch, user.asIdentifiedUser(), sshInfo, repo, rw, change);
       messages.addAll(validators.validate(receiveEvent));
     } catch (CommitValidationException e) {
       logDebug("Commit validation failed on {}", c.name());
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveConfig.java b/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
index 89158d3..cdbf310 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
@@ -28,6 +28,7 @@
   final boolean checkMagicRefs;
   final boolean checkReferencedObjectsAreReachable;
   final int maxBatchCommits;
+  final boolean disablePrivateChanges;
   private final int systemMaxBatchChanges;
   private final AccountLimits.Factory limitsFactory;
 
@@ -38,6 +39,7 @@
         config.getBoolean("receive", null, "checkReferencedObjectsAreReachable", true);
     maxBatchCommits = config.getInt("receive", null, "maxBatchCommits", 10000);
     systemMaxBatchChanges = config.getInt("receive", "maxBatchChanges", 0);
+    disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false);
     this.limitsFactory = limitsFactory;
   }
 
diff --git a/java/com/google/gerrit/server/git/strategy/CherryPick.java b/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 77aa950..7367a92 100644
--- a/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -15,10 +15,12 @@
 package com.google.gerrit.server.git.strategy;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.git.strategy.CommitMergeStatus.EMPTY_COMMIT;
 import static com.google.gerrit.server.git.strategy.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.server.ChangeUtil;
@@ -123,6 +125,10 @@
         toMerge.setStatusCode(CommitMergeStatus.PATH_CONFLICT);
         return;
       } catch (MergeIdenticalTreeException mie) {
+        if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)) {
+          toMerge.setStatusCode(EMPTY_COMMIT);
+          return;
+        }
         toMerge.setStatusCode(SKIPPED_IDENTICAL_TREE);
         return;
       }
diff --git a/java/com/google/gerrit/server/git/strategy/CommitMergeStatus.java b/java/com/google/gerrit/server/git/strategy/CommitMergeStatus.java
index e5c253d..634c909 100644
--- a/java/com/google/gerrit/server/git/strategy/CommitMergeStatus.java
+++ b/java/com/google/gerrit/server/git/strategy/CommitMergeStatus.java
@@ -60,7 +60,12 @@
   NOT_FAST_FORWARD(
       "Project policy requires all submissions to be a fast-forward.\n"
           + "\n"
-          + "Please rebase the change locally and upload again for review.");
+          + "Please rebase the change locally and upload again for review."),
+
+  EMPTY_COMMIT(
+      "Change could not be merged because the commit is empty.\n"
+          + "\n"
+          + "Project policy requires all commits to contain modifications to at least one file.");
 
   private final String message;
 
diff --git a/java/com/google/gerrit/server/git/strategy/FastForwardOp.java b/java/com/google/gerrit/server/git/strategy/FastForwardOp.java
index a3b10cb..50c75ef 100644
--- a/java/com/google/gerrit/server/git/strategy/FastForwardOp.java
+++ b/java/com/google/gerrit/server/git/strategy/FastForwardOp.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.git.strategy;
 
+import static com.google.gerrit.server.git.strategy.CommitMergeStatus.EMPTY_COMMIT;
+
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.update.RepoContext;
@@ -25,6 +28,12 @@
 
   @Override
   protected void updateRepoImpl(RepoContext ctx) throws IntegrationException {
+    if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)
+        && toMerge.getTree().equals(toMerge.getParent(0).getTree())) {
+      toMerge.setStatusCode(EMPTY_COMMIT);
+      return;
+    }
+
     args.mergeTip.moveTipTo(toMerge, toMerge);
   }
 }
diff --git a/java/com/google/gerrit/server/git/strategy/MergeOneOp.java b/java/com/google/gerrit/server/git/strategy/MergeOneOp.java
index 3c3812d..b6d97b9 100644
--- a/java/com/google/gerrit/server/git/strategy/MergeOneOp.java
+++ b/java/com/google/gerrit/server/git/strategy/MergeOneOp.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.git.strategy;
 
+import static com.google.gerrit.server.git.strategy.CommitMergeStatus.EMPTY_COMMIT;
+
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.update.RepoContext;
@@ -47,6 +50,11 @@
             args.destBranch,
             args.mergeTip.getCurrentTip(),
             toMerge);
+    if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)
+        && merged.getTree().equals(merged.getParent(0).getTree())) {
+      toMerge.setStatusCode(EMPTY_COMMIT);
+      return;
+    }
     args.mergeTip.moveTipTo(amendGitlink(merged), toMerge);
   }
 }
diff --git a/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
index a3e4e16..80107e8 100644
--- a/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git.strategy;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.git.strategy.CommitMergeStatus.EMPTY_COMMIT;
 import static com.google.gerrit.server.git.strategy.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
 
 import com.google.common.collect.ImmutableList;
@@ -124,6 +125,12 @@
       if (args.mergeUtil.canFastForward(
           args.mergeSorter, args.mergeTip.getCurrentTip(), args.rw, toMerge)) {
         if (!rebaseAlways) {
+          if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)
+              && toMerge.getTree().equals(toMerge.getParent(0).getTree())) {
+            toMerge.setStatusCode(EMPTY_COMMIT);
+            return;
+          }
+
           args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
           toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
           acceptMergeTip(args.mergeTip);
@@ -192,6 +199,11 @@
         newCommit = args.rw.parseCommit(rebaseOp.getRebasedCommit());
         newPatchSetId = rebaseOp.getPatchSetId();
       }
+      if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)
+          && newCommit.getTree().equals(newCommit.getParent(0).getTree())) {
+        toMerge.setStatusCode(EMPTY_COMMIT);
+        return;
+      }
       newCommit = amendGitlink(newCommit);
       newCommit.copyFrom(toMerge);
       newCommit.setPatchsetId(newPatchSetId);
diff --git a/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java b/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
index 3a954fb..585361c 100644
--- a/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
+++ b/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
@@ -133,6 +133,7 @@
         return RebaseIfNecessary.dryRun(args, repo, tipCommit, toMergeCommit);
       case REBASE_ALWAYS:
         return RebaseAlways.dryRun(args, repo, tipCommit, toMergeCommit);
+      case INHERIT:
       default:
         String errorMsg = "No submit strategy for: " + submitType;
         log.error(errorMsg);
diff --git a/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java b/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
index 7678623..8600322 100644
--- a/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
+++ b/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
@@ -96,6 +96,7 @@
         return new RebaseIfNecessary(args);
       case REBASE_ALWAYS:
         return new RebaseAlways(args);
+      case INHERIT:
       default:
         String errorMsg = "No submit strategy for: " + submitType;
         log.error(errorMsg);
diff --git a/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java b/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java
index 57094af..271e392 100644
--- a/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java
+++ b/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java
@@ -128,6 +128,7 @@
         case CANNOT_CHERRY_PICK_ROOT:
         case CANNOT_REBASE_ROOT:
         case NOT_FAST_FORWARD:
+        case EMPTY_COMMIT:
           // TODO(dborowitz): Reformat these messages to be more appropriate for
           // short problem descriptions.
           commitStatus.problem(id, CharMatcher.is('\n').collapseFrom(s.getMessage(), ' '));
diff --git a/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
index 9a362d4..bd095ef 100644
--- a/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -465,6 +465,7 @@
         case REBASE_IF_NECESSARY:
         case REBASE_ALWAYS:
           return message(ctx, commit, CommitMergeStatus.CLEAN_REBASE);
+        case INHERIT:
         default:
           throw new IllegalStateException(
               "unexpected submit type "
diff --git a/java/com/google/gerrit/server/git/validators/AccountValidator.java b/java/com/google/gerrit/server/git/validators/AccountValidator.java
index 0f89413..bba49ea 100644
--- a/java/com/google/gerrit/server/git/validators/AccountValidator.java
+++ b/java/com/google/gerrit/server/git/validators/AccountValidator.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.server.git.validators;
 
+import static java.util.stream.Collectors.toSet;
+
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountConfig;
+import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -28,6 +31,7 @@
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 
 public class AccountValidator {
@@ -42,20 +46,21 @@
   }
 
   public List<String> validate(
-      Account.Id accountId, RevWalk rw, @Nullable ObjectId oldId, ObjectId newId)
+      Account.Id accountId, Repository repo, RevWalk rw, @Nullable ObjectId oldId, ObjectId newId)
       throws IOException {
     Optional<Account> oldAccount = Optional.empty();
     if (oldId != null && !ObjectId.zeroId().equals(oldId)) {
       try {
-        oldAccount = loadAccount(accountId, rw, oldId);
+        oldAccount = loadAccount(accountId, repo, rw, oldId, null);
       } catch (ConfigInvalidException e) {
         // ignore, maybe the new commit is repairing it now
       }
     }
 
+    List<String> messages = new ArrayList<>();
     Optional<Account> newAccount;
     try {
-      newAccount = loadAccount(accountId, rw, newId);
+      newAccount = loadAccount(accountId, repo, rw, newId, messages);
     } catch (ConfigInvalidException e) {
       return ImmutableList.of(
           String.format(
@@ -67,7 +72,6 @@
       return ImmutableList.of(String.format("account '%s' does not exist", accountId.get()));
     }
 
-    List<String> messages = new ArrayList<>();
     if (accountId.equals(self.get().getAccountId()) && !newAccount.get().isActive()) {
       messages.add("cannot deactivate own account");
     }
@@ -87,11 +91,24 @@
     return ImmutableList.copyOf(messages);
   }
 
-  private Optional<Account> loadAccount(Account.Id accountId, RevWalk rw, ObjectId commit)
+  private Optional<Account> loadAccount(
+      Account.Id accountId,
+      Repository repo,
+      RevWalk rw,
+      ObjectId commit,
+      @Nullable List<String> messages)
       throws IOException, ConfigInvalidException {
     rw.reset();
-    AccountConfig accountConfig = new AccountConfig(null, accountId);
-    accountConfig.load(rw, commit);
+    AccountConfig accountConfig = new AccountConfig(accountId, repo);
+    accountConfig.setEagerParsing(true).load(rw, commit);
+    if (messages != null) {
+      messages.addAll(
+          accountConfig
+              .getValidationErrors()
+              .stream()
+              .map(ValidationError::getMessage)
+              .collect(toSet()));
+    }
     return accountConfig.getLoadedAccount();
   }
 }
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index fc280c2..05ca98b 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
 import static java.util.stream.Collectors.toList;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.FooterConstants;
@@ -30,11 +31,13 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Branch.NameKey;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.WatchConfig;
 import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
@@ -42,9 +45,11 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.BanCommit;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ProjectCache;
@@ -85,6 +90,7 @@
     private final PersonIdent gerritIdent;
     private final String canonicalWebUrl;
     private final DynamicSet<CommitValidationListener> pluginValidators;
+    private final GitRepositoryManager repoManager;
     private final AllUsersName allUsers;
     private final AllProjectsName allProjects;
     private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
@@ -98,6 +104,7 @@
         @CanonicalWebUrl @Nullable String canonicalWebUrl,
         @GerritServerConfig Config cfg,
         DynamicSet<CommitValidationListener> pluginValidators,
+        GitRepositoryManager repoManager,
         AllUsersName allUsers,
         AllProjectsName allProjects,
         ExternalIdsConsistencyChecker externalIdsConsistencyChecker,
@@ -106,6 +113,7 @@
       this.gerritIdent = gerritIdent;
       this.canonicalWebUrl = canonicalWebUrl;
       this.pluginValidators = pluginValidators;
+      this.repoManager = repoManager;
       this.allUsers = allUsers;
       this.allProjects = allProjects;
       this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
@@ -121,54 +129,67 @@
         IdentifiedUser user,
         SshInfo sshInfo,
         Repository repo,
-        RevWalk rw)
+        RevWalk rw,
+        @Nullable Change change)
         throws IOException {
       NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(repo, rw);
       ProjectState projectState = projectCache.checkedGet(branch.getParentKey());
       return new CommitValidators(
           ImmutableList.of(
               new UploadMergesPermissionValidator(perm),
+              new ProjectStateValidationListener(projectState),
               new AmendedGerritMergeCommitValidationListener(perm, gerritIdent),
               new AuthorUploaderValidator(user, perm, canonicalWebUrl),
               new CommitterUploaderValidator(user, perm, canonicalWebUrl),
               new SignedOffByValidator(user, perm, projectState),
               new ChangeIdValidator(
-                  projectState, user, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
+                  projectState,
+                  user,
+                  canonicalWebUrl,
+                  installCommitMsgHookCommand,
+                  sshInfo,
+                  change),
               new ConfigValidator(branch, user, rw, allUsers, allProjects),
               new BannedCommitsValidator(rejectCommits),
               new PluginCommitValidationListener(pluginValidators),
               new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
-              new AccountCommitValidator(allUsers, accountValidator),
+              new AccountCommitValidator(repoManager, allUsers, accountValidator),
               new GroupCommitValidator(allUsers)));
     }
 
     public CommitValidators forGerritCommits(
-        PermissionBackend.ForRef perm,
-        Branch.NameKey branch,
+        ForRef perm,
+        NameKey branch,
         IdentifiedUser user,
         SshInfo sshInfo,
-        RevWalk rw)
+        RevWalk rw,
+        @Nullable Change change)
         throws IOException {
+      ProjectState projectState = projectCache.checkedGet(branch.getParentKey());
       return new CommitValidators(
           ImmutableList.of(
               new UploadMergesPermissionValidator(perm),
+              new ProjectStateValidationListener(projectState),
               new AmendedGerritMergeCommitValidationListener(perm, gerritIdent),
               new AuthorUploaderValidator(user, perm, canonicalWebUrl),
               new SignedOffByValidator(user, perm, projectCache.checkedGet(branch.getParentKey())),
               new ChangeIdValidator(
-                  projectCache.checkedGet(branch.getParentKey()),
+                  projectState,
                   user,
                   canonicalWebUrl,
                   installCommitMsgHookCommand,
-                  sshInfo),
+                  sshInfo,
+                  change),
               new ConfigValidator(branch, user, rw, allUsers, allProjects),
               new PluginCommitValidationListener(pluginValidators),
               new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
-              new AccountCommitValidator(allUsers, accountValidator),
+              new AccountCommitValidator(repoManager, allUsers, accountValidator),
               new GroupCommitValidator(allUsers)));
     }
 
-    public CommitValidators forMergedCommits(PermissionBackend.ForRef perm, IdentifiedUser user) {
+    public CommitValidators forMergedCommits(
+        Project.NameKey project, PermissionBackend.ForRef perm, IdentifiedUser user)
+        throws IOException {
       // Generally only include validators that are based on permissions of the
       // user creating a change for a merged commit; generally exclude
       // validators that would require amending the change in order to correct.
@@ -185,6 +206,7 @@
       return new CommitValidators(
           ImmutableList.of(
               new UploadMergesPermissionValidator(perm),
+              new ProjectStateValidationListener(projectCache.checkedGet(project)),
               new AuthorUploaderValidator(user, perm, canonicalWebUrl),
               new CommitterUploaderValidator(user, perm, canonicalWebUrl)));
     }
@@ -225,6 +247,15 @@
         "[%s] invalid "
             + FooterConstants.CHANGE_ID.getName()
             + " line format in commit message footer";
+
+    @VisibleForTesting
+    public static final String CHANGE_ID_MISMATCH_MSG =
+        "[%s] "
+            + FooterConstants.CHANGE_ID.getName()
+            + " in commit message footer does not match"
+            + FooterConstants.CHANGE_ID.getName()
+            + " of target change";
+
     private static final Pattern CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN);
 
     private final ProjectState projectState;
@@ -232,18 +263,21 @@
     private final String installCommitMsgHookCommand;
     private final SshInfo sshInfo;
     private final IdentifiedUser user;
+    private final Change change;
 
     public ChangeIdValidator(
         ProjectState projectState,
         IdentifiedUser user,
         String canonicalWebUrl,
         String installCommitMsgHookCommand,
-        SshInfo sshInfo) {
+        SshInfo sshInfo,
+        Change change) {
       this.projectState = projectState;
       this.canonicalWebUrl = canonicalWebUrl;
       this.installCommitMsgHookCommand = installCommitMsgHookCommand;
       this.sshInfo = sshInfo;
       this.user = user;
+      this.change = change;
     }
 
     @Override
@@ -283,7 +317,12 @@
           messages.add(getMissingChangeIdErrorMsg(errMsg, receiveEvent.commit));
           throw new CommitValidationException(errMsg, messages);
         }
+        if (change != null && !v.equals(change.getKey().get())) {
+          String errMsg = String.format(CHANGE_ID_MISMATCH_MSG, sha1);
+          throw new CommitValidationException(errMsg);
+        }
       }
+
       return Collections.emptyList();
     }
 
@@ -418,34 +457,6 @@
         }
       }
 
-      if (allUsers.equals(branch.getParentKey()) && RefNames.isRefsUsers(branch.get())) {
-        List<CommitValidationMessage> messages = new ArrayList<>();
-        Account.Id accountId = Account.Id.fromRef(branch.get());
-        if (accountId != null) {
-          try {
-            WatchConfig wc = new WatchConfig(accountId);
-            wc.load(rw, receiveEvent.command.getNewId());
-            if (!wc.getValidationErrors().isEmpty()) {
-              addError("Invalid project configuration:", messages);
-              for (ValidationError err : wc.getValidationErrors()) {
-                addError("  " + err.getMessage(), messages);
-              }
-              throw new ConfigInvalidException("invalid watch configuration");
-            }
-          } catch (IOException | ConfigInvalidException e) {
-            log.error(
-                "User "
-                    + user.getUserName()
-                    + " tried to push an invalid watch configuration "
-                    + receiveEvent.command.getNewId().name()
-                    + " for account "
-                    + accountId.get(),
-                e);
-            throw new CommitValidationException("invalid watch configuration", messages);
-          }
-        }
-      }
-
       return Collections.emptyList();
     }
   }
@@ -729,10 +740,15 @@
   }
 
   public static class AccountCommitValidator implements CommitValidationListener {
+    private final GitRepositoryManager repoManager;
     private final AllUsersName allUsers;
     private final AccountValidator accountValidator;
 
-    public AccountCommitValidator(AllUsersName allUsers, AccountValidator accountValidator) {
+    public AccountCommitValidator(
+        GitRepositoryManager repoManager,
+        AllUsersName allUsers,
+        AccountValidator accountValidator) {
+      this.repoManager = repoManager;
       this.allUsers = allUsers;
       this.accountValidator = accountValidator;
     }
@@ -755,10 +771,11 @@
         return Collections.emptyList();
       }
 
-      try {
+      try (Repository repo = repoManager.openRepository(allUsers)) {
         List<String> errorMessages =
             accountValidator.validate(
                 accountId,
+                repo,
                 receiveEvent.revWalk,
                 receiveEvent.command.getOldId(),
                 receiveEvent.commit);
@@ -808,6 +825,24 @@
     }
   }
 
+  /** Rejects updates to projects that don't allow writes. */
+  public static class ProjectStateValidationListener implements CommitValidationListener {
+    private final ProjectState projectState;
+
+    public ProjectStateValidationListener(ProjectState projectState) {
+      this.projectState = projectState;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+        throws CommitValidationException {
+      if (projectState.statePermitsWrite()) {
+        return Collections.emptyList();
+      }
+      throw new CommitValidationException("project state does not permit write");
+    }
+  }
+
   private static CommitValidationMessage invalidEmail(
       RevCommit c,
       String type,
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 22bb05f..5b20ff6 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -285,7 +285,7 @@
       }
 
       try (RevWalk rw = new RevWalk(repo)) {
-        List<String> errorMessages = accountValidator.validate(accountId, rw, null, commit);
+        List<String> errorMessages = accountValidator.validate(accountId, repo, rw, null, commit);
         if (!errorMessages.isEmpty()) {
           throw new MergeValidationException(
               "invalid account configuration: " + Joiner.on("; ").join(errorMessages));
diff --git a/java/com/google/gerrit/server/group/db/GroupNameNotes.java b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
index 1e6ebdd..87aee8e 100644
--- a/java/com/google/gerrit/server/group/db/GroupNameNotes.java
+++ b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
@@ -21,8 +21,10 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
+import com.google.common.collect.HashMultiset;
 import com.google.common.collect.ImmutableBiMap;
-import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multiset;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
@@ -32,11 +34,9 @@
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import java.io.IOException;
 import java.util.Collection;
-import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -68,8 +68,80 @@
   @VisibleForTesting
   static final String UNIQUE_REF_ERROR = "GroupReference collection must contain unique references";
 
-  public static void updateGroupNames(
-      Repository allUsersRepo,
+  public static GroupNameNotes forRename(
+      Repository repository,
+      AccountGroup.UUID groupUuid,
+      AccountGroup.NameKey oldName,
+      AccountGroup.NameKey newName)
+      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+    checkNotNull(oldName);
+    checkNotNull(newName);
+
+    GroupNameNotes groupNameNotes = new GroupNameNotes(groupUuid, oldName, newName);
+    groupNameNotes.load(repository);
+    groupNameNotes.ensureNewNameIsNotUsed();
+    return groupNameNotes;
+  }
+
+  public static GroupNameNotes forNewGroup(
+      Repository repository, AccountGroup.UUID groupUuid, AccountGroup.NameKey groupName)
+      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+    checkNotNull(groupName);
+
+    GroupNameNotes groupNameNotes = new GroupNameNotes(groupUuid, null, groupName);
+    groupNameNotes.load(repository);
+    groupNameNotes.ensureNewNameIsNotUsed();
+    return groupNameNotes;
+  }
+
+  public static Optional<GroupReference> loadGroup(
+      Repository repository, AccountGroup.NameKey groupName)
+      throws IOException, ConfigInvalidException {
+    Ref ref = repository.exactRef(RefNames.REFS_GROUPNAMES);
+    if (ref == null) {
+      return Optional.empty();
+    }
+
+    try (RevWalk revWalk = new RevWalk(repository);
+        ObjectReader reader = revWalk.getObjectReader()) {
+      RevCommit notesCommit = revWalk.parseCommit(ref.getObjectId());
+      NoteMap noteMap = NoteMap.read(reader, notesCommit);
+      ObjectId noteDataBlobId = noteMap.get(getNoteKey(groupName));
+      if (noteDataBlobId == null) {
+        return Optional.empty();
+      }
+      return Optional.of(getGroupReference(reader, noteDataBlobId));
+    }
+  }
+
+  public static ImmutableList<GroupReference> loadAllGroups(Repository repository)
+      throws IOException, ConfigInvalidException {
+    Ref ref = repository.exactRef(RefNames.REFS_GROUPNAMES);
+    if (ref == null) {
+      return ImmutableList.of();
+    }
+    try (RevWalk revWalk = new RevWalk(repository);
+        ObjectReader reader = revWalk.getObjectReader()) {
+      RevCommit notesCommit = revWalk.parseCommit(ref.getObjectId());
+      NoteMap noteMap = NoteMap.read(reader, notesCommit);
+
+      Multiset<GroupReference> groupReferences = HashMultiset.create();
+      for (Note note : noteMap) {
+        GroupReference groupReference = getGroupReference(reader, note.getData());
+        int numOfOccurrences = groupReferences.add(groupReference, 1);
+        if (numOfOccurrences > 1) {
+          GroupsNoteDbConsistencyChecker.logConsistencyProblemAsWarning(
+              "The UUID of group %s (%s) is duplicate in group name notes",
+              groupReference.getName(), groupReference.getUUID());
+        }
+      }
+
+      return ImmutableList.copyOf(groupReferences);
+    }
+  }
+
+  public static void updateAllGroups(
+      Repository repository,
       ObjectInserter inserter,
       BatchRefUpdate bru,
       Collection<GroupReference> groupReferences,
@@ -82,7 +154,7 @@
         RevWalk rw = new RevWalk(reader)) {
       // Always start from an empty map, discarding old notes.
       NoteMap noteMap = NoteMap.newEmptyMap();
-      Ref ref = allUsersRepo.exactRef(RefNames.REFS_GROUPNAMES);
+      Ref ref = repository.exactRef(RefNames.REFS_GROUPNAMES);
       RevCommit oldCommit = ref != null ? rw.parseCommit(ref.getObjectId()) : null;
 
       for (Map.Entry<AccountGroup.UUID, String> e : biMap.entrySet()) {
@@ -124,8 +196,8 @@
   }
 
   private final AccountGroup.UUID groupUuid;
-  private final Optional<AccountGroup.NameKey> oldGroupName;
-  private final Optional<AccountGroup.NameKey> newGroupName;
+  private Optional<AccountGroup.NameKey> oldGroupName;
+  private Optional<AccountGroup.NameKey> newGroupName;
 
   private boolean nameConflicting;
 
@@ -144,77 +216,6 @@
     }
   }
 
-  public static GroupNameNotes loadForRename(
-      Repository repository,
-      AccountGroup.UUID groupUuid,
-      AccountGroup.NameKey oldName,
-      AccountGroup.NameKey newName)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
-    checkNotNull(oldName);
-    checkNotNull(newName);
-
-    GroupNameNotes groupNameNotes = new GroupNameNotes(groupUuid, oldName, newName);
-    groupNameNotes.load(repository);
-    groupNameNotes.ensureNewNameIsNotUsed();
-    return groupNameNotes;
-  }
-
-  public static GroupNameNotes loadForNewGroup(
-      Repository repository, AccountGroup.UUID groupUuid, AccountGroup.NameKey groupName)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
-    checkNotNull(groupName);
-
-    GroupNameNotes groupNameNotes = new GroupNameNotes(groupUuid, null, groupName);
-    groupNameNotes.load(repository);
-    groupNameNotes.ensureNewNameIsNotUsed();
-    return groupNameNotes;
-  }
-
-  public static ImmutableSet<GroupReference> loadAllGroupReferences(Repository repository)
-      throws IOException, ConfigInvalidException {
-    Ref ref = repository.exactRef(RefNames.REFS_GROUPNAMES);
-    if (ref == null) {
-      return ImmutableSet.of();
-    }
-    try (RevWalk revWalk = new RevWalk(repository);
-        ObjectReader reader = revWalk.getObjectReader()) {
-      RevCommit notesCommit = revWalk.parseCommit(ref.getObjectId());
-      NoteMap noteMap = NoteMap.read(reader, notesCommit);
-
-      Set<GroupReference> groupReferences = new LinkedHashSet<>();
-      for (Note note : noteMap) {
-        GroupReference groupReference = getGroupReference(reader, note.getData());
-        boolean result = groupReferences.add(groupReference);
-        if (!result) {
-          GroupsNoteDbConsistencyChecker.logConsistencyProblemAsWarning(
-              "The UUID of group %s (%s) is duplicate in group name notes",
-              groupReference.getName(), groupReference.getUUID());
-        }
-      }
-
-      return ImmutableSet.copyOf(groupReferences);
-    }
-  }
-
-  public static Optional<GroupReference> loadOneGroupReference(
-      Repository allUsersRepo, String groupName) throws IOException, ConfigInvalidException {
-    Ref ref = allUsersRepo.exactRef(RefNames.REFS_GROUPNAMES);
-    if (ref == null) {
-      return Optional.empty();
-    }
-
-    try (RevWalk revWalk = new RevWalk(allUsersRepo);
-        ObjectReader reader = revWalk.getObjectReader()) {
-      RevCommit notesCommit = revWalk.parseCommit(ref.getObjectId());
-      NoteMap noteMap = NoteMap.read(reader, notesCommit);
-      ObjectId noteDataBlobId = noteMap.get(getNoteKey(new AccountGroup.NameKey(groupName)));
-      if (noteDataBlobId == null) {
-        return Optional.empty();
-      }
-      return Optional.of(getGroupReference(reader, noteDataBlobId));
-    }
-  }
-
   @Override
   protected String getRefName() {
     return RefNames.REFS_GROUPNAMES;
@@ -278,6 +279,9 @@
     commit.setTreeId(noteMap.writeTree(inserter));
     commit.setMessage(getCommitMessage());
 
+    oldGroupName = Optional.empty();
+    newGroupName = Optional.empty();
+
     return true;
   }
 
@@ -311,8 +315,7 @@
     return config.toText();
   }
 
-  @VisibleForTesting
-  public static GroupReference getGroupReference(ObjectReader reader, ObjectId noteDataBlobId)
+  private static GroupReference getGroupReference(ObjectReader reader, ObjectId noteDataBlobId)
       throws IOException, ConfigInvalidException {
     byte[] noteData = reader.open(noteDataBlobId, OBJ_BLOB).getCachedBytes();
     return getFromNoteData(noteData);
diff --git a/java/com/google/gerrit/server/group/db/Groups.java b/java/com/google/gerrit/server/group/db/Groups.java
index f3232be..d0ea9ca 100644
--- a/java/com/google/gerrit/server/group/db/Groups.java
+++ b/java/com/google/gerrit/server/group/db/Groups.java
@@ -192,7 +192,7 @@
       throws OrmException, IOException, ConfigInvalidException {
     if (groupsMigration.readFromNoteDb()) {
       try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
-        return GroupNameNotes.loadAllGroupReferences(allUsersRepo).stream();
+        return GroupNameNotes.loadAllGroups(allUsersRepo).stream();
       }
     }
 
@@ -298,10 +298,9 @@
         .filter(groupUuid -> !AccountGroup.isInternalGroup(groupUuid));
   }
 
-  private Stream<AccountGroup.UUID> getExternalGroupsFromNoteDb(Repository allUsersRepo)
+  private static Stream<AccountGroup.UUID> getExternalGroupsFromNoteDb(Repository allUsersRepo)
       throws IOException, ConfigInvalidException {
-    ImmutableSet<GroupReference> allInternalGroups =
-        GroupNameNotes.loadAllGroupReferences(allUsersRepo);
+    ImmutableList<GroupReference> allInternalGroups = GroupNameNotes.loadAllGroups(allUsersRepo);
     ImmutableSet.Builder<AccountGroup.UUID> allSubgroups = ImmutableSet.builder();
     for (GroupReference internalGroup : allInternalGroups) {
       Optional<InternalGroup> group = getGroupFromNoteDb(allUsersRepo, internalGroup.getUUID());
diff --git a/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java b/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java
index 541697a..a0eae4a 100644
--- a/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.AllUsersName;
@@ -113,7 +114,7 @@
     }
 
     for (Account.Id id : g.getMembers().asList()) {
-      Account account;
+      AccountState account;
       try {
         account = accounts.get(id);
       } catch (ConfigInvalidException e) {
diff --git a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
index 65ac12b..9f0cb3a 100644
--- a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
+++ b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
@@ -218,7 +218,7 @@
       Repository allUsersRepo, InternalGroup group) throws IOException {
     List<ConsistencyCheckInfo.ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, group.getName(), group.getGroupUUID());
+            allUsersRepo, group.getNameKey(), group.getGroupUUID());
     problems.forEach(GroupsNoteDbConsistencyChecker::logConsistencyProblem);
   }
 
@@ -232,10 +232,10 @@
    */
   @VisibleForTesting
   static List<ConsistencyProblemInfo> checkWithGroupNameNotes(
-      Repository allUsersRepo, String groupName, AccountGroup.UUID groupUUID) throws IOException {
+      Repository allUsersRepo, AccountGroup.NameKey groupName, AccountGroup.UUID groupUUID)
+      throws IOException {
     try {
-      Optional<GroupReference> groupRef =
-          GroupNameNotes.loadOneGroupReference(allUsersRepo, groupName);
+      Optional<GroupReference> groupRef = GroupNameNotes.loadGroup(allUsersRepo, groupName);
 
       if (!groupRef.isPresent()) {
         return ImmutableList.of(
@@ -243,7 +243,6 @@
       }
 
       AccountGroup.UUID uuid = groupRef.get().getUUID();
-      String name = groupRef.get().getName();
 
       List<ConsistencyProblemInfo> problems = new ArrayList<>();
       if (!Objects.equals(groupUUID, uuid)) {
@@ -253,9 +252,11 @@
                 groupName, groupUUID, uuid));
       }
 
-      if (!Objects.equals(groupName, name)) {
+      String name = groupName.get();
+      String actualName = groupRef.get().getName();
+      if (!Objects.equals(name, actualName)) {
         problems.add(
-            warning("group note of name '%s' claims to represent name of '%s'", groupName, name));
+            warning("group note of name '%s' claims to represent name of '%s'", name, actualName));
       }
       return problems;
     } catch (ConfigInvalidException e) {
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index d93c8bd..fd5c453 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -482,7 +482,7 @@
     try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
       AccountGroup.NameKey groupName = groupUpdate.getName().orElseGet(groupCreation::getNameKey);
       GroupNameNotes groupNameNotes =
-          GroupNameNotes.loadForNewGroup(allUsersRepo, groupCreation.getGroupUUID(), groupName);
+          GroupNameNotes.forNewGroup(allUsersRepo, groupCreation.getGroupUUID(), groupName);
 
       GroupConfig groupConfig = GroupConfig.createForNewGroup(allUsersRepo, groupCreation);
       groupConfig.setGroupUpdate(groupUpdate, this::getAccountNameEmail, this::getGroupName);
@@ -515,7 +515,7 @@
       if (groupUpdate.getName().isPresent()) {
         AccountGroup.NameKey oldName = originalGroup.getNameKey();
         AccountGroup.NameKey newName = groupUpdate.getName().get();
-        groupNameNotes = GroupNameNotes.loadForRename(allUsersRepo, groupUuid, oldName, newName);
+        groupNameNotes = GroupNameNotes.forRename(allUsersRepo, groupUuid, oldName, newName);
       }
 
       commit(allUsersRepo, groupConfig, groupNameNotes);
diff --git a/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java b/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java
index 46fd666..9197a01 100644
--- a/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java
+++ b/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java
@@ -15,14 +15,10 @@
 package com.google.gerrit.server.group.db.testing;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.gerrit.server.group.db.GroupNameNotes.getGroupReference;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Streams;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.CommitUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -31,29 +27,12 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.notes.Note;
-import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
 
 /** Test utilities for low-level NoteDb groups. */
 public class GroupTestUtil {
-  public static ImmutableMap<String, String> readNameToUuidMap(Repository repo) throws Exception {
-    ImmutableMap.Builder<String, String> result = ImmutableMap.builder();
-    try (RevWalk rw = new RevWalk(repo)) {
-      Ref ref = repo.exactRef(RefNames.REFS_GROUPNAMES);
-      if (ref != null) {
-        NoteMap noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(ref.getObjectId()));
-        for (Note note : noteMap) {
-          GroupReference gr = getGroupReference(rw.getObjectReader(), note.getData());
-          result.put(gr.getName(), gr.getUUID().get());
-        }
-      }
-    }
-    return result.build();
-  }
-
   // TODO(dborowitz): Move somewhere even more common.
   public static ImmutableList<CommitInfo> log(Repository repo, String refName) throws Exception {
     try (RevWalk rw = new RevWalk(repo)) {
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index ec67d9d..8eb55fa 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -121,34 +121,38 @@
     Set<Address> smtpRcptToPlaintextOnly = new HashSet<>();
     if (shouldSendMessage()) {
       if (fromId != null) {
-        final Account fromUser = args.accountCache.get(fromId).getAccount();
-        GeneralPreferencesInfo senderPrefs = fromUser.getGeneralPreferencesInfo();
-
-        if (senderPrefs != null && senderPrefs.getEmailStrategy() == CC_ON_OWN_COMMENTS) {
-          // If we are impersonating a user, make sure they receive a CC of
-          // this message so they can always review and audit what we sent
-          // on their behalf to others.
-          //
-          add(RecipientType.CC, fromId);
-        } else if (!accountsToNotify.containsValue(fromId) && rcptTo.remove(fromId)) {
-          // If they don't want a copy, but we queued one up anyway,
-          // drop them from the recipient lists.
-          //
-          removeUser(fromUser);
+        AccountState fromUser = args.accountCache.get(fromId);
+        if (fromUser != null) {
+          GeneralPreferencesInfo senderPrefs = fromUser.getGeneralPreferences();
+          if (senderPrefs != null && senderPrefs.getEmailStrategy() == CC_ON_OWN_COMMENTS) {
+            // If we are impersonating a user, make sure they receive a CC of
+            // this message so they can always review and audit what we sent
+            // on their behalf to others.
+            //
+            add(RecipientType.CC, fromId);
+          } else if (!accountsToNotify.containsValue(fromId) && rcptTo.remove(fromId)) {
+            // If they don't want a copy, but we queued one up anyway,
+            // drop them from the recipient lists.
+            //
+            removeUser(fromUser.getAccount());
+          }
         }
       }
       // Check the preferences of all recipients. If any user has disabled
       // his email notifications then drop him from recipients' list.
       // In addition, check if users only want to receive plaintext email.
       for (Account.Id id : rcptTo) {
-        Account thisUser = args.accountCache.get(id).getAccount();
-        GeneralPreferencesInfo prefs = thisUser.getGeneralPreferencesInfo();
-        if (prefs == null || prefs.getEmailStrategy() == DISABLED) {
-          removeUser(thisUser);
-        } else if (useHtml() && prefs.getEmailFormat() == EmailFormat.PLAINTEXT) {
-          removeUser(thisUser);
-          smtpRcptToPlaintextOnly.add(
-              new Address(thisUser.getFullName(), thisUser.getPreferredEmail()));
+        AccountState thisUser = args.accountCache.get(id);
+        if (thisUser != null) {
+          Account thisUserAccount = thisUser.getAccount();
+          GeneralPreferencesInfo prefs = thisUser.getGeneralPreferences();
+          if (prefs == null || prefs.getEmailStrategy() == DISABLED) {
+            removeUser(thisUserAccount);
+          } else if (useHtml() && prefs.getEmailFormat() == EmailFormat.PLAINTEXT) {
+            removeUser(thisUserAccount);
+            smtpRcptToPlaintextOnly.add(
+                new Address(thisUserAccount.getFullName(), thisUserAccount.getPreferredEmail()));
+          }
         }
         if (smtpRcptTo.isEmpty() && smtpRcptToPlaintextOnly.isEmpty()) {
           return;
diff --git a/java/com/google/gerrit/server/permissions/RefPermission.java b/java/com/google/gerrit/server/permissions/RefPermission.java
index 607162e..0d4d6ff 100644
--- a/java/com/google/gerrit/server/permissions/RefPermission.java
+++ b/java/com/google/gerrit/server/permissions/RefPermission.java
@@ -35,6 +35,9 @@
   /** Create a change to code review a commit. */
   CREATE_CHANGE,
 
+  /** Create a tag. */
+  CREATE_TAG(Permission.CREATE_TAG),
+
   /**
    * Creates changes, then also immediately submits them during {@code push}.
    *
diff --git a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
index 1f94025..3b75256 100644
--- a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
+++ b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
@@ -65,6 +65,9 @@
               BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE,
               new Mapper(
                   i -> i.matchAuthorToCommitterDate, (i, v) -> i.matchAuthorToCommitterDate = v))
+          .put(
+              BooleanProjectConfig.REJECT_EMPTY_COMMIT,
+              new Mapper(i -> i.rejectEmptyCommit, (i, v) -> i.rejectEmptyCommit = v))
           .build();
 
   static {
diff --git a/java/com/google/gerrit/server/project/ChangeControl.java b/java/com/google/gerrit/server/project/ChangeControl.java
index 63dd9a9..10c04af 100644
--- a/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/java/com/google/gerrit/server/project/ChangeControl.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -87,7 +88,7 @@
   private final ChangeNotes notes;
   private final PatchSetUtil patchSetUtil;
 
-  ChangeControl(
+  private ChangeControl(
       ChangeData.Factory changeDataFactory,
       ApprovalsUtil approvalsUtil,
       RefControl refControl,
@@ -100,53 +101,44 @@
     this.patchSetUtil = patchSetUtil;
   }
 
-  ChangeControl forUser(CurrentUser who) {
+  ForChange asForChange(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
+    return new ForChangeImpl(cd, db);
+  }
+
+  private ChangeControl forUser(CurrentUser who) {
     if (getUser().equals(who)) {
       return this;
     }
     return new ChangeControl(
-        changeDataFactory, approvalsUtil, getRefControl().forUser(who), notes, patchSetUtil);
-  }
-
-  private RefControl getRefControl() {
-    return refControl;
+        changeDataFactory, approvalsUtil, refControl.forUser(who), notes, patchSetUtil);
   }
 
   private CurrentUser getUser() {
-    return getRefControl().getUser();
+    return refControl.getUser();
   }
 
   private ProjectControl getProjectControl() {
-    return getRefControl().getProjectControl();
+    return refControl.getProjectControl();
   }
 
   private Change getChange() {
     return notes.getChange();
   }
 
-  private ChangeNotes getNotes() {
-    return notes;
-  }
-
   /** Can this user see this change? */
   private boolean isVisible(ReviewDb db, @Nullable ChangeData cd) throws OrmException {
     if (getChange().isPrivate() && !isPrivateVisible(db, cd)) {
       return false;
     }
-    return isRefVisible();
-  }
-
-  /** Can the user see this change? Does not account for draft status */
-  private boolean isRefVisible() {
-    return getRefControl().isVisible();
+    return refControl.isVisible() && getProjectControl().getProject().getState().permitsRead();
   }
 
   /** Can this user abandon this change? */
   private boolean canAbandon(ReviewDb db) throws OrmException {
     return (isOwner() // owner (aka creator) of the change can abandon
-            || getRefControl().isOwner() // branch owner can abandon
+            || refControl.isOwner() // branch owner can abandon
             || getProjectControl().isOwner() // project owner can abandon
-            || getRefControl().canAbandon() // user can abandon a specific ref
+            || refControl.canPerform(Permission.ABANDON) // user can abandon a specific ref
             || getProjectControl().isAdmin())
         && !isPatchSetLocked(db);
   }
@@ -156,7 +148,7 @@
     switch (status) {
       case NEW:
       case ABANDONED:
-        return (isOwner() && getRefControl().canDeleteOwnChanges())
+        return (isOwner() && refControl.canPerform(Permission.DELETE_OWN_CHANGES))
             || getProjectControl().isAdmin();
       case MERGED:
       default:
@@ -166,7 +158,7 @@
 
   /** Can this user rebase this change? */
   private boolean canRebase(ReviewDb db) throws OrmException {
-    return (isOwner() || getRefControl().canSubmit(isOwner()) || getRefControl().canRebase())
+    return (isOwner() || refControl.canSubmit(isOwner()) || refControl.canRebase())
         && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE)
         && !isPatchSetLocked(db);
   }
@@ -179,18 +171,18 @@
 
   /** The range of permitted values associated with a label permission. */
   private PermissionRange getRange(String permission) {
-    return getRefControl().getRange(permission, isOwner());
+    return refControl.getRange(permission, isOwner());
   }
 
   /** Can this user add a patch set to this change? */
   private boolean canAddPatchSet(ReviewDb db) throws OrmException {
-    if (!refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE) || isPatchSetLocked(db)) {
+    if (!(refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE)) || isPatchSetLocked(db)) {
       return false;
     }
     if (isOwner()) {
       return true;
     }
-    return getRefControl().canAddPatchSet();
+    return refControl.canAddPatchSet();
   }
 
   /** Is the current patch set locked against state changes? */
@@ -201,11 +193,11 @@
 
     for (PatchSetApproval ap :
         approvalsUtil.byPatchSet(
-            db, getNotes(), getUser(), getChange().currentPatchSetId(), null, null)) {
+            db, notes, getUser(), getChange().currentPatchSetId(), null, null)) {
       LabelType type =
           getProjectControl()
               .getProjectState()
-              .getLabelTypes(getNotes(), getUser())
+              .getLabelTypes(notes, getUser())
               .byLabel(ap.getLabel());
       if (type != null
           && ap.getValue() == 1
@@ -238,7 +230,8 @@
   /** Is this user a reviewer for the change? */
   private boolean isReviewer(ReviewDb db, @Nullable ChangeData cd) throws OrmException {
     if (getUser().isIdentifiedUser()) {
-      Collection<Account.Id> results = changeData(db, cd).reviewers().all();
+      cd = cd != null ? cd : changeDataFactory.create(db, notes);
+      Collection<Account.Id> results = cd.reviewers().all();
       return results.contains(getUser().getAccountId());
     }
     return false;
@@ -248,19 +241,20 @@
   private boolean canEditTopicName() {
     if (getChange().getStatus().isOpen()) {
       return isOwner() // owner (aka creator) of the change can edit topic
-          || getRefControl().isOwner() // branch owner can edit topic
+          || refControl.isOwner() // branch owner can edit topic
           || getProjectControl().isOwner() // project owner can edit topic
-          || getRefControl().canEditTopicName() // user can edit topic on a specific ref
+          || refControl.canPerform(
+              Permission.EDIT_TOPIC_NAME) // user can edit topic on a specific ref
           || getProjectControl().isAdmin();
     }
-    return getRefControl().canForceEditTopicName();
+    return refControl.canForceEditTopicName();
   }
 
   /** Can this user edit the description? */
   private boolean canEditDescription() {
     if (getChange().getStatus().isOpen()) {
       return isOwner() // owner (aka creator) of the change can edit desc
-          || getRefControl().isOwner() // branch owner can edit desc
+          || refControl.isOwner() // branch owner can edit desc
           || getProjectControl().isOwner() // project owner can edit desc
           || getProjectControl().isAdmin();
     }
@@ -270,34 +264,27 @@
   private boolean canEditAssignee() {
     return isOwner()
         || getProjectControl().isOwner()
-        || getRefControl().canEditAssignee()
+        || refControl.canPerform(Permission.EDIT_ASSIGNEE)
         || isAssignee();
   }
 
   /** Can this user edit the hashtag name? */
   private boolean canEditHashtags() {
     return isOwner() // owner (aka creator) of the change can edit hashtags
-        || getRefControl().isOwner() // branch owner can edit hashtags
+        || refControl.isOwner() // branch owner can edit hashtags
         || getProjectControl().isOwner() // project owner can edit hashtags
-        || getRefControl().canEditHashtags() // user can edit hashtag on a specific ref
+        || refControl.canPerform(
+            Permission.EDIT_HASHTAGS) // user can edit hashtag on a specific ref
         || getProjectControl().isAdmin();
   }
 
-  private ChangeData changeData(ReviewDb db, @Nullable ChangeData cd) {
-    return cd != null ? cd : changeDataFactory.create(db, getNotes());
-  }
-
   private boolean isPrivateVisible(ReviewDb db, ChangeData cd) throws OrmException {
     return isOwner()
         || isReviewer(db, cd)
-        || getRefControl().canViewPrivateChanges()
+        || refControl.canPerform(Permission.VIEW_PRIVATE_CHANGES)
         || getUser().isInternalUser();
   }
 
-  ForChange asForChange(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
-    return new ForChangeImpl(cd, db);
-  }
-
   private class ForChangeImpl extends ForChange {
     private ChangeData cd;
     private Map<String, PermissionRange> labels;
@@ -321,7 +308,7 @@
       if (cd == null) {
         ReviewDb reviewDb = db();
         checkState(reviewDb != null, "need ReviewDb");
-        cd = changeDataFactory.create(reviewDb, getNotes());
+        cd = changeDataFactory.create(reviewDb, notes);
       }
       return cd;
     }
@@ -391,11 +378,11 @@
           case RESTORE:
             return canRestore(db());
           case SUBMIT:
-            return getRefControl().canSubmit(isOwner());
+            return refControl.canSubmit(isOwner());
 
           case REMOVE_REVIEWER:
           case SUBMIT_AS:
-            return getRefControl().canPerform(perm.permissionName().get());
+            return refControl.canPerform(perm.permissionName().get());
         }
       } catch (OrmException e) {
         throw new PermissionBackendException("unavailable", e);
@@ -428,7 +415,7 @@
     }
   }
 
-  static <T extends ChangePermissionOrLabel> Set<T> newSet(Collection<T> permSet) {
+  private static <T extends ChangePermissionOrLabel> Set<T> newSet(Collection<T> permSet) {
     if (permSet instanceof EnumSet) {
       @SuppressWarnings({"unchecked", "rawtypes"})
       Set<T> s = ((EnumSet) permSet).clone();
diff --git a/java/com/google/gerrit/server/project/CreateProjectArgs.java b/java/com/google/gerrit/server/project/CreateProjectArgs.java
index b98ffc2..e4623b2 100644
--- a/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -34,6 +34,7 @@
   public InheritableBoolean contentMerge;
   public InheritableBoolean newChangeForAllNotInTarget;
   public InheritableBoolean changeIdRequired;
+  public InheritableBoolean rejectEmptyCommit;
   public boolean createEmptyCommit;
   public String maxObjectSizeLimit;
 
diff --git a/java/com/google/gerrit/server/project/CreateRefControl.java b/java/com/google/gerrit/server/project/CreateRefControl.java
index de6fba2..d45bed9 100644
--- a/java/com/google/gerrit/server/project/CreateRefControl.java
+++ b/java/com/google/gerrit/server/project/CreateRefControl.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -41,11 +42,14 @@
 
   private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
+  private final Reachable reachable;
 
   @Inject
-  CreateRefControl(PermissionBackend permissionBackend, ProjectCache projectCache) {
+  CreateRefControl(
+      PermissionBackend permissionBackend, ProjectCache projectCache, Reachable reachable) {
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
+    this.reachable = reachable;
   }
 
   /**
@@ -57,25 +61,25 @@
    * @param object the object the user will start the reference with
    * @throws AuthException if creation is denied; the message explains the denial.
    * @throws PermissionBackendException on failure of permission checks.
+   * @throws ResourceConflictException if the project state does not permit the operation
    */
   public void checkCreateRef(
       Provider<? extends CurrentUser> user,
       Repository repo,
       Branch.NameKey branch,
       RevObject object)
-      throws AuthException, PermissionBackendException, NoSuchProjectException, IOException {
+      throws AuthException, PermissionBackendException, NoSuchProjectException, IOException,
+          ResourceConflictException {
     ProjectState ps = projectCache.checkedGet(branch.getParentKey());
     if (ps == null) {
       throw new NoSuchProjectException(branch.getParentKey());
     }
-    if (!ps.getProject().getState().permitsWrite()) {
-      throw new AuthException("project state does not permit write");
-    }
+    ps.checkStatePermitsWrite();
 
     PermissionBackend.ForRef perm = permissionBackend.user(user).ref(branch);
     if (object instanceof RevCommit) {
       perm.check(RefPermission.CREATE);
-      checkCreateCommit(user, repo, (RevCommit) object, ps, perm);
+      checkCreateCommit(repo, (RevCommit) object, ps, perm);
     } else if (object instanceof RevTag) {
       RevTag tag = (RevTag) object;
       try (RevWalk rw = new RevWalk(repo)) {
@@ -95,7 +99,7 @@
 
       RevObject target = tag.getObject();
       if (target instanceof RevCommit) {
-        checkCreateCommit(user, repo, (RevCommit) target, ps, perm);
+        checkCreateCommit(repo, (RevCommit) target, ps, perm);
       } else {
         checkCreateRef(user, repo, branch, target);
       }
@@ -118,11 +122,7 @@
    * new commit to the repository.
    */
   private void checkCreateCommit(
-      Provider<? extends CurrentUser> user,
-      Repository repo,
-      RevCommit commit,
-      ProjectState projectState,
-      PermissionBackend.ForRef forRef)
+      Repository repo, RevCommit commit, ProjectState projectState, PermissionBackend.ForRef forRef)
       throws AuthException, PermissionBackendException {
     try {
       // If the user has update (push) permission, they can create the ref regardless
@@ -132,8 +132,7 @@
     } catch (AuthException denied) {
       // Fall through to check reachability.
     }
-
-    if (projectState.controlFor(user.get()).isReachableFromHeadsOrTags(repo, commit)) {
+    if (reachable.fromHeadsOrTags(projectState, repo, commit)) {
       // If the user has no push permissions, check whether the object is
       // merged into a branch or tag readable by this user. If so, they are
       // not effectively "pushing" more objects, so they can create the ref
diff --git a/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java b/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java
index bdfc67f..44c5e0b 100644
--- a/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java
+++ b/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java
@@ -30,8 +30,7 @@
     @Override
     protected void configure() {
       // TODO(sop) Hide ProjectControl, RefControl, ChangeControl related bindings.
-      bind(ProjectControl.GenericFactory.class);
-      factory(ProjectControl.AssistedFactory.class);
+      factory(ProjectControl.Factory.class);
       bind(ChangeControl.Factory.class);
     }
   }
diff --git a/java/com/google/gerrit/server/project/ProjectCache.java b/java/com/google/gerrit/server/project/ProjectCache.java
index 63052bd..9ebcc99 100644
--- a/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/java/com/google/gerrit/server/project/ProjectCache.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import java.io.IOException;
@@ -67,8 +68,14 @@
    */
   void remove(Project p) throws IOException;
 
+  /**
+   * Remove information about the given project from the cache. It will no longer be returned from
+   * {@link #all()}.
+   */
+  void remove(Project.NameKey name) throws IOException;
+
   /** @return sorted iteration of projects. */
-  Iterable<Project.NameKey> all();
+  ImmutableSortedSet<Project.NameKey> all();
 
   /**
    * @return estimated set of relevant groups extracted from hot project access rules. If the cache
@@ -82,7 +89,7 @@
    * @param prefix common prefix.
    * @return sorted iteration of projects sharing the same prefix.
    */
-  Iterable<Project.NameKey> byName(String prefix);
+  ImmutableSortedSet<Project.NameKey> byName(String prefix);
 
   /** Notify the cache that a new project was constructed. */
   void onCreateProject(Project.NameKey newProjectName) throws IOException;
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 4b2161b..68270e2 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -19,6 +19,8 @@
 import com.google.common.base.Throwables;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -36,12 +38,8 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 import java.io.IOException;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Set;
-import java.util.SortedSet;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
@@ -64,7 +62,7 @@
       protected void configure() {
         cache(CACHE_NAME, String.class, ProjectState.class).loader(Loader.class);
 
-        cache(CACHE_LIST, ListKey.class, new TypeLiteral<SortedSet<Project.NameKey>>() {})
+        cache(CACHE_LIST, ListKey.class, new TypeLiteral<ImmutableSortedSet<Project.NameKey>>() {})
             .maximumWeight(1)
             .loader(Lister.class);
 
@@ -86,7 +84,7 @@
   private final AllProjectsName allProjectsName;
   private final AllUsersName allUsersName;
   private final LoadingCache<String, ProjectState> byName;
-  private final LoadingCache<ListKey, SortedSet<Project.NameKey>> list;
+  private final LoadingCache<ListKey, ImmutableSortedSet<Project.NameKey>> list;
   private final Lock listLock;
   private final ProjectCacheClock clock;
   private final Provider<ProjectIndexer> indexer;
@@ -96,7 +94,7 @@
       final AllProjectsName allProjectsName,
       final AllUsersName allUsersName,
       @Named(CACHE_NAME) LoadingCache<String, ProjectState> byName,
-      @Named(CACHE_LIST) LoadingCache<ListKey, SortedSet<Project.NameKey>> list,
+      @Named(CACHE_LIST) LoadingCache<ListKey, ImmutableSortedSet<Project.NameKey>> list,
       ProjectCacheClock clock,
       Provider<ProjectIndexer> indexer) {
     this.allProjectsName = allProjectsName;
@@ -176,26 +174,32 @@
 
   @Override
   public void remove(Project p) throws IOException {
+    remove(p.getNameKey());
+  }
+
+  @Override
+  public void remove(Project.NameKey name) throws IOException {
     listLock.lock();
     try {
-      SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL));
-      n.remove(p.getNameKey());
-      list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+      list.put(
+          ListKey.ALL,
+          ImmutableSortedSet.copyOf(Sets.difference(list.get(ListKey.ALL), ImmutableSet.of(name))));
     } catch (ExecutionException e) {
       log.warn("Cannot list available projects", e);
     } finally {
       listLock.unlock();
     }
-    evict(p);
+    evict(name);
   }
 
   @Override
   public void onCreateProject(Project.NameKey newProjectName) throws IOException {
     listLock.lock();
     try {
-      SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL));
-      n.add(newProjectName);
-      list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+      list.put(
+          ListKey.ALL,
+          ImmutableSortedSet.copyOf(
+              Sets.union(list.get(ListKey.ALL), ImmutableSet.of(newProjectName))));
     } catch (ExecutionException e) {
       log.warn("Cannot list available projects", e);
     } finally {
@@ -205,12 +209,12 @@
   }
 
   @Override
-  public SortedSet<Project.NameKey> all() {
+  public ImmutableSortedSet<Project.NameKey> all() {
     try {
       return list.get(ListKey.ALL);
     } catch (ExecutionException e) {
       log.warn("Cannot list available projects", e);
-      return Collections.emptySortedSet();
+      return ImmutableSortedSet.of();
     }
   }
 
@@ -228,58 +232,16 @@
   }
 
   @Override
-  public Iterable<Project.NameKey> byName(String pfx) {
-    final Iterable<Project.NameKey> src;
+  public ImmutableSortedSet<Project.NameKey> byName(String pfx) {
+    Project.NameKey start = new Project.NameKey(pfx);
+    Project.NameKey end = new Project.NameKey(pfx + Character.MAX_VALUE);
     try {
-      src = list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx));
+      // Right endpoint is exclusive, but U+FFFF is a non-character so no project ends with it.
+      return list.get(ListKey.ALL).subSet(start, end);
     } catch (ExecutionException e) {
       log.warn("Cannot look up projects for prefix " + pfx, e);
-      return Collections.emptyList();
+      return ImmutableSortedSet.of();
     }
-    return new Iterable<Project.NameKey>() {
-      @Override
-      public Iterator<Project.NameKey> iterator() {
-        return new Iterator<Project.NameKey>() {
-          private Iterator<Project.NameKey> itr = src.iterator();
-          private Project.NameKey next;
-
-          @Override
-          public boolean hasNext() {
-            if (next != null) {
-              return true;
-            }
-
-            if (!itr.hasNext()) {
-              return false;
-            }
-
-            Project.NameKey r = itr.next();
-            if (r.get().startsWith(pfx)) {
-              next = r;
-              return true;
-            }
-            itr = Collections.<Project.NameKey>emptyList().iterator();
-            return false;
-          }
-
-          @Override
-          public Project.NameKey next() {
-            if (!hasNext()) {
-              throw new NoSuchElementException();
-            }
-
-            Project.NameKey r = next;
-            next = null;
-            return r;
-          }
-
-          @Override
-          public void remove() {
-            throw new UnsupportedOperationException();
-          }
-        };
-      }
-    };
   }
 
   static class Loader extends CacheLoader<String, ProjectState> {
@@ -315,7 +277,7 @@
     private ListKey() {}
   }
 
-  static class Lister extends CacheLoader<ListKey, SortedSet<Project.NameKey>> {
+  static class Lister extends CacheLoader<ListKey, ImmutableSortedSet<Project.NameKey>> {
     private final GitRepositoryManager mgr;
 
     @Inject
@@ -324,8 +286,8 @@
     }
 
     @Override
-    public SortedSet<Project.NameKey> load(ListKey key) throws Exception {
-      return mgr.list();
+    public ImmutableSortedSet<Project.NameKey> load(ListKey key) throws Exception {
+      return ImmutableSortedSet.copyOf(mgr.list());
     }
   }
 }
diff --git a/java/com/google/gerrit/server/project/ProjectControl.java b/java/com/google/gerrit/server/project/ProjectControl.java
index 3e6d10e..68dbf86 100644
--- a/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/java/com/google/gerrit/server/project/ProjectControl.java
@@ -49,7 +49,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
@@ -58,30 +57,10 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
 
 /** Access control management for a user accessing a project's data. */
-public class ProjectControl {
-  public static class GenericFactory {
-    private final ProjectCache projectCache;
-
-    @Inject
-    GenericFactory(ProjectCache pc) {
-      projectCache = pc;
-    }
-
-    public ProjectControl controlFor(Project.NameKey nameKey, CurrentUser user)
-        throws NoSuchProjectException, IOException {
-      final ProjectState p = projectCache.checkedGet(nameKey);
-      if (p == null) {
-        throw new NoSuchProjectException(nameKey);
-      }
-      return p.controlFor(user);
-    }
-  }
-
-  interface AssistedFactory {
+class ProjectControl {
+  interface Factory {
     ProjectControl create(CurrentUser who, ProjectState ps);
   }
 
@@ -103,7 +82,6 @@
   private final PermissionBackend.WithUser perm;
   private final CurrentUser user;
   private final ProjectState state;
-  private final Reachable reachable;
   private final ChangeControl.Factory changeControlFactory;
   private final PermissionCollection.Factory permissionFilter;
 
@@ -116,7 +94,6 @@
       @GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups,
       @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
       PermissionCollection.Factory permissionFilter,
-      Reachable reachable,
       ChangeControl.Factory changeControlFactory,
       PermissionBackend permissionBackend,
       @Assisted CurrentUser who,
@@ -125,7 +102,6 @@
     this.uploadGroups = uploadGroups;
     this.receiveGroups = receiveGroups;
     this.permissionFilter = permissionFilter;
-    this.reachable = reachable;
     this.perm = permissionBackend.user(who);
     user = who;
     state = ps;
@@ -138,6 +114,10 @@
     return r;
   }
 
+  ForProject asForProject() {
+    return new ForProjectImpl();
+  }
+
   ChangeControl controlFor(ReviewDb db, Change change) throws OrmException {
     return changeControlFactory.create(
         controlForRef(change.getDest()), db, change.getProject(), change.getId());
@@ -164,10 +144,6 @@
     return ctl;
   }
 
-  boolean isReachableFromHeadsOrTags(Repository repo, RevCommit commit) {
-    return reachable.fromHeadsOrTags(state, repo, commit);
-  }
-
   CurrentUser getUser() {
     return user;
   }
@@ -195,6 +171,19 @@
         || isOwner();
   }
 
+  boolean isAdmin() {
+    try {
+      perm.check(GlobalPermission.ADMINISTRATE_SERVER);
+      return true;
+    } catch (AuthException | PermissionBackendException e) {
+      return false;
+    }
+  }
+
+  boolean match(PermissionRule rule, boolean isChangeOwner) {
+    return match(rule.getGroup().getUUID(), isChangeOwner);
+  }
+
   /** Can the user run upload pack? */
   private boolean canRunUploadPack() {
     for (AccountGroup.UUID group : uploadGroups) {
@@ -241,15 +230,6 @@
     return false;
   }
 
-  boolean isAdmin() {
-    try {
-      perm.check(GlobalPermission.ADMINISTRATE_SERVER);
-      return true;
-    } catch (AuthException | PermissionBackendException e) {
-      return false;
-    }
-  }
-
   private boolean isDeclaredOwner() {
     if (declaredOwner == null) {
       GroupMembership effectiveGroups = user.getEffectiveGroups();
@@ -325,19 +305,15 @@
     return allSections;
   }
 
-  boolean match(PermissionRule rule) {
+  private boolean match(PermissionRule rule) {
     return match(rule.getGroup().getUUID());
   }
 
-  boolean match(PermissionRule rule, boolean isChangeOwner) {
-    return match(rule.getGroup().getUUID(), isChangeOwner);
-  }
-
-  boolean match(AccountGroup.UUID uuid) {
+  private boolean match(AccountGroup.UUID uuid) {
     return match(uuid, false);
   }
 
-  boolean match(AccountGroup.UUID uuid, boolean isChangeOwner) {
+  private boolean match(AccountGroup.UUID uuid, boolean isChangeOwner) {
     if (SystemGroupBackend.PROJECT_OWNERS.equals(uuid)) {
       return isDeclaredOwner();
     } else if (SystemGroupBackend.CHANGE_OWNER.equals(uuid)) {
@@ -347,14 +323,6 @@
     }
   }
 
-  boolean canRead() {
-    return !isHidden() && allRefsAreVisible(Collections.emptySet());
-  }
-
-  ForProject asForProject() {
-    return new ForProjectImpl();
-  }
-
   private class ForProjectImpl extends ForProject {
     @Override
     public ForProject user(CurrentUser user) {
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index a189d92..6a5ecd8 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -31,6 +31,8 @@
 import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.extensions.api.projects.ThemeInfo;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -85,7 +87,7 @@
   private final SitePaths sitePaths;
   private final AllProjectsName allProjectsName;
   private final ProjectCache projectCache;
-  private final ProjectControl.AssistedFactory projectControlFactory;
+  private final ProjectControl.Factory projectControlFactory;
   private final PrologEnvironment.Factory envFactory;
   private final GitRepositoryManager gitMgr;
   private final RulesCache rulesCache;
@@ -119,7 +121,7 @@
       final ProjectCache projectCache,
       final AllProjectsName allProjectsName,
       final AllUsersName allUsersName,
-      final ProjectControl.AssistedFactory projectControlFactory,
+      final ProjectControl.Factory projectControlFactory,
       final PrologEnvironment.Factory envFactory,
       final GitRepositoryManager gitMgr,
       final RulesCache rulesCache,
@@ -257,6 +259,21 @@
     return config.getMaxObjectSizeLimit();
   }
 
+  public boolean statePermitsRead() {
+    return getProject().getState().permitsRead();
+  }
+
+  public boolean statePermitsWrite() {
+    return getProject().getState().permitsWrite();
+  }
+
+  public void checkStatePermitsWrite() throws ResourceConflictException {
+    if (!statePermitsWrite()) {
+      throw new ResourceConflictException(
+          "project state " + getProject().getState().name() + " does not permit write");
+    }
+  }
+
   /** Get the sections that pertain only to this project. */
   List<SectionMatcher> getLocalAccessSections() {
     List<SectionMatcher> sm = localAccessSections;
@@ -490,6 +507,16 @@
     return getGroups(getLocalAccessSections());
   }
 
+  public SubmitType getSubmitType() {
+    for (ProjectState s : tree()) {
+      SubmitType t = s.getProject().getConfiguredSubmitType();
+      if (t != SubmitType.INHERIT) {
+        return t;
+      }
+    }
+    return Project.DEFAULT_ALL_PROJECTS_SUBMIT_TYPE;
+  }
+
   private static Set<GroupReference> getGroups(List<SectionMatcher> sectionMatcherList) {
     final Set<GroupReference> all = new HashSet<>();
     for (SectionMatcher matcher : sectionMatcherList) {
diff --git a/java/com/google/gerrit/server/project/RefControl.java b/java/com/google/gerrit/server/project/RefControl.java
index 0651b1d..f0a9b64 100644
--- a/java/com/google/gerrit/server/project/RefControl.java
+++ b/java/com/google/gerrit/server/project/RefControl.java
@@ -32,6 +32,7 @@
 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.gerrit.server.util.MagicBranch;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.util.Providers;
 import java.util.ArrayList;
@@ -45,7 +46,7 @@
 import java.util.Set;
 
 /** Manages access control for Git references (aka branches, tags). */
-public class RefControl {
+class RefControl {
   private final ProjectControl projectControl;
   private final String refName;
 
@@ -67,10 +68,6 @@
     this.effective = new HashMap<>();
   }
 
-  String getRefName() {
-    return refName;
-  }
-
   ProjectControl getProjectControl() {
     return projectControl;
   }
@@ -82,9 +79,9 @@
   RefControl forUser(CurrentUser who) {
     ProjectControl newCtl = projectControl.forUser(who);
     if (relevant.isUserSpecific()) {
-      return newCtl.controlForRef(getRefName());
+      return newCtl.controlForRef(refName);
     }
-    return new RefControl(newCtl, getRefName(), relevant);
+    return new RefControl(newCtl, refName, relevant);
   }
 
   /** Is this user a ref owner? */
@@ -103,37 +100,21 @@
   /** Can this user see this reference exists? */
   boolean isVisible() {
     if (isVisible == null) {
-      isVisible =
-          (getUser().isInternalUser() || canPerform(Permission.READ))
-              && isProjectStatePermittingRead();
+      isVisible = getUser().isInternalUser() || canPerform(Permission.READ);
     }
     return isVisible;
   }
 
-  private boolean canUpload() {
-    return projectControl.controlForRef("refs/for/" + getRefName()).canPerform(Permission.PUSH)
-        && isProjectStatePermittingWrite();
-  }
-
   /** @return true if this user can add a new patch set to this ref */
   boolean canAddPatchSet() {
     return projectControl
-            .controlForRef("refs/for/" + getRefName())
-            .canPerform(Permission.ADD_PATCH_SET)
-        && isProjectStatePermittingWrite();
-  }
-
-  /** @return true if this user can submit merge patch sets to this ref */
-  private boolean canUploadMerges() {
-    return projectControl
-            .controlForRef("refs/for/" + getRefName())
-            .canPerform(Permission.PUSH_MERGE)
-        && isProjectStatePermittingWrite();
+        .controlForRef(MagicBranch.NEW_CHANGE + refName)
+        .canPerform(Permission.ADD_PATCH_SET);
   }
 
   /** @return true if this user can rebase changes on this ref */
   boolean canRebase() {
-    return canPerform(Permission.REBASE) && isProjectStatePermittingWrite();
+    return canPerform(Permission.REBASE);
   }
 
   /** @return true if this user can submit patch sets to this ref */
@@ -146,7 +127,52 @@
       // granting of powers beyond submitting to the configuration.
       return projectControl.isOwner();
     }
-    return canPerform(Permission.SUBMIT, isChangeOwner) && isProjectStatePermittingWrite();
+    return canPerform(Permission.SUBMIT, isChangeOwner);
+  }
+
+  /** @return true if this user can force edit topic names. */
+  boolean canForceEditTopicName() {
+    return canForcePerform(Permission.EDIT_TOPIC_NAME);
+  }
+
+  /** The range of permitted values associated with a label permission. */
+  PermissionRange getRange(String permission) {
+    return getRange(permission, false);
+  }
+
+  /** The range of permitted values associated with a label permission. */
+  PermissionRange getRange(String permission, boolean isChangeOwner) {
+    if (Permission.hasRange(permission)) {
+      return toRange(permission, access(permission, isChangeOwner));
+    }
+    return null;
+  }
+
+  /** True if the user is blocked from using this permission. */
+  boolean isBlocked(String permissionName) {
+    return !doCanPerform(permissionName, false, true);
+  }
+
+  /** True if the user has this permission. Works only for non labels. */
+  boolean canPerform(String permissionName) {
+    return canPerform(permissionName, false);
+  }
+
+  ForRef asForRef() {
+    return new ForRefImpl();
+  }
+
+  private boolean canPerform(String permissionName, boolean isChangeOwner) {
+    return doCanPerform(permissionName, isChangeOwner, false);
+  }
+
+  private boolean canUpload() {
+    return projectControl.controlForRef("refs/for/" + refName).canPerform(Permission.PUSH);
+  }
+
+  /** @return true if this user can submit merge patch sets to this ref */
+  private boolean canUploadMerges() {
+    return projectControl.controlForRef("refs/for/" + refName).canPerform(Permission.PUSH_MERGE);
   }
 
   /** @return true if the user can update the reference as a fast-forward. */
@@ -165,15 +191,11 @@
         return false;
       }
     }
-    return canPerform(Permission.PUSH) && isProjectStatePermittingWrite();
+    return canPerform(Permission.PUSH);
   }
 
   /** @return true if the user can rewind (force push) the reference. */
   private boolean canForceUpdate() {
-    if (!isProjectStatePermittingWrite()) {
-      return false;
-    }
-
     if (canPushWithForce()) {
       return true;
     }
@@ -192,17 +214,8 @@
     }
   }
 
-  private boolean isProjectStatePermittingWrite() {
-    return getProjectControl().getProject().getState().permitsWrite();
-  }
-
-  private boolean isProjectStatePermittingRead() {
-    return getProjectControl().getProject().getState().permitsRead();
-  }
-
   private boolean canPushWithForce() {
-    if (!isProjectStatePermittingWrite()
-        || (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner())) {
+    if (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner()) {
       // Pushing requires being at least project owner, in addition to push.
       // Pushing configuration changes modifies the access control
       // rules. Allowing this to be done by a non-project-owner opens
@@ -219,7 +232,7 @@
    * @return {@code true} if the user specified can delete a Git ref.
    */
   private boolean canDelete() {
-    if (!isProjectStatePermittingWrite() || (RefNames.REFS_CONFIG.equals(refName))) {
+    if (RefNames.REFS_CONFIG.equals(refName)) {
       // Never allow removal of the refs/meta/config branch.
       // Deleting the branch would destroy all Gerrit specific
       // metadata about the project, including its access rules.
@@ -266,53 +279,6 @@
     return canPerform(Permission.FORGE_SERVER);
   }
 
-  /** @return true if this user can abandon a change for this ref */
-  boolean canAbandon() {
-    return canPerform(Permission.ABANDON);
-  }
-
-  /** @return true if this user can view private changes. */
-  boolean canViewPrivateChanges() {
-    return canPerform(Permission.VIEW_PRIVATE_CHANGES);
-  }
-
-  /** @return true if this user can delete their own changes. */
-  boolean canDeleteOwnChanges() {
-    return canPerform(Permission.DELETE_OWN_CHANGES);
-  }
-
-  /** @return true if this user can edit topic names. */
-  boolean canEditTopicName() {
-    return canPerform(Permission.EDIT_TOPIC_NAME);
-  }
-
-  /** @return true if this user can edit hashtag names. */
-  boolean canEditHashtags() {
-    return canPerform(Permission.EDIT_HASHTAGS);
-  }
-
-  boolean canEditAssignee() {
-    return canPerform(Permission.EDIT_ASSIGNEE);
-  }
-
-  /** @return true if this user can force edit topic names. */
-  boolean canForceEditTopicName() {
-    return canForcePerform(Permission.EDIT_TOPIC_NAME);
-  }
-
-  /** The range of permitted values associated with a label permission. */
-  PermissionRange getRange(String permission) {
-    return getRange(permission, false);
-  }
-
-  /** The range of permitted values associated with a label permission. */
-  PermissionRange getRange(String permission, boolean isChangeOwner) {
-    if (Permission.hasRange(permission)) {
-      return toRange(permission, access(permission, isChangeOwner));
-    }
-    return null;
-  }
-
   private static class AllowedRange {
     private int allowMin;
     private int allowMax;
@@ -376,20 +342,6 @@
     return new PermissionRange(permissionName, min, max);
   }
 
-  /** True if the user has this permission. Works only for non labels. */
-  public boolean canPerform(String permissionName) {
-    return canPerform(permissionName, false);
-  }
-
-  boolean canPerform(String permissionName, boolean isChangeOwner) {
-    return doCanPerform(permissionName, isChangeOwner, false);
-  }
-
-  /** True if the user is blocked from using this permission. */
-  boolean isBlocked(String permissionName) {
-    return !doCanPerform(permissionName, false, true);
-  }
-
   private boolean doCanPerform(String permissionName, boolean isChangeOwner, boolean blockOnly) {
     List<PermissionRule> access = access(permissionName, isChangeOwner);
     List<PermissionRule> overridden = relevant.getOverridden(permissionName);
@@ -481,10 +433,6 @@
     return mine;
   }
 
-  ForRef asForRef() {
-    return new ForRefImpl();
-  }
-
   private class ForRefImpl extends ForRef {
     @Override
     public ForRef user(CurrentUser user) {
@@ -523,7 +471,7 @@
     @Override
     public void check(RefPermission perm) throws AuthException, PermissionBackendException {
       if (!can(perm)) {
-        throw new AuthException(perm.describeForException() + " not permitted for " + getRefName());
+        throw new AuthException(perm.describeForException() + " not permitted for " + refName);
       }
     }
 
@@ -542,7 +490,7 @@
     private boolean can(RefPermission perm) throws PermissionBackendException {
       switch (perm) {
         case READ:
-          return isVisible();
+          return isVisible() && getProjectControl().getProjectState().statePermitsRead();
         case CREATE:
           // TODO This isn't an accurate test.
           return canPerform(perm.permissionName().get());
@@ -567,11 +515,14 @@
         case CREATE_CHANGE:
           return canUpload();
 
+        case CREATE_TAG:
+          return canPerform(Permission.CREATE_TAG);
+
         case UPDATE_BY_SUBMIT:
-          return projectControl.controlForRef("refs/for/" + getRefName()).canSubmit(true);
+          return projectControl.controlForRef(MagicBranch.NEW_CHANGE + refName).canSubmit(true);
 
         case READ_PRIVATE_CHANGES:
-          return canViewPrivateChanges();
+          return canPerform(Permission.VIEW_PRIVATE_CHANGES);
 
         case READ_CONFIG:
           return projectControl
diff --git a/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index 118814d..e91d36e 100644
--- a/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -35,16 +35,16 @@
 public class RemoveReviewerControl {
   private final PermissionBackend permissionBackend;
   private final Provider<ReviewDb> dbProvider;
-  private final ProjectControl.GenericFactory projectControlFactory;
+  private final ProjectCache projectCache;
 
   @Inject
   RemoveReviewerControl(
       PermissionBackend permissionBackend,
       Provider<ReviewDb> dbProvider,
-      ProjectControl.GenericFactory projectControlFactory) {
+      ProjectCache projectCache) {
     this.permissionBackend = permissionBackend;
     this.dbProvider = dbProvider;
-    this.projectControlFactory = projectControlFactory;
+    this.projectCache = projectCache;
   }
 
   /**
@@ -116,7 +116,11 @@
     // Users with the remove reviewer permission, the branch owner, project
     // owner and site admin can remove anyone
     // TODO(hiesel): Remove all Control usage
-    ProjectControl ctl = projectControlFactory.controlFor(change.getProject(), currentUser);
+    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
diff --git a/java/com/google/gerrit/server/query/account/AccountPredicates.java b/java/com/google/gerrit/server/query/account/AccountPredicates.java
index 9213353..22df2ce 100644
--- a/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.IndexPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryBuilder;
@@ -35,14 +36,26 @@
     return Predicate.and(p, isActive());
   }
 
-  public static Predicate<AccountState> defaultPredicate(String query) {
+  public static Predicate<AccountState> defaultPredicate(
+      Schema<AccountState> schema, boolean canSeeSecondaryEmails, String query) {
     // Adapt the capacity of this list when adding more default predicates.
     List<Predicate<AccountState>> preds = Lists.newArrayListWithCapacity(3);
     Integer id = Ints.tryParse(query);
     if (id != null) {
       preds.add(id(new Account.Id(id)));
     }
-    preds.add(equalsName(query));
+    if (canSeeSecondaryEmails) {
+      preds.add(equalsNameIcludingSecondaryEmails(query));
+    } else {
+      if (schema.hasField(AccountField.NAME_PART_NO_SECONDARY_EMAIL)) {
+        preds.add(equalsName(query));
+      } else {
+        preds.add(AccountPredicates.fullName(query));
+        if (schema.hasField(AccountField.PREFERRED_EMAIL)) {
+          preds.add(AccountPredicates.preferredEmail(query));
+        }
+      }
+    }
     preds.add(username(query));
     // Adapt the capacity of the "predicates" list when adding more default
     // predicates.
@@ -54,7 +67,7 @@
         AccountField.ID, AccountQueryBuilder.FIELD_ACCOUNT, accountId.toString());
   }
 
-  public static Predicate<AccountState> email(String email) {
+  public static Predicate<AccountState> emailIncludingSecondaryEmails(String email) {
     return new AccountPredicate(
         AccountField.EMAIL, AccountQueryBuilder.FIELD_EMAIL, email.toLowerCase());
   }
@@ -71,12 +84,19 @@
         AccountField.PREFERRED_EMAIL_EXACT, AccountQueryBuilder.FIELD_PREFERRED_EMAIL_EXACT, email);
   }
 
-  public static Predicate<AccountState> equalsName(String name) {
+  public static Predicate<AccountState> equalsNameIcludingSecondaryEmails(String name) {
     return new AccountPredicate(
         AccountField.NAME_PART, AccountQueryBuilder.FIELD_NAME, name.toLowerCase());
   }
 
-  public static Predicate<AccountState> externalId(String externalId) {
+  public static Predicate<AccountState> equalsName(String name) {
+    return new AccountPredicate(
+        AccountField.NAME_PART_NO_SECONDARY_EMAIL,
+        AccountQueryBuilder.FIELD_NAME,
+        name.toLowerCase());
+  }
+
+  public static Predicate<AccountState> externalIdIncludingSecondaryEmails(String externalId) {
     return new AccountPredicate(AccountField.EXTERNAL_ID, externalId);
   }
 
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index 946a729..055b423 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -18,6 +18,8 @@
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.errors.NotSignedInException;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.LimitPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryBuilder;
@@ -26,12 +28,21 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.account.AccountField;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /** Parses a query string meant to be applied to account objects. */
 public class AccountQueryBuilder extends QueryBuilder<AccountState> {
+  private static final Logger log = LoggerFactory.getLogger(AccountQueryBuilder.class);
+
   public static final String FIELD_ACCOUNT = "account";
   public static final String FIELD_EMAIL = "email";
   public static final String FIELD_LIMIT = "limit";
@@ -46,10 +57,17 @@
 
   public static class Arguments {
     private final Provider<CurrentUser> self;
+    private final AccountIndexCollection indexes;
+    private final PermissionBackend permissionBackend;
 
     @Inject
-    public Arguments(Provider<CurrentUser> self) {
+    public Arguments(
+        Provider<CurrentUser> self,
+        AccountIndexCollection indexes,
+        PermissionBackend permissionBackend) {
       this.self = self;
+      this.indexes = indexes;
+      this.permissionBackend = permissionBackend;
     }
 
     IdentifiedUser getIdentifiedUser() throws QueryParseException {
@@ -71,6 +89,11 @@
         throw new QueryParseException(NotSignedInException.MESSAGE, e);
       }
     }
+
+    Schema<AccountState> schema() {
+      Index<?, AccountState> index = indexes != null ? indexes.getSearchIndex() : null;
+      return index != null ? index.getSchema() : null;
+    }
   }
 
   private final Arguments args;
@@ -82,8 +105,17 @@
   }
 
   @Operator
-  public Predicate<AccountState> email(String email) {
-    return AccountPredicates.email(email);
+  public Predicate<AccountState> email(String email)
+      throws PermissionBackendException, QueryParseException {
+    if (canSeeSecondaryEmails()) {
+      return AccountPredicates.emailIncludingSecondaryEmails(email);
+    }
+
+    if (args.schema().hasField(AccountField.PREFERRED_EMAIL)) {
+      return AccountPredicates.preferredEmail(email);
+    }
+
+    throw new QueryParseException("'email' operator is not supported by account index version");
   }
 
   @Operator
@@ -107,8 +139,17 @@
   }
 
   @Operator
-  public Predicate<AccountState> name(String name) {
-    return AccountPredicates.equalsName(name);
+  public Predicate<AccountState> name(String name)
+      throws PermissionBackendException, QueryParseException {
+    if (canSeeSecondaryEmails()) {
+      return AccountPredicates.equalsNameIcludingSecondaryEmails(name);
+    }
+
+    if (args.schema().hasField(AccountField.NAME_PART_NO_SECONDARY_EMAIL)) {
+      return AccountPredicates.equalsName(name);
+    }
+
+    return AccountPredicates.fullName(name);
   }
 
   @Operator
@@ -124,7 +165,8 @@
 
   @Override
   protected Predicate<AccountState> defaultField(String query) {
-    Predicate<AccountState> defaultPredicate = AccountPredicates.defaultPredicate(query);
+    Predicate<AccountState> defaultPredicate =
+        AccountPredicates.defaultPredicate(args.schema(), checkedCanSeeSecondaryEmails(), query);
     if ("self".equalsIgnoreCase(query) || "me".equalsIgnoreCase(query)) {
       try {
         return Predicate.or(defaultPredicate, AccountPredicates.id(self()));
@@ -138,4 +180,20 @@
   private Account.Id self() throws QueryParseException {
     return args.getIdentifiedUser().getAccountId();
   }
+
+  private boolean canSeeSecondaryEmails() throws PermissionBackendException, QueryParseException {
+    return args.permissionBackend.user(args.getUser()).test(GlobalPermission.MODIFY_ACCOUNT);
+  }
+
+  private boolean checkedCanSeeSecondaryEmails() {
+    try {
+      return canSeeSecondaryEmails();
+    } catch (PermissionBackendException e) {
+      log.error("Permission check failed", e);
+      return false;
+    } catch (QueryParseException e) {
+      // User is not signed in.
+      return false;
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index 2fb837c..f1be580 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -83,7 +83,7 @@
   }
 
   public List<AccountState> byDefault(String query) throws OrmException {
-    return query(AccountPredicates.defaultPredicate(query));
+    return query(AccountPredicates.defaultPredicate(schema(), true, query));
   }
 
   public List<AccountState> byExternalId(String scheme, String id) throws OrmException {
@@ -91,7 +91,7 @@
   }
 
   public List<AccountState> byExternalId(ExternalId.Key externalId) throws OrmException {
-    return query(AccountPredicates.externalId(externalId.toString()));
+    return query(AccountPredicates.externalIdIncludingSecondaryEmails(externalId.toString()));
   }
 
   public AccountState oneByExternalId(String externalId) throws OrmException {
diff --git a/java/com/google/gerrit/server/query/change/RegexPathPredicate.java b/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
index 46b4cd5..3764a98 100644
--- a/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
@@ -33,7 +33,7 @@
     } catch (IOException e) {
       throw new OrmException(e);
     }
-    return RegexListSearcher.ofStrings(getValue()).hasMatch(files);
+    return RegexListSearcher.ofStrings(getValue()).search(files).findAny().isPresent();
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
index 1388523..5a1f6bf 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
@@ -24,9 +24,8 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.account.WatchConfig;
+import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -45,19 +44,16 @@
     implements RestModifyView<AccountResource, List<ProjectWatchInfo>> {
   private final Provider<IdentifiedUser> self;
   private final PermissionBackend permissionBackend;
-  private final AccountCache accountCache;
-  private final WatchConfig.Accessor watchConfig;
+  private final AccountsUpdate.User accountsUpdate;
 
   @Inject
   DeleteWatchedProjects(
       Provider<IdentifiedUser> self,
       PermissionBackend permissionBackend,
-      AccountCache accountCache,
-      WatchConfig.Accessor watchConfig) {
+      AccountsUpdate.User accountsUpdate) {
     this.self = self;
     this.permissionBackend = permissionBackend;
-    this.accountCache = accountCache;
-    this.watchConfig = watchConfig;
+    this.accountsUpdate = accountsUpdate;
   }
 
   @Override
@@ -72,14 +68,18 @@
     }
 
     Account.Id accountId = rsrc.getUser().getAccountId();
-    watchConfig.deleteProjectWatches(
-        accountId,
-        input
-            .stream()
-            .filter(Objects::nonNull)
-            .map(w -> ProjectWatchKey.create(new Project.NameKey(w.project), w.filter))
-            .collect(toList()));
-    accountCache.evict(accountId);
+    accountsUpdate
+        .create()
+        .update(
+            "Delete Project Watches via API",
+            accountId,
+            u ->
+                u.deleteProjectWatches(
+                    input
+                        .stream()
+                        .filter(Objects::nonNull)
+                        .map(w -> ProjectWatchKey.create(new Project.NameKey(w.project), w.filter))
+                        .collect(toList())));
     return Response.none();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetEmails.java b/java/com/google/gerrit/server/restapi/account/GetEmails.java
index 9c482a3..640cc64 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEmails.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEmails.java
@@ -15,8 +15,15 @@
 package com.google.gerrit.server.restapi.account;
 
 import com.google.gerrit.extensions.common.EmailInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -25,9 +32,22 @@
 
 @Singleton
 public class GetEmails implements RestReadView<AccountResource> {
+  private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
+
+  @Inject
+  GetEmails(Provider<CurrentUser> self, PermissionBackend permissionBackend) {
+    this.self = self;
+    this.permissionBackend = permissionBackend;
+  }
 
   @Override
-  public List<EmailInfo> apply(AccountResource rsrc) {
+  public List<EmailInfo> apply(AccountResource rsrc)
+      throws AuthException, PermissionBackendException {
+    if (self.get() != rsrc.getUser()) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+    }
+
     List<EmailInfo> emails = new ArrayList<>();
     for (String email : rsrc.getUser().getEmailAddresses()) {
       if (email != null) {
diff --git a/java/com/google/gerrit/server/restapi/account/GetPreferences.java b/java/com/google/gerrit/server/restapi/account/GetPreferences.java
index b071ade..46bc389 100644
--- a/java/com/google/gerrit/server/restapi/account/GetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetPreferences.java
@@ -50,6 +50,6 @@
     }
 
     Account.Id id = rsrc.getUser().getAccountId();
-    return accountCache.get(id).getAccount().getGeneralPreferencesInfo();
+    return accountCache.get(id).getGeneralPreferences();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
index b465029..ffddc7c 100644
--- a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
@@ -18,11 +18,13 @@
 import com.google.common.collect.ComparisonChain;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.account.WatchConfig;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.WatchConfig.NotifyType;
 import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -45,30 +47,31 @@
 public class GetWatchedProjects implements RestReadView<AccountResource> {
   private final PermissionBackend permissionBackend;
   private final Provider<IdentifiedUser> self;
-  private final WatchConfig.Accessor watchConfig;
+  private final Accounts accounts;
 
   @Inject
   public GetWatchedProjects(
-      PermissionBackend permissionBackend,
-      Provider<IdentifiedUser> self,
-      WatchConfig.Accessor watchConfig) {
+      PermissionBackend permissionBackend, Provider<IdentifiedUser> self, Accounts accounts) {
     this.permissionBackend = permissionBackend;
     this.self = self;
-    this.watchConfig = watchConfig;
+    this.accounts = accounts;
   }
 
   @Override
   public List<ProjectWatchInfo> apply(AccountResource rsrc)
       throws OrmException, AuthException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+          PermissionBackendException, ResourceNotFoundException {
     if (self.get() != rsrc.getUser()) {
       permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
     Account.Id accountId = rsrc.getUser().getAccountId();
+    AccountState account = accounts.get(accountId);
+    if (account == null) {
+      throw new ResourceNotFoundException();
+    }
     List<ProjectWatchInfo> projectWatchInfos = new ArrayList<>();
-    for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e :
-        watchConfig.getProjectWatches(accountId).entrySet()) {
+    for (Map.Entry<ProjectWatchKey, Set<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/Index.java b/java/com/google/gerrit/server/restapi/account/Index.java
index 2f4a87c..20a381a 100644
--- a/java/com/google/gerrit/server/restapi/account/Index.java
+++ b/java/com/google/gerrit/server/restapi/account/Index.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2016 The Android Open Source Project
+// Copyright (C) 2016 The Android Open Source Project
 //
-//Licensed under the Apache License, Version 2.0 (the "License");
-//you may not use this file except in compliance with the License.
-//You may obtain a copy of the License at
+// 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
+// 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.
+// 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.account;
 
diff --git a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
index 145ce0e..bceaaf6 100644
--- a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
@@ -19,10 +19,9 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.WatchConfig;
 import com.google.gerrit.server.account.WatchConfig.NotifyType;
 import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
@@ -49,8 +48,7 @@
   private final PermissionBackend permissionBackend;
   private final GetWatchedProjects getWatchedProjects;
   private final ProjectsCollection projectsCollection;
-  private final AccountCache accountCache;
-  private final WatchConfig.Accessor watchConfig;
+  private final AccountsUpdate.User accountsUpdate;
 
   @Inject
   public PostWatchedProjects(
@@ -58,14 +56,12 @@
       PermissionBackend permissionBackend,
       GetWatchedProjects getWatchedProjects,
       ProjectsCollection projectsCollection,
-      AccountCache accountCache,
-      WatchConfig.Accessor watchConfig) {
+      AccountsUpdate.User accountsUpdate) {
     this.self = self;
     this.permissionBackend = permissionBackend;
     this.getWatchedProjects = getWatchedProjects;
     this.projectsCollection = projectsCollection;
-    this.accountCache = accountCache;
-    this.watchConfig = watchConfig;
+    this.accountsUpdate = accountsUpdate;
   }
 
   @Override
@@ -76,9 +72,13 @@
       permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
-    Account.Id accountId = rsrc.getUser().getAccountId();
-    watchConfig.upsertProjectWatches(accountId, asMap(input));
-    accountCache.evict(accountId);
+    Map<ProjectWatchKey, Set<NotifyType>> projectWatches = asMap(input);
+    accountsUpdate
+        .create()
+        .update(
+            "Update Project Watches via API",
+            rsrc.getUser().getAccountId(),
+            u -> u.updateProjectWatches(projectWatches));
     return getWatchedProjects.apply(rsrc);
   }
 
diff --git a/java/com/google/gerrit/server/restapi/account/PutUsername.java b/java/com/google/gerrit/server/restapi/account/PutUsername.java
index 646fd44..fc40152 100644
--- a/java/com/google/gerrit/server/restapi/account/PutUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/PutUsername.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.server.restapi.account;
 
-import com.google.gerrit.common.errors.NameAlreadyUsedException;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+
+import com.google.common.base.Strings;
 import com.google.gerrit.extensions.api.accounts.UsernameInput;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -22,14 +24,18 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.account.ChangeUserName;
-import com.google.gerrit.server.account.InvalidUserNameException;
+import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
 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.ssh.SshKeyCache;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -40,19 +46,25 @@
 @Singleton
 public class PutUsername implements RestModifyView<AccountResource, UsernameInput> {
   private final Provider<CurrentUser> self;
-  private final ChangeUserName.Factory changeUserNameFactory;
   private final PermissionBackend permissionBackend;
+  private final ExternalIds externalIds;
+  private final AccountsUpdate.Server accountsUpdate;
+  private final SshKeyCache sshKeyCache;
   private final Realm realm;
 
   @Inject
   PutUsername(
       Provider<CurrentUser> self,
-      ChangeUserName.Factory changeUserNameFactory,
       PermissionBackend permissionBackend,
+      ExternalIds externalIds,
+      AccountsUpdate.Server accountsUpdate,
+      SshKeyCache sshKeyCache,
       Realm realm) {
     this.self = self;
-    this.changeUserNameFactory = changeUserNameFactory;
     this.permissionBackend = permissionBackend;
+    this.externalIds = externalIds;
+    this.accountsUpdate = accountsUpdate;
+    this.sshKeyCache = sshKeyCache;
     this.realm = realm;
   }
 
@@ -73,19 +85,39 @@
       input = new UsernameInput();
     }
 
-    try {
-      changeUserNameFactory.create("Set Username via API", rsrc.getUser(), input.username).call();
-    } catch (IllegalStateException e) {
-      if (ChangeUserName.USERNAME_CANNOT_BE_CHANGED.equals(e.getMessage())) {
-        throw new MethodNotAllowedException(e.getMessage());
-      }
-      throw e;
-    } catch (InvalidUserNameException e) {
+    Account.Id accountId = rsrc.getUser().getAccountId();
+    if (!externalIds.byAccount(accountId, SCHEME_USERNAME).isEmpty()) {
+      throw new MethodNotAllowedException("Username cannot be changed.");
+    }
+
+    if (Strings.isNullOrEmpty(input.username)) {
+      return input.username;
+    }
+
+    if (!ExternalId.isValidUsername(input.username)) {
       throw new UnprocessableEntityException("invalid username");
-    } catch (NameAlreadyUsedException e) {
+    }
+
+    ExternalId.Key key = ExternalId.Key.create(SCHEME_USERNAME, input.username);
+    try {
+      accountsUpdate
+          .create()
+          .update(
+              "Set Username via API",
+              accountId,
+              u -> u.addExternalId(ExternalId.create(key, accountId, null, null)));
+    } catch (OrmDuplicateKeyException dupeErr) {
+      // If we are using this identity, don't report the exception.
+      ExternalId other = externalIds.get(key);
+      if (other != null && other.accountId().equals(accountId)) {
+        return input.username;
+      }
+
+      // Otherwise, someone else has this identity.
       throw new ResourceConflictException("username already used");
     }
 
+    sshKeyCache.evict(input.username);
     return input.username;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
index f508fa2..fa4550d 100644
--- a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
@@ -21,22 +21,28 @@
 import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryResult;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gerrit.server.account.AccountInfoComparator;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.GerritServerConfig;
+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.query.account.AccountPredicates;
 import com.google.gerrit.server.query.account.AccountQueryBuilder;
 import com.google.gerrit.server.query.account.AccountQueryProcessor;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.LinkedHashMap;
@@ -49,6 +55,8 @@
 public class QueryAccounts implements RestReadView<TopLevelResource> {
   private static final int MAX_SUGGEST_RESULTS = 100;
 
+  private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
   private final AccountLoader.Factory accountLoaderFactory;
   private final AccountQueryBuilder queryBuilder;
   private final AccountQueryProcessor queryProcessor;
@@ -117,10 +125,14 @@
 
   @Inject
   QueryAccounts(
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
       AccountLoader.Factory accountLoaderFactory,
       AccountQueryBuilder queryBuilder,
       AccountQueryProcessor queryProcessor,
       @GerritServerConfig Config cfg) {
+    this.self = self;
+    this.permissionBackend = permissionBackend;
     this.accountLoaderFactory = accountLoaderFactory;
     this.queryBuilder = queryBuilder;
     this.queryProcessor = queryProcessor;
@@ -143,7 +155,7 @@
 
   @Override
   public List<AccountInfo> apply(TopLevelResource rsrc)
-      throws OrmException, BadRequestException, MethodNotAllowedException {
+      throws OrmException, RestApiException, PermissionBackendException {
     if (Strings.isNullOrEmpty(query)) {
       throw new BadRequestException("missing query field");
     }
@@ -156,14 +168,21 @@
     if (options.contains(ListAccountsOption.DETAILS)) {
       fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
     }
+    boolean modifyAccountCapabilityChecked = false;
     if (options.contains(ListAccountsOption.ALL_EMAILS)) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      modifyAccountCapabilityChecked = true;
       fillOptions.add(FillOptions.EMAIL);
       fillOptions.add(FillOptions.SECONDARY_EMAILS);
     }
     if (suggest) {
       fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
       fillOptions.add(FillOptions.EMAIL);
-      fillOptions.add(FillOptions.SECONDARY_EMAILS);
+
+      if (modifyAccountCapabilityChecked
+          || permissionBackend.user(self).test(GlobalPermission.MODIFY_ACCOUNT)) {
+        fillOptions.add(FillOptions.SECONDARY_EMAILS);
+      }
     }
     accountLoader = accountLoaderFactory.create(fillOptions);
 
diff --git a/java/com/google/gerrit/server/restapi/account/SetPreferences.java b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
index 2c9f97a..9b2b231 100644
--- a/java/com/google/gerrit/server/restapi/account/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
@@ -14,19 +14,8 @@
 
 package com.google.gerrit.server.restapi.account;
 
-import static com.google.gerrit.server.config.ConfigUtil.storeSection;
-import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_ID;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_MATCH;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_TOKEN;
-import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
-import static com.google.gerrit.server.git.UserConfigSections.URL_ALIAS;
-
 import com.google.common.base.Strings;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
-import com.google.gerrit.extensions.client.MenuItem;
 import com.google.gerrit.extensions.config.DownloadScheme;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -36,33 +25,24 @@
 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.GeneralPreferencesLoader;
-import com.google.gerrit.server.account.VersionedAccountPreferences;
-import com.google.gerrit.server.config.AllUsersName;
-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.account.PreferencesConfig;
 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;
 import java.io.IOException;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
 
 @Singleton
 public class SetPreferences implements RestModifyView<AccountResource, GeneralPreferencesInfo> {
   private final Provider<CurrentUser> self;
   private final AccountCache cache;
   private final PermissionBackend permissionBackend;
-  private final GeneralPreferencesLoader loader;
-  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
-  private final AllUsersName allUsersName;
+  private final AccountsUpdate.User accountsUpdate;
   private final DynamicMap<DownloadScheme> downloadSchemes;
 
   @Inject
@@ -70,124 +50,31 @@
       Provider<CurrentUser> self,
       AccountCache cache,
       PermissionBackend permissionBackend,
-      GeneralPreferencesLoader loader,
-      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
-      AllUsersName allUsersName,
+      AccountsUpdate.User accountsUpdate,
       DynamicMap<DownloadScheme> downloadSchemes) {
     this.self = self;
-    this.loader = loader;
     this.cache = cache;
     this.permissionBackend = permissionBackend;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
-    this.allUsersName = allUsersName;
+    this.accountsUpdate = accountsUpdate;
     this.downloadSchemes = downloadSchemes;
   }
 
   @Override
-  public GeneralPreferencesInfo apply(AccountResource rsrc, GeneralPreferencesInfo i)
+  public GeneralPreferencesInfo apply(AccountResource rsrc, GeneralPreferencesInfo input)
       throws AuthException, BadRequestException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+          PermissionBackendException, OrmException {
     if (self.get() != rsrc.getUser()) {
       permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
-    checkDownloadScheme(i.downloadScheme);
+    checkDownloadScheme(input.downloadScheme);
+    PreferencesConfig.validateMy(input.my);
     Account.Id id = rsrc.getUser().getAccountId();
-    GeneralPreferencesInfo n = loader.merge(id, i);
 
-    n.changeTable = i.changeTable;
-    n.my = i.my;
-    n.urlAliases = i.urlAliases;
-
-    writeToGit(id, n);
-
-    return cache.get(id).getAccount().getGeneralPreferencesInfo();
-  }
-
-  private void writeToGit(Account.Id id, GeneralPreferencesInfo i)
-      throws RepositoryNotFoundException, IOException, ConfigInvalidException, BadRequestException {
-    VersionedAccountPreferences prefs;
-    try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
-      prefs = VersionedAccountPreferences.forUser(id);
-      prefs.load(md);
-
-      storeSection(
-          prefs.getConfig(),
-          UserConfigSections.GENERAL,
-          null,
-          i,
-          loader.readDefaultsFromGit(md.getRepository(), null));
-
-      storeMyChangeTableColumns(prefs, i.changeTable);
-      storeMyMenus(prefs, i.my);
-      storeUrlAliases(prefs, i.urlAliases);
-      prefs.commit(md);
-      cache.evict(id);
-    }
-  }
-
-  public static void storeMyMenus(VersionedAccountPreferences prefs, List<MenuItem> my)
-      throws BadRequestException {
-    Config cfg = prefs.getConfig();
-    if (my != null) {
-      unsetSection(cfg, UserConfigSections.MY);
-      for (MenuItem item : my) {
-        checkRequiredMenuItemField(item.name, "name");
-        checkRequiredMenuItemField(item.url, "URL");
-
-        set(cfg, item.name, KEY_URL, item.url);
-        set(cfg, item.name, KEY_TARGET, item.target);
-        set(cfg, item.name, KEY_ID, item.id);
-      }
-    }
-  }
-
-  public static void storeMyChangeTableColumns(
-      VersionedAccountPreferences prefs, List<String> changeTable) {
-    Config cfg = prefs.getConfig();
-    if (changeTable != null) {
-      unsetSection(cfg, UserConfigSections.CHANGE_TABLE);
-      cfg.setStringList(UserConfigSections.CHANGE_TABLE, null, CHANGE_TABLE_COLUMN, changeTable);
-    }
-  }
-
-  private static void set(Config cfg, String section, String key, @Nullable String val) {
-    if (val == null || val.trim().isEmpty()) {
-      cfg.unset(UserConfigSections.MY, section.trim(), key);
-    } else {
-      cfg.setString(UserConfigSections.MY, section.trim(), key, val.trim());
-    }
-  }
-
-  private static void unsetSection(Config cfg, String section) {
-    cfg.unsetSection(section, null);
-    for (String subsection : cfg.getSubsections(section)) {
-      cfg.unsetSection(section, subsection);
-    }
-  }
-
-  public static void storeUrlAliases(
-      VersionedAccountPreferences prefs, Map<String, String> urlAliases) {
-    if (urlAliases != null) {
-      Config cfg = prefs.getConfig();
-      for (String subsection : cfg.getSubsections(URL_ALIAS)) {
-        cfg.unsetSection(URL_ALIAS, subsection);
-      }
-
-      int i = 1;
-      for (Entry<String, String> e : urlAliases.entrySet()) {
-        cfg.setString(URL_ALIAS, URL_ALIAS + i, KEY_MATCH, e.getKey());
-        cfg.setString(URL_ALIAS, URL_ALIAS + i, KEY_TOKEN, e.getValue());
-        i++;
-      }
-    }
-  }
-
-  private static void checkRequiredMenuItemField(String value, String name)
-      throws BadRequestException {
-    if (value == null || value.trim().isEmpty()) {
-      throw new BadRequestException(name + " for menu item is required");
-    }
+    accountsUpdate
+        .create()
+        .update("Set Preferences via API", id, u -> u.setGeneralPreferences(input));
+    return cache.get(id).getGeneralPreferences();
   }
 
   private void checkDownloadScheme(String downloadScheme) throws BadRequestException {
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPick.java b/java/com/google/gerrit/server/restapi/change/CherryPick.java
index c1479b7..2de5ba46 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPick.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPick.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 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.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -47,16 +48,20 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Singleton
 public class CherryPick
     extends RetryingRestModifyView<RevisionResource, CherryPickInput, ChangeInfo>
     implements UiAction<RevisionResource> {
+  private static final Logger log = LoggerFactory.getLogger(CherryPick.class);
   private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> user;
   private final CherryPickChange cherryPickChange;
   private final ChangeJson.Factory json;
   private final ContributorAgreementsChecker contributorAgreements;
+  private final ProjectCache projectCache;
 
   @Inject
   CherryPick(
@@ -65,13 +70,15 @@
       RetryHelper retryHelper,
       CherryPickChange cherryPickChange,
       ChangeJson.Factory json,
-      ContributorAgreementsChecker contributorAgreements) {
+      ContributorAgreementsChecker contributorAgreements,
+      ProjectCache projectCache) {
     super(retryHelper);
     this.permissionBackend = permissionBackend;
     this.user = user;
     this.cherryPickChange = cherryPickChange;
     this.json = json;
     this.contributorAgreements = contributorAgreements;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -94,6 +101,7 @@
         .project(rsrc.getChange().getProject())
         .ref(refName)
         .check(RefPermission.CREATE_CHANGE);
+    projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite();
 
     try {
       Change.Id cherryPickedChangeId =
@@ -113,12 +121,18 @@
 
   @Override
   public UiAction.Description getDescription(RevisionResource rsrc) {
+    boolean projectStatePermitsWrite = false;
+    try {
+      projectStatePermitsWrite = projectCache.checkedGet(rsrc.getProject()).statePermitsWrite();
+    } catch (IOException e) {
+      log.error("Failed to check if project state permits write: " + rsrc.getProject(), e);
+    }
     return new UiAction.Description()
         .setLabel("Cherry Pick")
         .setTitle("Cherry pick change to a different branch")
         .setVisible(
             and(
-                rsrc.isCurrent(),
+                rsrc.isCurrent() && projectStatePermitsWrite,
                 permissionBackend
                     .user(user)
                     .project(rsrc.getProject())
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
index 039c3ca6..7c10086 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
@@ -94,6 +94,7 @@
         .project(projectName)
         .ref(refName)
         .check(RefPermission.CREATE_CHANGE);
+    rsrc.getProjectState().checkStatePermitsWrite();
 
     try {
       Change.Id cherryPickedChangeId =
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index d4e1c40..9aa1957 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.common.MergeInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -114,6 +115,7 @@
   private final SubmitType submitType;
   private final NotifyUtil notifyUtil;
   private final ContributorAgreementsChecker contributorAgreements;
+  private final boolean disablePrivateChanges;
 
   @Inject
   CreateChange(
@@ -152,6 +154,7 @@
     this.changeFinder = changeFinder;
     this.psUtil = psUtil;
     this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
+    this.disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false);
     this.mergeUtilFactory = mergeUtilFactory;
     this.notifyUtil = notifyUtil;
     this.contributorAgreements = contributorAgreements;
@@ -181,11 +184,19 @@
     }
 
     ProjectResource rsrc = projectsCollection.parse(input.project);
+    boolean privateByDefault = rsrc.getProjectState().is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
+    boolean isPrivate = input.isPrivate == null ? privateByDefault : input.isPrivate;
+
+    if (isPrivate && disablePrivateChanges) {
+      throw new MethodNotAllowedException("private changes are disabled");
+    }
+
     contributorAgreements.check(rsrc.getNameKey(), rsrc.getUser());
 
     Project.NameKey project = rsrc.getNameKey();
     String refName = RefNames.fullName(input.branch);
     permissionBackend.user(user).project(project).ref(refName).check(RefPermission.CREATE_CHANGE);
+    rsrc.getProjectState().checkStatePermitsWrite();
 
     try (Repository git = gitManager.openRepository(project);
         ObjectInserter oi = git.newObjectInserter();
@@ -231,7 +242,7 @@
       IdentifiedUser me = user.get().asIdentifiedUser();
       PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
       AccountState account = accountCache.get(me.getAccountId());
-      GeneralPreferencesInfo info = account.getAccount().getGeneralPreferencesInfo();
+      GeneralPreferencesInfo info = account.getGeneralPreferences();
 
       ObjectId treeId = mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree();
       ObjectId id = ChangeIdUtil.computeChangeId(treeId, mergeTip, author, author, input.subject);
@@ -257,7 +268,6 @@
         c = newCommit(oi, rw, author, mergeTip, commitMessage);
       }
 
-      boolean privateByDefault = rsrc.getProjectState().is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
       Change.Id changeId = new Change.Id(seq.nextChangeId());
       ChangeInserter ins = changeInserterFactory.create(changeId, c, refName);
       ins.setMessage(String.format("Uploaded patch set %s.", ins.getPatchSetId().get()));
@@ -266,7 +276,7 @@
         topic = Strings.emptyToNull(topic.trim());
       }
       ins.setTopic(topic);
-      ins.setPrivate(input.isPrivate == null ? privateByDefault : input.isPrivate);
+      ins.setPrivate(isPrivate);
       ins.setWorkInProgress(input.workInProgress != null && input.workInProgress);
       ins.setGroups(groups);
       ins.setNotify(input.notify);
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index 33a7453..dcaba77 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -130,6 +130,9 @@
           UpdateException, PermissionBackendException {
     rsrc.permissions().database(db).check(ChangePermission.ADD_PATCH_SET);
 
+    ProjectState projectState = projectCache.checkedGet(rsrc.getProject());
+    projectState.checkStatePermitsWrite();
+
     MergeInput merge = in.merge;
     if (merge == null || Strings.isNullOrEmpty(merge.source)) {
       throw new BadRequestException("merge.source must be non-empty");
@@ -137,7 +140,6 @@
     in.baseChange = Strings.nullToEmpty(in.baseChange).trim();
 
     PatchSet ps = psUtil.current(db.get(), rsrc.getNotes());
-    ProjectState projectState = projectCache.checkedGet(rsrc.getProject());
     Change change = rsrc.getChange();
     Project.NameKey project = change.getProject();
     Branch.NameKey dest = change.getDest();
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index 2607f9c..2ad954d 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -70,10 +70,14 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Move extends RetryingRestModifyView<ChangeResource, MoveInput, ChangeInfo>
     implements UiAction<ChangeResource> {
+  private static final Logger log = LoggerFactory.getLogger(Move.class);
+
   private final PermissionBackend permissionBackend;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson.Factory json;
@@ -114,7 +118,8 @@
   @Override
   protected ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, MoveInput input)
-      throws RestApiException, OrmException, UpdateException, PermissionBackendException {
+      throws RestApiException, OrmException, UpdateException, PermissionBackendException,
+          IOException {
     Change change = rsrc.getChange();
     Project.NameKey project = rsrc.getProject();
     IdentifiedUser caller = rsrc.getUser().asIdentifiedUser();
@@ -136,6 +141,7 @@
     } catch (AuthException denied) {
       throw new AuthException("move not permitted", denied);
     }
+    projectCache.checkedGet(project).checkStatePermitsWrite();
 
     try (BatchUpdate u =
         updateFactory.create(dbProvider.get(), project, caller, TimeUtil.nowTs())) {
@@ -274,12 +280,18 @@
   @Override
   public UiAction.Description getDescription(ChangeResource rsrc) {
     Change change = rsrc.getChange();
+    boolean projectStatePermitsWrite = false;
+    try {
+      projectStatePermitsWrite = projectCache.checkedGet(rsrc.getProject()).statePermitsWrite();
+    } catch (IOException e) {
+      log.error("Failed to check if project state permits write: " + rsrc.getProject(), e);
+    }
     return new UiAction.Description()
         .setLabel("Move Change")
         .setTitle("Move change to a different branch")
         .setVisible(
             and(
-                change.getStatus().isOpen(),
+                change.getStatus().isOpen() && projectStatePermitsWrite,
                 and(
                     permissionBackend
                         .user(rsrc.getUser())
diff --git a/java/com/google/gerrit/server/restapi/change/PostPrivate.java b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
index 9f02fa8..5a13346 100644
--- a/java/com/google/gerrit/server/restapi/change/PostPrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
@@ -27,6 +28,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -36,6 +38,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
 
 @Singleton
 public class PostPrivate
@@ -45,6 +48,7 @@
   private final Provider<ReviewDb> dbProvider;
   private final PermissionBackend permissionBackend;
   private final SetPrivateOp.Factory setPrivateOpFactory;
+  private final boolean disablePrivateChanges;
 
   @Inject
   PostPrivate(
@@ -52,18 +56,24 @@
       RetryHelper retryHelper,
       ChangeMessagesUtil cmUtil,
       PermissionBackend permissionBackend,
-      SetPrivateOp.Factory setPrivateOpFactory) {
+      SetPrivateOp.Factory setPrivateOpFactory,
+      @GerritServerConfig Config config) {
     super(retryHelper);
     this.dbProvider = dbProvider;
     this.cmUtil = cmUtil;
     this.permissionBackend = permissionBackend;
     this.setPrivateOpFactory = setPrivateOpFactory;
+    this.disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false);
   }
 
   @Override
   public Response<String> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, SetPrivateOp.Input input)
       throws RestApiException, UpdateException {
+    if (disablePrivateChanges) {
+      throw new MethodNotAllowedException("private changes are disabled");
+    }
+
     if (!canSetPrivate(rsrc).value()) {
       throw new AuthException("not allowed to mark private");
     }
@@ -88,7 +98,7 @@
     return new UiAction.Description()
         .setLabel("Mark private")
         .setTitle("Mark change as private")
-        .setVisible(and(!change.isPrivate(), canSetPrivate(rsrc)));
+        .setVisible(and(!disablePrivateChanges && !change.isPrivate(), canSetPrivate(rsrc)));
   }
 
   private BooleanCondition canSetPrivate(ChangeResource rsrc) {
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 7507666..c2c5dce 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
@@ -27,7 +28,6 @@
 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
@@ -232,10 +232,9 @@
     }
     ProjectState projectState = projectCache.checkedGet(revision.getProject());
     LabelTypes labelTypes = projectState.getLabelTypes(revision.getNotes(), revision.getUser());
+    input.drafts = firstNonNull(input.drafts, DraftHandling.KEEP);
     if (input.onBehalfOf != null) {
       revision = onBehalfOf(revision, labelTypes, input);
-    } else if (input.drafts == null) {
-      input.drafts = DraftHandling.DELETE;
     }
     if (input.labels != null) {
       checkLabels(revision, labelTypes, input.labels);
@@ -435,9 +434,6 @@
       throw new AuthException(
           String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf));
     }
-    if (in.drafts == null) {
-      in.drafts = DraftHandling.KEEP;
-    }
     if (in.drafts != DraftHandling.KEEP) {
       throw new AuthException("not allowed to modify other user's drafts");
     }
@@ -900,7 +896,6 @@
         }
       }
 
-      List<Comment> toDel = new ArrayList<>();
       List<Comment> toPublish = new ArrayList<>();
 
       Set<CommentSetEntry> existingIds =
@@ -931,23 +926,19 @@
       }
 
       switch (in.drafts) {
-        case KEEP:
-        default:
-          break;
-        case DELETE:
-          toDel.addAll(drafts.values());
-          break;
         case PUBLISH:
         case PUBLISH_ALL_REVISIONS:
           commentsUtil.publish(ctx, psId, drafts.values(), in.tag);
           comments.addAll(drafts.values());
           break;
+        case KEEP:
+        default:
+          break;
       }
       ChangeUpdate u = ctx.getUpdate(psId);
-      commentsUtil.deleteComments(ctx.getDb(), u, toDel);
       commentsUtil.putComments(ctx.getDb(), u, Status.PUBLISHED, toPublish);
       comments.addAll(toPublish);
-      return !toDel.isEmpty() || !toPublish.isEmpty();
+      return !toPublish.isEmpty();
     }
 
     private boolean insertRobotComments(ChangeContext ctx) throws OrmException {
@@ -1118,8 +1109,7 @@
 
     private boolean updateLabels(ProjectState projectState, ChangeContext ctx)
         throws OrmException, ResourceConflictException, IOException {
-      Map<String, Short> inLabels =
-          MoreObjects.firstNonNull(in.labels, Collections.<String, Short>emptyMap());
+      Map<String, Short> inLabels = firstNonNull(in.labels, Collections.emptyMap());
 
       // If no labels were modified and change is closed, abort early.
       // This avoids trying to record a modified label caused by a user
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index f277d2c..c9c43cb 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -179,7 +179,7 @@
   }
 
   private void ensureCanEditCommitMessage(ChangeNotes changeNotes)
-      throws AuthException, PermissionBackendException {
+      throws AuthException, PermissionBackendException, IOException, ResourceConflictException {
     if (!currentUserProvider.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
@@ -189,6 +189,7 @@
           .database(db.get())
           .change(changeNotes)
           .check(ChangePermission.ADD_PATCH_SET);
+      projectCache.checkedGet(changeNotes.getProjectName()).checkStatePermitsWrite();
     } catch (AuthException denied) {
       throw new AuthException("modifying commit message not permitted", denied);
     }
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 7ee5709..767ef7b 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -47,6 +47,7 @@
 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.ProjectCache;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -79,6 +80,7 @@
   private final ChangeJson.Factory json;
   private final Provider<ReviewDb> dbProvider;
   private final PermissionBackend permissionBackend;
+  private final ProjectCache projectCache;
 
   @Inject
   public Rebase(
@@ -88,7 +90,8 @@
       RebaseUtil rebaseUtil,
       ChangeJson.Factory json,
       Provider<ReviewDb> dbProvider,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ProjectCache projectCache) {
     super(retryHelper);
     this.repoManager = repoManager;
     this.rebaseFactory = rebaseFactory;
@@ -96,6 +99,7 @@
     this.json = json;
     this.dbProvider = dbProvider;
     this.permissionBackend = permissionBackend;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -104,6 +108,7 @@
       throws EmailException, OrmException, UpdateException, RestApiException, IOException,
           NoSuchChangeException, PermissionBackendException {
     rsrc.permissions().database(dbProvider).check(ChangePermission.REBASE);
+    projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite();
 
     Change change = rsrc.getChange();
     try (Repository repo = repoManager.openRepository(change.getProject());
@@ -205,6 +210,13 @@
     boolean visible = change.getStatus().isOpen() && resource.isCurrent();
     boolean enabled = false;
 
+    try {
+      visible &= projectCache.checkedGet(resource.getProject()).statePermitsWrite();
+    } catch (IOException e) {
+      log.error("Failed to check if project state permits write: " + resource.getProject(), e);
+      visible = false;
+    }
+
     if (visible) {
       try (Repository repo = repoManager.openRepository(dest.getParentKey());
           RevWalk rw = new RevWalk(repo)) {
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index 4bf1254..642c35a 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -50,6 +51,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -64,6 +66,7 @@
   private final ChangeMessagesUtil cmUtil;
   private final PatchSetUtil psUtil;
   private final ChangeRestored changeRestored;
+  private final ProjectCache projectCache;
 
   @Inject
   Restore(
@@ -73,7 +76,8 @@
       ChangeMessagesUtil cmUtil,
       PatchSetUtil psUtil,
       RetryHelper retryHelper,
-      ChangeRestored changeRestored) {
+      ChangeRestored changeRestored,
+      ProjectCache projectCache) {
     super(retryHelper);
     this.restoredSenderFactory = restoredSenderFactory;
     this.dbProvider = dbProvider;
@@ -81,13 +85,16 @@
     this.cmUtil = cmUtil;
     this.psUtil = psUtil;
     this.changeRestored = changeRestored;
+    this.projectCache = projectCache;
   }
 
   @Override
   protected ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource req, RestoreInput input)
-      throws RestApiException, UpdateException, OrmException, PermissionBackendException {
+      throws RestApiException, UpdateException, OrmException, PermissionBackendException,
+          IOException {
     req.permissions().database(dbProvider).check(ChangePermission.RESTORE);
+    projectCache.checkedGet(req.getProject()).checkStatePermitsWrite();
 
     Op op = new Op(input);
     try (BatchUpdate u =
@@ -154,12 +161,18 @@
 
   @Override
   public UiAction.Description getDescription(ChangeResource rsrc) {
+    boolean projectStatePermitsWrite = false;
+    try {
+      projectStatePermitsWrite = projectCache.checkedGet(rsrc.getProject()).statePermitsWrite();
+    } catch (IOException e) {
+      log.error("Failed to check if project state permits write: " + rsrc.getProject(), e);
+    }
     return new UiAction.Description()
         .setLabel("Restore")
         .setTitle("Restore the change")
         .setVisible(
             and(
-                rsrc.getChange().getStatus() == Status.ABANDONED,
+                rsrc.getChange().getStatus() == Status.ABANDONED && projectStatePermitsWrite,
                 rsrc.permissions().database(dbProvider).testCond(ChangePermission.RESTORE)));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java
index bdab012..b55ca5e 100644
--- a/java/com/google/gerrit/server/restapi/change/Revert.java
+++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -53,6 +53,7 @@
 import com.google.gerrit.server.project.ContributorAgreementsChecker;
 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.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -100,6 +101,7 @@
   private final ApprovalsUtil approvalsUtil;
   private final ChangeReverted changeReverted;
   private final ContributorAgreementsChecker contributorAgreements;
+  private final ProjectCache projectCache;
 
   @Inject
   Revert(
@@ -116,7 +118,8 @@
       @GerritPersonIdent PersonIdent serverIdent,
       ApprovalsUtil approvalsUtil,
       ChangeReverted changeReverted,
-      ContributorAgreementsChecker contributorAgreements) {
+      ContributorAgreementsChecker contributorAgreements,
+      ProjectCache projectCache) {
     super(retryHelper);
     this.db = db;
     this.permissionBackend = permissionBackend;
@@ -131,6 +134,7 @@
     this.approvalsUtil = approvalsUtil;
     this.changeReverted = changeReverted;
     this.contributorAgreements = contributorAgreements;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -145,6 +149,7 @@
 
     contributorAgreements.check(rsrc.getProject(), rsrc.getUser());
     permissionBackend.user(rsrc.getUser()).ref(change.getDest()).check(CREATE_CHANGE);
+    projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite();
 
     Change.Id revertId =
         revert(updateFactory, rsrc.getNotes(), rsrc.getUser(), Strings.emptyToNull(input.message));
@@ -243,12 +248,18 @@
   @Override
   public UiAction.Description getDescription(ChangeResource rsrc) {
     Change change = rsrc.getChange();
+    boolean projectStatePermitsWrite = false;
+    try {
+      projectStatePermitsWrite = projectCache.checkedGet(rsrc.getProject()).statePermitsWrite();
+    } catch (IOException e) {
+      log.error("Failed to check if project state permits write: " + rsrc.getProject(), e);
+    }
     return new UiAction.Description()
         .setLabel("Revert")
         .setTitle("Revert the change")
         .setVisible(
             and(
-                change.getStatus() == Change.Status.MERGED,
+                change.getStatus() == Change.Status.MERGED && projectStatePermitsWrite,
                 permissionBackend
                     .user(rsrc.getUser())
                     .ref(change.getDest())
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 70ebbc1..7a2a148 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gerrit.server.account.AccountLoader;
@@ -44,6 +45,9 @@
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.account.AccountPredicates;
@@ -51,6 +55,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -109,7 +114,7 @@
   // give the ranking algorithm a good set of candidates it can work with
   private static final int CANDIDATE_LIST_MULTIPLIER = 2;
 
-  private final AccountLoader accountLoader;
+  private final AccountLoader.Factory accountLoaderFactory;
   private final AccountQueryBuilder accountQueryBuilder;
   private final GroupBackend groupBackend;
   private final GroupMembers groupMembers;
@@ -118,6 +123,8 @@
   private final AccountIndexCollection accountIndexes;
   private final IndexConfig indexConfig;
   private final AccountControl.Factory accountControlFactory;
+  private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
 
   @Inject
   ReviewersUtil(
@@ -129,10 +136,10 @@
       Metrics metrics,
       AccountIndexCollection accountIndexes,
       IndexConfig indexConfig,
-      AccountControl.Factory accountControlFactory) {
-    Set<FillOptions> fillOptions = EnumSet.of(FillOptions.SECONDARY_EMAILS);
-    fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
-    this.accountLoader = accountLoaderFactory.create(fillOptions);
+      AccountControl.Factory accountControlFactory,
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend) {
+    this.accountLoaderFactory = accountLoaderFactory;
     this.accountQueryBuilder = accountQueryBuilder;
     this.groupBackend = groupBackend;
     this.groupMembers = groupMembers;
@@ -141,6 +148,8 @@
     this.accountIndexes = accountIndexes;
     this.indexConfig = indexConfig;
     this.accountControlFactory = accountControlFactory;
+    this.self = self;
+    this.permissionBackend = permissionBackend;
   }
 
   public interface VisibilityControl {
@@ -153,7 +162,7 @@
       ProjectState projectState,
       VisibilityControl visibilityControl,
       boolean excludeGroups)
-      throws IOException, OrmException, ConfigInvalidException {
+      throws IOException, OrmException, ConfigInvalidException, PermissionBackendException {
     String query = suggestReviewers.getQuery();
     int limit = suggestReviewers.getLimit();
 
@@ -242,7 +251,14 @@
   }
 
   private List<SuggestedReviewerInfo> loadAccounts(List<Account.Id> accountIds)
-      throws OrmException {
+      throws OrmException, PermissionBackendException {
+    Set<FillOptions> fillOptions =
+        permissionBackend.user(self).test(GlobalPermission.MODIFY_ACCOUNT)
+            ? EnumSet.of(FillOptions.SECONDARY_EMAILS)
+            : EnumSet.noneOf(FillOptions.class);
+    fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
+    AccountLoader accountLoader = accountLoaderFactory.create(fillOptions);
+
     try (Timer0.Context ctx = metrics.loadAccountsLatency.start()) {
       List<SuggestedReviewerInfo> reviewer =
           accountIds
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index 04aafff..264a0fb 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -57,6 +57,7 @@
 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.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.restapi.account.AccountsCollection;
@@ -132,6 +133,7 @@
   private final boolean submitWholeTopic;
   private final Provider<InternalChangeQuery> queryProvider;
   private final PatchSetUtil psUtil;
+  private final ProjectCache projectCache;
 
   @Inject
   Submit(
@@ -146,7 +148,8 @@
       AccountsCollection accounts,
       @GerritServerConfig Config cfg,
       Provider<InternalChangeQuery> queryProvider,
-      PatchSetUtil psUtil) {
+      PatchSetUtil psUtil,
+      ProjectCache projectCache) {
     this.dbProvider = dbProvider;
     this.repoManager = repoManager;
     this.permissionBackend = permissionBackend;
@@ -183,6 +186,7 @@
                 cfg.getString("change", null, "submitTopicTooltip"), DEFAULT_TOPIC_TOOLTIP));
     this.queryProvider = queryProvider;
     this.psUtil = psUtil;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -197,6 +201,7 @@
       rsrc.permissions().check(ChangePermission.SUBMIT);
       submitter = rsrc.getUser().asIdentifiedUser();
     }
+    projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite();
 
     return new Output(mergeChange(rsrc, submitter, input));
   }
@@ -303,6 +308,15 @@
       return null; // submit not visible
     }
 
+    try {
+      if (!projectCache.checkedGet(resource.getProject()).statePermitsWrite()) {
+        return null; // submit not visible
+      }
+    } catch (IOException e) {
+      log.error("Error checking if change is submittable", e);
+      throw new OrmRuntimeException("Could not determine problems for the change", e);
+    }
+
     ReviewDb db = dbProvider.get();
     ChangeData cd = changeDataFactory.create(db, resource.getNotes());
     try {
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
index b8792c4..4dc5b06 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
@@ -19,15 +19,14 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.restapi.change.ReviewersUtil.VisibilityControl;
 import com.google.gwtorm.server.OrmException;
@@ -49,7 +48,6 @@
   )
   boolean excludeGroups;
 
-  private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> self;
   private final ProjectCache projectCache;
 
@@ -58,20 +56,19 @@
       AccountVisibility av,
       GenericFactory identifiedUserFactory,
       Provider<ReviewDb> dbProvider,
-      PermissionBackend permissionBackend,
       Provider<CurrentUser> self,
       @GerritServerConfig Config cfg,
       ReviewersUtil reviewersUtil,
       ProjectCache projectCache) {
     super(av, identifiedUserFactory, dbProvider, cfg, reviewersUtil);
-    this.permissionBackend = permissionBackend;
     this.self = self;
     this.projectCache = projectCache;
   }
 
   @Override
   public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
-      throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException {
+      throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     if (!self.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
@@ -86,14 +83,9 @@
   private VisibilityControl getVisibility(ChangeResource rsrc) {
     // Use the destination reference, not the change, as drafts may deny
     // anyone who is not already a reviewer.
-    // TODO(hiesel) Replace this with a check on the change resource once support for drafts was removed
-    PermissionBackend.ForRef perm = permissionBackend.user(self).ref(rsrc.getChange().getDest());
-    return new VisibilityControl() {
-      @Override
-      public boolean isVisibleTo(Account.Id account) throws OrmException {
-        IdentifiedUser who = identifiedUserFactory.create(account);
-        return perm.user(who).testOrFalse(RefPermission.READ);
-      }
+    return account -> {
+      IdentifiedUser who = identifiedUserFactory.create(account);
+      return rsrc.permissions().user(who).testOrFalse(ChangePermission.READ);
     };
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetPreferences.java b/java/com/google/gerrit/server/restapi/config/GetPreferences.java
index c8a173f..3c7453c 100644
--- a/java/com/google/gerrit/server/restapi/config/GetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetPreferences.java
@@ -14,33 +14,25 @@
 
 package com.google.gerrit.server.restapi.config;
 
-import static com.google.gerrit.server.config.ConfigUtil.loadSection;
-
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.GeneralPreferencesLoader;
-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
 public class GetPreferences implements RestReadView<ConfigResource> {
-  private final GeneralPreferencesLoader loader;
   private final GitRepositoryManager gitMgr;
   private final AllUsersName allUsersName;
 
   @Inject
-  public GetPreferences(
-      GeneralPreferencesLoader loader, GitRepositoryManager gitMgr, AllUsersName allUsersName) {
-    this.loader = loader;
+  public GetPreferences(GitRepositoryManager gitMgr, AllUsersName allUsersName) {
     this.gitMgr = gitMgr;
     this.allUsersName = allUsersName;
   }
@@ -48,30 +40,8 @@
   @Override
   public GeneralPreferencesInfo apply(ConfigResource rsrc)
       throws IOException, ConfigInvalidException {
-    return readFromGit(gitMgr, loader, allUsersName, null);
-  }
-
-  static GeneralPreferencesInfo readFromGit(
-      GitRepositoryManager gitMgr,
-      GeneralPreferencesLoader loader,
-      AllUsersName allUsersName,
-      GeneralPreferencesInfo in)
-      throws IOException, ConfigInvalidException, RepositoryNotFoundException {
     try (Repository git = gitMgr.openRepository(allUsersName)) {
-      VersionedAccountPreferences p = VersionedAccountPreferences.forDefault();
-      p.load(git);
-
-      GeneralPreferencesInfo r =
-          loadSection(
-              p.getConfig(),
-              UserConfigSections.GENERAL,
-              null,
-              new GeneralPreferencesInfo(),
-              GeneralPreferencesInfo.defaults(),
-              in);
-
-      // TODO(davido): Maintain cache of default values in AllUsers repository
-      return loader.loadMyMenusAndUrlAliases(r, p, null);
+      return PreferencesConfig.readDefaultPreferences(git);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 4c23f59..f31277d 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -246,6 +246,8 @@
     info.updateDelay =
         (int) ConfigUtil.getTimeUnit(cfg, "change", null, "updateDelay", 300, TimeUnit.SECONDS);
     info.submitWholeTopic = MergeSuperSet.wholeTopicEnabled(cfg);
+    info.disablePrivateChanges =
+        toBoolean(config.getBoolean("change", null, "disablePrivateChanges", false));
     return info;
   }
 
diff --git a/java/com/google/gerrit/server/restapi/config/SetPreferences.java b/java/com/google/gerrit/server/restapi/config/SetPreferences.java
index 17908c3..be990e2 100644
--- a/java/com/google/gerrit/server/restapi/config/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetPreferences.java
@@ -14,10 +14,7 @@
 
 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 static com.google.gerrit.server.restapi.config.GetPreferences.readFromGit;
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -25,20 +22,16 @@
 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.GeneralPreferencesLoader;
-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.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;
 
@@ -47,57 +40,31 @@
 public class SetPreferences implements RestModifyView<ConfigResource, GeneralPreferencesInfo> {
   private static final Logger log = LoggerFactory.getLogger(SetPreferences.class);
 
-  private final GeneralPreferencesLoader loader;
-  private final GitRepositoryManager gitManager;
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final AllUsersName allUsersName;
   private final AccountCache accountCache;
 
   @Inject
   SetPreferences(
-      GeneralPreferencesLoader loader,
-      GitRepositoryManager gitManager,
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       AllUsersName allUsersName,
       AccountCache accountCache) {
-    this.loader = loader;
-    this.gitManager = gitManager;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.allUsersName = allUsersName;
     this.accountCache = accountCache;
   }
 
   @Override
-  public GeneralPreferencesInfo apply(ConfigResource rsrc, GeneralPreferencesInfo i)
+  public GeneralPreferencesInfo apply(ConfigResource rsrc, GeneralPreferencesInfo input)
       throws BadRequestException, IOException, ConfigInvalidException {
-    if (!hasSetFields(i)) {
+    if (!hasSetFields(input)) {
       throw new BadRequestException("unsupported option");
     }
-    return writeToGit(readFromGit(gitManager, loader, allUsersName, i));
-  }
-
-  private GeneralPreferencesInfo writeToGit(GeneralPreferencesInfo i)
-      throws RepositoryNotFoundException, IOException, ConfigInvalidException, BadRequestException {
+    PreferencesConfig.validateMy(input.my);
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
-      VersionedAccountPreferences p = VersionedAccountPreferences.forDefault();
-      p.load(md);
-      storeSection(
-          p.getConfig(), UserConfigSections.GENERAL, null, i, GeneralPreferencesInfo.defaults());
-      com.google.gerrit.server.restapi.account.SetPreferences.storeMyMenus(p, i.my);
-      com.google.gerrit.server.restapi.account.SetPreferences.storeUrlAliases(p, i.urlAliases);
-      p.commit(md);
-
+      GeneralPreferencesInfo updatedPrefs = PreferencesConfig.updateDefaultPreferences(md, input);
       accountCache.evictAllNoReindex();
-
-      GeneralPreferencesInfo r =
-          loadSection(
-              p.getConfig(),
-              UserConfigSections.GENERAL,
-              null,
-              new GeneralPreferencesInfo(),
-              GeneralPreferencesInfo.defaults(),
-              null);
-      return loader.loadMyMenusAndUrlAliases(r, p, null);
+      return updatedPrefs;
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java b/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
index 70dc317..0d52090 100644
--- a/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
+++ b/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
@@ -42,6 +43,7 @@
 import java.util.TreeMap;
 
 public class ConfigInfoImpl extends ConfigInfo {
+  @SuppressWarnings("deprecation")
   public ConfigInfoImpl(
       boolean serverEnableSignedPush,
       ProjectState projectState,
@@ -79,7 +81,18 @@
     maxObjectSizeLimit.inheritedValue = config.getFormattedMaxObjectSizeLimit();
     this.maxObjectSizeLimit = maxObjectSizeLimit;
 
-    this.submitType = p.getSubmitType();
+    this.defaultSubmitType = new SubmitTypeInfo();
+    this.defaultSubmitType.value = projectState.getSubmitType();
+    this.defaultSubmitType.configuredValue =
+        MoreObjects.firstNonNull(
+            projectState.getConfig().getProject().getConfiguredSubmitType(),
+            Project.DEFAULT_SUBMIT_TYPE);
+    ProjectState parent =
+        projectState.isAllProjects() ? projectState : projectState.parents().get(0);
+    this.defaultSubmitType.inheritedValue = parent.getSubmitType();
+
+    this.submitType = this.defaultSubmitType.value;
+
     this.state =
         p.getState() != com.google.gerrit.extensions.client.ProjectState.ACTIVE
             ? p.getState()
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index a06c8c5..44005c0 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
@@ -65,6 +66,7 @@
   private final Provider<ReviewDb> db;
   private final SetAccessUtil setAccess;
   private final ChangeJson.Factory jsonFactory;
+  private final ProjectCache projectCache;
 
   @Inject
   CreateAccessChange(
@@ -75,7 +77,8 @@
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       Provider<ReviewDb> db,
       SetAccessUtil accessUtil,
-      ChangeJson.Factory jsonFactory) {
+      ChangeJson.Factory jsonFactory,
+      ProjectCache projectCache) {
     this.permissionBackend = permissionBackend;
     this.seq = seq;
     this.changeInserterFactory = changeInserterFactory;
@@ -84,6 +87,7 @@
     this.db = db;
     this.setAccess = accessUtil;
     this.jsonFactory = jsonFactory;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -103,6 +107,7 @@
         throw new PermissionDeniedException("cannot create change for " + RefNames.REFS_CONFIG);
       }
     }
+    projectCache.checkedGet(rsrc.getNameKey()).checkStatePermitsWrite();
 
     MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
     List<AccessSection> removals = setAccess.getAccessSections(input.remove);
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index 0b62c15..38bc982 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -183,6 +183,7 @@
         info.revision = revid.getName();
         info.canDelete =
             permissionBackend.user(identifiedUser).ref(name).testOrFalse(RefPermission.DELETE)
+                    && rsrc.getProjectState().statePermitsWrite()
                 ? true
                 : null;
         return info;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index 56c004a..976ab09 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -197,6 +197,8 @@
             input.createNewChangeForAllNotInTarget, InheritableBoolean.INHERIT);
     args.changeIdRequired =
         MoreObjects.firstNonNull(input.requireChangeId, InheritableBoolean.INHERIT);
+    args.rejectEmptyCommit =
+        MoreObjects.firstNonNull(input.rejectEmptyCommit, InheritableBoolean.INHERIT);
     try {
       args.maxObjectSizeLimit = ProjectConfig.validMaxObjectSizeLimit(input.maxObjectSizeLimit);
     } catch (ConfigInvalidException e) {
@@ -287,6 +289,7 @@
           BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
           args.newChangeForAllNotInTarget);
       newProject.setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, args.changeIdRequired);
+      newProject.setBooleanConfig(BooleanProjectConfig.REJECT_EMPTY_COMMIT, args.rejectEmptyCommit);
       newProject.setMaxObjectSizeLimit(args.maxObjectSizeLimit);
       if (args.newParent != null) {
         newProject.setParentName(args.newParent);
diff --git a/java/com/google/gerrit/server/restapi/project/CreateTag.java b/java/com/google/gerrit/server/restapi/project/CreateTag.java
index 0b0ce10..f501faf 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateTag.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateTag.java
@@ -18,7 +18,6 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.api.projects.TagInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -36,9 +35,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.project.RefUtil;
 import com.google.gerrit.server.project.RefUtil.InvalidRevisionException;
 import com.google.inject.Inject;
@@ -71,7 +68,6 @@
   private final TagCache tagCache;
   private final GitReferenceUpdated referenceUpdated;
   private final WebLinks links;
-  private final ProjectControl.GenericFactory projectControlFactory;
   private String ref;
 
   @Inject
@@ -82,7 +78,6 @@
       TagCache tagCache,
       GitReferenceUpdated referenceUpdated,
       WebLinks webLinks,
-      ProjectControl.GenericFactory projectControlFactory,
       @Assisted String ref) {
     this.permissionBackend = permissionBackend;
     this.identifiedUser = identifiedUser;
@@ -90,7 +85,6 @@
     this.tagCache = tagCache;
     this.referenceUpdated = referenceUpdated;
     this.links = webLinks;
-    this.projectControlFactory = projectControlFactory;
     this.ref = ref;
   }
 
@@ -108,12 +102,6 @@
     }
 
     ref = RefUtil.normalizeTagRef(ref);
-
-    // TODO(hiesel): Remove dependency on RefControl
-    RefControl refControl =
-        projectControlFactory
-            .controlFor(resource.getNameKey(), resource.getUser())
-            .controlForRef(ref);
     PermissionBackend.ForRef perm =
         permissionBackend.user(identifiedUser).project(resource.getNameKey()).ref(ref);
 
@@ -126,7 +114,7 @@
       boolean isSigned = isAnnotated && input.message.contains("-----BEGIN PGP SIGNATURE-----\n");
       if (isSigned) {
         throw new MethodNotAllowedException("Cannot create signed tag \"" + ref + "\"");
-      } else if (isAnnotated && !refControl.canPerform(Permission.CREATE_TAG)) {
+      } else if (isAnnotated && !check(perm, RefPermission.CREATE_TAG)) {
         throw new AuthException("Cannot create annotated tag \"" + ref + "\"");
       } else {
         perm.check(RefPermission.CREATE);
@@ -159,7 +147,7 @@
             result.getObjectId(),
             identifiedUser.get().getAccount());
         try (RevWalk w = new RevWalk(repo)) {
-          return ListTags.createTagInfo(perm, result, w, resource.getNameKey(), links);
+          return ListTags.createTagInfo(perm, result, w, resource.getProjectState(), links);
         }
       }
     } catch (InvalidRevisionException e) {
@@ -169,4 +157,14 @@
       throw new IOException(e);
     }
   }
+
+  private static boolean check(PermissionBackend.ForRef perm, RefPermission permission)
+      throws PermissionBackendException {
+    try {
+      perm.check(permission);
+      return true;
+    } catch (AuthException e) {
+      return false;
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
index 09bbca9..3114f8a 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
@@ -57,6 +57,7 @@
   public Response<?> apply(BranchResource rsrc, Input input)
       throws RestApiException, OrmException, IOException, PermissionBackendException {
     permissionBackend.user(user).ref(rsrc.getBranchKey()).check(RefPermission.DELETE);
+    rsrc.getProjectState().checkStatePermitsWrite();
 
     if (!queryProvider.get().setLimit(1).byBranchOpen(rsrc.getBranchKey()).isEmpty()) {
       throw new ResourceConflictException("branch " + rsrc.getBranchKey() + " has open changes");
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index b1b575b..7bd1c4a 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -235,6 +235,10 @@
           "it doesn't exist or you do not have permission to delete it");
     }
 
+    if (!project.getProjectState().statePermitsWrite()) {
+      command.setResult(Result.REJECTED_OTHER_REASON, "project state does not permit write");
+    }
+
     if (!refName.startsWith(R_TAGS)) {
       Branch.NameKey branchKey = new Branch.NameKey(project.getNameKey(), ref.getName());
       if (!queryProvider.get().setLimit(1).byBranchOpen(branchKey).isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteTag.java b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
index cce7103..f432129 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteTag.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
@@ -56,6 +56,7 @@
         .project(resource.getNameKey())
         .ref(tag)
         .check(RefPermission.DELETE);
+    resource.getProjectState().checkStatePermitsWrite();
     deleteRefFactory.create(resource).ref(tag).delete();
     return Response.none();
   }
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 1568a4c..520d74c 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -251,8 +251,10 @@
     info.isOwner = toBoolean(canWriteConfig);
     info.canUpload =
         toBoolean(
-            canWriteConfig
-                || (canReadConfig && perm.ref(RefNames.REFS_CONFIG).testOrFalse(CREATE_CHANGE)));
+            projectState.statePermitsWrite()
+                && (canWriteConfig
+                    || (canReadConfig
+                        && perm.ref(RefNames.REFS_CONFIG).testOrFalse(CREATE_CHANGE))));
     info.canAdd = toBoolean(perm.testOrFalse(CREATE_REF));
     info.configVisible = canReadConfig || canWriteConfig;
 
diff --git a/java/com/google/gerrit/server/restapi/project/ListBranches.java b/java/com/google/gerrit/server/restapi/project/ListBranches.java
index 5675be1..416a5c2 100644
--- a/java/com/google/gerrit/server/restapi/project/ListBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/ListBranches.java
@@ -204,7 +204,11 @@
         branches.add(b);
 
         if (!Constants.HEAD.equals(ref.getName())) {
-          b.canDelete = perm.ref(ref.getName()).testOrFalse(RefPermission.DELETE) ? true : null;
+          b.canDelete =
+              perm.ref(ref.getName()).testOrFalse(RefPermission.DELETE)
+                      && rsrc.getProjectState().statePermitsWrite()
+                  ? true
+                  : null;
         }
         continue;
       }
@@ -248,7 +252,11 @@
     info.ref = ref.getName();
     info.revision = ref.getObjectId() != null ? ref.getObjectId().name() : null;
     info.canDelete =
-        !targets.contains(ref.getName()) && perm.testOrFalse(RefPermission.DELETE) ? true : null;
+        !targets.contains(ref.getName())
+                && perm.testOrFalse(RefPermission.DELETE)
+                && projectState.statePermitsWrite()
+            ? true
+            : null;
 
     BranchResource rsrc = new BranchResource(projectState, user, ref);
     for (UiAction.Description d : uiActions.from(branchViews, rsrc)) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index d2aecca..6eb5c88 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -21,7 +21,6 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
@@ -47,7 +46,6 @@
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectNode;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.restapi.group.GroupsCollection;
 import com.google.gerrit.server.util.RegexListSearcher;
@@ -65,16 +63,16 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Set;
+import java.util.Objects;
 import java.util.SortedMap;
 import java.util.SortedSet;
 import java.util.TreeMap;
 import java.util.TreeSet;
+import java.util.stream.Stream;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
@@ -516,31 +514,37 @@
 
   private Collection<Project.NameKey> filter(PermissionBackend.WithUser perm)
       throws BadRequestException, PermissionBackendException {
-    Collection<Project.NameKey> matches = Lists.newArrayList(scan());
+    Stream<Project.NameKey> matches = scan();
     if (type == FilterType.PARENT_CANDIDATES) {
       matches = parentsOf(matches);
     }
-    return perm.filter(ProjectPermission.ACCESS, matches).stream().sorted().collect(toList());
+    // TODO(dborowitz): Streamified PermissionBackend#filter.
+    return perm.filter(ProjectPermission.ACCESS, matches.collect(toList()))
+        .stream()
+        .sorted()
+        .collect(toList());
   }
 
-  private Collection<Project.NameKey> parentsOf(Collection<Project.NameKey> matches) {
-    Set<Project.NameKey> parents = new HashSet<>();
-    for (Project.NameKey p : matches) {
-      ProjectState ps = projectCache.get(p);
-      if (ps != null) {
-        Project.NameKey parent = ps.getProject().getParent();
-        if (parent != null) {
-          if (projectCache.get(parent) != null) {
-            parents.add(parent);
-          } else {
-            log.warn(
-                String.format(
-                    "parent project %s of project %s not found", parent.get(), ps.getName()));
-          }
-        }
-      }
-    }
-    return parents;
+  private Stream<Project.NameKey> parentsOf(Stream<Project.NameKey> matches) {
+    return matches
+        .map(
+            p -> {
+              ProjectState ps = projectCache.get(p);
+              if (ps != null) {
+                Project.NameKey parent = ps.getProject().getParent();
+                if (parent != null) {
+                  if (projectCache.get(parent) != null) {
+                    return parent;
+                  }
+                  log.warn(
+                      String.format(
+                          "parent project %s of project %s not found", parent.get(), ps.getName()));
+                }
+              }
+              return null;
+            })
+        .filter(Objects::nonNull)
+        .distinct();
   }
 
   private boolean isParentAccessible(
@@ -560,32 +564,28 @@
     return b;
   }
 
-  private Iterable<Project.NameKey> scan() throws BadRequestException {
+  private Stream<Project.NameKey> scan() throws BadRequestException {
     if (matchPrefix != null) {
       checkMatchOptions(matchSubstring == null && matchRegex == null);
-      return projectCache.byName(matchPrefix);
+      return projectCache.byName(matchPrefix).stream();
     } else if (matchSubstring != null) {
       checkMatchOptions(matchPrefix == null && matchRegex == null);
-      return Iterables.filter(
-          projectCache.all(),
-          p -> p.get().toLowerCase(Locale.US).contains(matchSubstring.toLowerCase(Locale.US)));
+      return projectCache
+          .all()
+          .stream()
+          .filter(
+              p -> p.get().toLowerCase(Locale.US).contains(matchSubstring.toLowerCase(Locale.US)));
     } else if (matchRegex != null) {
       checkMatchOptions(matchPrefix == null && matchSubstring == null);
       RegexListSearcher<Project.NameKey> searcher;
       try {
-        searcher =
-            new RegexListSearcher<Project.NameKey>(matchRegex) {
-              @Override
-              public String apply(Project.NameKey in) {
-                return in.get();
-              }
-            };
+        searcher = new RegexListSearcher<>(matchRegex, Project.NameKey::get);
       } catch (IllegalArgumentException e) {
         throw new BadRequestException(e.getMessage());
       }
       return searcher.search(ImmutableList.copyOf(projectCache.all()));
     } else {
-      return projectCache.all();
+      return projectCache.all().stream();
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index a2b7082..af4586c 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -140,7 +140,8 @@
           visibleTags(
               resource.getProjectState(), repo, repo.getRefDatabase().getRefs(Constants.R_TAGS));
       for (Ref ref : all.values()) {
-        tags.add(createTagInfo(perm.ref(ref.getName()), ref, rw, resource.getNameKey(), links));
+        tags.add(
+            createTagInfo(perm.ref(ref.getName()), ref, rw, resource.getProjectState(), links));
       }
     }
 
@@ -180,7 +181,7 @@
                 .ref(ref.getName()),
             ref,
             rw,
-            resource.getNameKey(),
+            resource.getProjectState(),
             links);
       }
     }
@@ -188,15 +189,12 @@
   }
 
   public static TagInfo createTagInfo(
-      PermissionBackend.ForRef perm,
-      Ref ref,
-      RevWalk rw,
-      Project.NameKey projectName,
-      WebLinks links)
+      PermissionBackend.ForRef perm, Ref ref, RevWalk rw, ProjectState projectState, WebLinks links)
       throws MissingObjectException, IOException {
     RevObject object = rw.parseAny(ref.getObjectId());
-    Boolean canDelete = perm.testOrFalse(RefPermission.DELETE) ? true : null;
-    List<WebLinkInfo> webLinks = links.getTagLinks(projectName.get(), ref.getName());
+    Boolean canDelete =
+        perm.testOrFalse(RefPermission.DELETE) && projectState.statePermitsWrite() ? true : null;
+    List<WebLinkInfo> webLinks = links.getTagLinks(projectState.getName(), ref.getName());
     if (object instanceof RevTag) {
       // Annotated or signed tag
       RevTag tag = (RevTag) object;
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java
index b74b640..67380dc 100644
--- a/java/com/google/gerrit/server/restapi/project/Module.java
+++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -100,5 +100,6 @@
     put(PROJECT_KIND, "config").to(PutConfig.class);
 
     factory(DeleteRef.Factory.class);
+    factory(ProjectNode.Factory.class);
   }
 }
diff --git a/java/com/google/gerrit/server/project/ProjectNode.java b/java/com/google/gerrit/server/restapi/project/ProjectNode.java
similarity index 87%
rename from java/com/google/gerrit/server/project/ProjectNode.java
rename to java/com/google/gerrit/server/restapi/project/ProjectNode.java
index e1ba692..54f7574 100644
--- a/java/com/google/gerrit/server/project/ProjectNode.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectNode.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.project;
+package com.google.gerrit.server.restapi.project;
 
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -23,8 +23,8 @@
 import java.util.TreeSet;
 
 /** Node of a Project in a tree formatted by {@link ListProjects}. */
-public class ProjectNode implements TreeNode, Comparable<ProjectNode> {
-  public interface Factory {
+class ProjectNode implements TreeNode, Comparable<ProjectNode> {
+  interface Factory {
     ProjectNode create(Project project, boolean isVisible);
   }
 
@@ -49,15 +49,15 @@
    *
    * @return Project parent name, {@code null} for the 'All-Projects' root project
    */
-  public Project.NameKey getParentName() {
+  Project.NameKey getParentName() {
     return project.getParent(allProjectsName);
   }
 
-  public boolean isAllProjects() {
+  boolean isAllProjects() {
     return allProjectsName.equals(project.getNameKey());
   }
 
-  public Project getProject() {
+  Project getProject() {
     return project;
   }
 
@@ -76,7 +76,7 @@
     return children;
   }
 
-  public void addChild(ProjectNode child) {
+  void addChild(ProjectNode child) {
     children.add(child);
   }
 
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
index 3fb9bb9..8cefd66 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
@@ -101,7 +101,9 @@
             if (pri.min != null) {
               r.setMin(pri.min);
             }
-            r.setAction(GetAccess.ACTION_TYPE.inverse().get(pri.action));
+            if (pri.action != null) {
+              r.setAction(GetAccess.ACTION_TYPE.inverse().get(pri.action));
+            }
             if (pri.force != null) {
               r.setForce(pri.force);
             }
diff --git a/java/com/google/gerrit/server/schema/SchemaCreator.java b/java/com/google/gerrit/server/schema/SchemaCreator.java
index 2004c98..895e75c 100644
--- a/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -240,7 +240,7 @@
 
     AccountGroup.NameKey groupName = groupUpdate.getName().orElseGet(groupCreation::getNameKey);
     GroupNameNotes groupNameNotes =
-        GroupNameNotes.loadForNewGroup(allUsersRepo, groupCreation.getGroupUUID(), groupName);
+        GroupNameNotes.forNewGroup(allUsersRepo, groupCreation.getGroupUUID(), groupName);
 
     commit(allUsersRepo, groupConfig, groupNameNotes);
 
diff --git a/java/com/google/gerrit/server/schema/Schema_139.java b/java/com/google/gerrit/server/schema/Schema_139.java
index 4dfc41a..f2c30df 100644
--- a/java/com/google/gerrit/server/schema/Schema_139.java
+++ b/java/com/google/gerrit/server/schema/Schema_139.java
@@ -1,16 +1,16 @@
-//Copyright (C) 2016 The Android Open Source Project
+// Copyright (C) 2016 The Android Open Source Project
 //
-//Licensed under the Apache License, Version 2.0 (the "License");
-//you may not use this file except in compliance with the License.
-//You may obtain a copy of the License at
+// 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
+// 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.
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
 
 package com.google.gerrit.server.schema;
 
@@ -24,7 +24,8 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.WatchConfig;
+import com.google.gerrit.server.account.AccountConfig;
+import com.google.gerrit.server.account.InternalAccountUpdate;
 import com.google.gerrit.server.account.WatchConfig.NotifyType;
 import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.config.AllUsersName;
@@ -147,10 +148,14 @@
           md.getCommitBuilder().setCommitter(serverUser);
           md.setMessage(MSG);
 
-          WatchConfig watchConfig = new WatchConfig(e.getKey());
-          watchConfig.load(md);
-          watchConfig.setProjectWatches(projectWatches);
-          watchConfig.commit(md);
+          AccountConfig accountConfig = new AccountConfig(e.getKey(), git);
+          accountConfig.load(md);
+          accountConfig.setAccountUpdate(
+              InternalAccountUpdate.builder()
+                  .deleteProjectWatches(accountConfig.getProjectWatches().keySet())
+                  .updateProjectWatches(projectWatches)
+                  .build());
+          accountConfig.commit(md);
         }
       }
       bru.execute(rw, NullProgressMonitor.INSTANCE);
diff --git a/java/com/google/gerrit/server/schema/Schema_147.java b/java/com/google/gerrit/server/schema/Schema_147.java
index 29ae7d5..fd85463 100644
--- a/java/com/google/gerrit/server/schema/Schema_147.java
+++ b/java/com/google/gerrit/server/schema/Schema_147.java
@@ -19,10 +19,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -34,25 +31,23 @@
 import java.util.HashSet;
 import java.util.Objects;
 import java.util.Set;
-import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 
 /** Delete user branches for which no account exists. */
 public class Schema_147 extends SchemaVersion {
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
-  private final PersonIdent serverIdent;
 
   @Inject
   Schema_147(
-      Provider<Schema_146> prior,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      @GerritPersonIdent PersonIdent serverIdent) {
+      Provider<Schema_146> prior, GitRepositoryManager repoManager, AllUsersName allUsersName) {
     super(prior);
     this.repoManager = repoManager;
     this.allUsersName = allUsersName;
-    this.serverIdent = serverIdent;
   }
 
   @Override
@@ -69,8 +64,7 @@
               .collect(toSet());
       accountIdsFromUserBranches.removeAll(accountIdsFromReviewDb);
       for (Account.Id accountId : accountIdsFromUserBranches) {
-        AccountsUpdate.deleteUserBranch(
-            repo, allUsersName, GitReferenceUpdated.DISABLED, null, serverIdent, accountId);
+        deleteUserBranch(repo, accountId);
       }
     } catch (IOException e) {
       throw new OrmException("Failed to delete user branches for non-existing accounts.", e);
@@ -87,4 +81,21 @@
       return ids;
     }
   }
+
+  private void deleteUserBranch(Repository allUsersRepo, Account.Id accountId) throws IOException {
+    String refName = RefNames.refsUsers(accountId);
+    Ref ref = allUsersRepo.exactRef(refName);
+    if (ref == null) {
+      return;
+    }
+
+    RefUpdate ru = allUsersRepo.updateRef(refName);
+    ru.setExpectedOldObjectId(ref.getObjectId());
+    ru.setNewObjectId(ObjectId.zeroId());
+    ru.setForceUpdate(true);
+    Result result = ru.delete();
+    if (result != Result.FORCED) {
+      throw new IOException(String.format("Failed to delete ref %s: %s", refName, result.name()));
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/schema/Schema_154.java b/java/com/google/gerrit/server/schema/Schema_154.java
index 8e05d38..0e5c110 100644
--- a/java/com/google/gerrit/server/schema/Schema_154.java
+++ b/java/com/google/gerrit/server/schema/Schema_154.java
@@ -139,10 +139,7 @@
     PersonIdent ident = serverIdent.get();
     md.getCommitBuilder().setAuthor(ident);
     md.getCommitBuilder().setCommitter(ident);
-    AccountConfig accountConfig = new AccountConfig(null, account.getId());
-    accountConfig.load(allUsersRepo);
-    accountConfig.setAccount(account);
-    accountConfig.commit(md);
+    new AccountConfig(account.getId(), allUsersRepo).load().setAccount(account).commit(md);
   }
 
   @FunctionalInterface
diff --git a/java/com/google/gerrit/server/util/RegexListSearcher.java b/java/com/google/gerrit/server/util/RegexListSearcher.java
index 91cb709..11543bb 100644
--- a/java/com/google/gerrit/server/util/RegexListSearcher.java
+++ b/java/com/google/gerrit/server/util/RegexListSearcher.java
@@ -16,9 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import com.google.common.base.Function;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Chars;
 import dk.brics.automaton.Automaton;
@@ -26,26 +23,26 @@
 import dk.brics.automaton.RunAutomaton;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Stream;
 
-/** Helper to search sorted lists for elements matching a regex. */
-public abstract class RegexListSearcher<T> implements Function<T, String> {
+/** Helper to search sorted lists for elements matching a {@link RegExp}. */
+public final class RegexListSearcher<T> {
   public static RegexListSearcher<String> ofStrings(String re) {
-    return new RegexListSearcher<String>(re) {
-      @Override
-      public String apply(String in) {
-        return in;
-      }
-    };
+    return new RegexListSearcher<>(re, in -> in);
   }
 
   private final RunAutomaton pattern;
+  private final Function<T, String> toStringFunc;
 
   private final String prefixBegin;
   private final String prefixEnd;
   private final int prefixLen;
   private final boolean prefixOnly;
 
-  public RegexListSearcher(String re) {
+  public RegexListSearcher(String re, Function<T, String> toStringFunc) {
+    this.toStringFunc = checkNotNull(toStringFunc);
+
     if (re.startsWith("^")) {
       re = re.substring(1);
     }
@@ -70,34 +67,34 @@
     pattern = prefixOnly ? null : new RunAutomaton(automaton);
   }
 
-  public Iterable<T> search(List<T> list) {
+  public Stream<T> search(List<T> list) {
     checkNotNull(list);
     int begin;
     int end;
 
     if (0 < prefixLen) {
-      // Assumes many consecutive elements may have the same prefix, so the cost
-      // of two binary searches is less than iterating to find the endpoints.
-      begin = find(list, prefixBegin);
-      end = find(list, prefixEnd);
+      // Assumes many consecutive elements may have the same prefix, so the cost of two binary
+      // searches is less than iterating linearly and running the regexp find the endpoints.
+      List<String> strings = Lists.transform(list, toStringFunc::apply);
+      begin = find(strings, prefixBegin);
+      end = find(strings, prefixEnd);
     } else {
       begin = 0;
       end = list.size();
     }
-
-    if (prefixOnly) {
-      return begin < end ? list.subList(begin, end) : ImmutableList.<T>of();
+    if (begin >= end) {
+      return Stream.empty();
     }
 
-    return Iterables.filter(list.subList(begin, end), x -> pattern.run(apply(x)));
+    Stream<T> result = list.subList(begin, end).stream();
+    if (!prefixOnly) {
+      result = result.filter(x -> pattern.run(toStringFunc.apply(x)));
+    }
+    return result;
   }
 
-  public boolean hasMatch(List<T> list) {
-    return !Iterables.isEmpty(search(list));
-  }
-
-  private int find(List<T> list, String p) {
-    int r = Collections.binarySearch(Lists.transform(list, this), p);
+  private static int find(List<String> list, String p) {
+    int r = Collections.binarySearch(list, p);
     return r < 0 ? -(r + 1) : r;
   }
 }
diff --git a/java/com/google/gerrit/sshd/BaseCommand.java b/java/com/google/gerrit/sshd/BaseCommand.java
index fa3a0f5..cabc21d 100644
--- a/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/java/com/google/gerrit/sshd/BaseCommand.java
@@ -370,6 +370,22 @@
     }
   }
 
+  protected String getTaskDescription() {
+    StringBuilder m = new StringBuilder();
+    m.append(context.getCommandLine());
+    return m.toString();
+  }
+
+  private String getTaskName() {
+    StringBuilder m = new StringBuilder();
+    m.append(getTaskDescription());
+    if (user.isIdentifiedUser()) {
+      IdentifiedUser u = user.asIdentifiedUser();
+      m.append(" (").append(u.getAccount().getUserName()).append(")");
+    }
+    return m.toString();
+  }
+
   private final class TaskThunk implements CancelableRunnable, ProjectRunnable {
     private final CommandRunnable thunk;
     private final String taskName;
@@ -377,14 +393,7 @@
 
     private TaskThunk(CommandRunnable thunk) {
       this.thunk = thunk;
-
-      StringBuilder m = new StringBuilder();
-      m.append(context.getCommandLine());
-      if (user.isIdentifiedUser()) {
-        IdentifiedUser u = user.asIdentifiedUser();
-        m.append(" (").append(u.getAccount().getUserName()).append(")");
-      }
-      this.taskName = m.toString();
+      this.taskName = getTaskName();
     }
 
     @Override
diff --git a/java/com/google/gerrit/sshd/HostKeyProvider.java b/java/com/google/gerrit/sshd/HostKeyProvider.java
index c0b6d5a..bffcfcd 100644
--- a/java/com/google/gerrit/sshd/HostKeyProvider.java
+++ b/java/com/google/gerrit/sshd/HostKeyProvider.java
@@ -39,7 +39,6 @@
   public KeyPairProvider get() {
     Path objKey = site.ssh_key;
     Path rsaKey = site.ssh_rsa;
-    Path dsaKey = site.ssh_dsa;
     Path ecdsaKey_256 = site.ssh_ecdsa_256;
     Path ecdsaKey_384 = site.ssh_ecdsa_384;
     Path ecdsaKey_521 = site.ssh_ecdsa_521;
@@ -49,9 +48,6 @@
     if (Files.exists(rsaKey)) {
       stdKeys.add(rsaKey.toAbsolutePath().toFile());
     }
-    if (Files.exists(dsaKey)) {
-      stdKeys.add(dsaKey.toAbsolutePath().toFile());
-    }
     if (Files.exists(ecdsaKey_256)) {
       stdKeys.add(ecdsaKey_256.toAbsolutePath().toFile());
     }
diff --git a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index d6ecb0a..2051a00 100644
--- a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -100,6 +100,9 @@
   @Option(name = "--change-id", usage = "if change-id is required")
   private InheritableBoolean requireChangeID = InheritableBoolean.INHERIT;
 
+  @Option(name = "--reject-empty-commit", usage = "if empty commits should be rejected on submit")
+  private InheritableBoolean rejectEmptyCommit = InheritableBoolean.INHERIT;
+
   @Option(
     name = "--new-change-for-all-not-in-target",
     usage = "if a new change will be created for every commit not in target branch"
@@ -201,6 +204,7 @@
         input.branches = branch;
         input.createEmptyCommit = createEmptyCommit;
         input.maxObjectSizeLimit = maxObjectSizeLimit;
+        input.rejectEmptyCommit = rejectEmptyCommit;
         if (pluginConfigValues != null) {
           input.pluginConfigValues = parsePluginConfigValues(pluginConfigValues);
         }
diff --git a/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 1d764b9..1be32a8 100644
--- a/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -240,6 +240,11 @@
     }
   }
 
+  @Override
+  protected String getTaskDescription() {
+    return "gerrit review";
+  }
+
   private void applyReview(PatchSet patchSet, ReviewInput review) throws RestApiException {
     gApi.changes()
         .id(patchSet.getId().getParentKey().get())
diff --git a/java/com/google/gerrit/sshd/commands/UploadArchive.java b/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 7518cbb..95dbb40 100644
--- a/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -140,7 +140,7 @@
         break;
       }
       if (!s.startsWith(argCmd)) {
-        throw new Failure(1, "fatal: 'argument' token or flush expected");
+        throw new Failure(1, "fatal: 'argument' token or flush expected, got " + s);
       }
       String[] parts = s.substring(argCmd.length()).split("=", 2);
       for (String p : parts) {
@@ -173,18 +173,18 @@
 
       ArchiveFormat f = allowedFormats.getExtensions().get("." + options.format);
       if (f == null) {
-        throw new Failure(3, "fatal: upload-archive not permitted");
+        throw new Failure(3, "fatal: upload-archive not permitted for format " + options.format);
       }
 
       // Find out the object to get from the specified reference and paths
       ObjectId treeId = repo.resolve(options.treeIsh);
       if (treeId == null) {
-        throw new Failure(4, "fatal: reference not found");
+        throw new Failure(4, "fatal: reference not found: " + options.treeIsh);
       }
 
       // Verify the user has permissions to read the specified tree.
       if (!canRead(treeId)) {
-        throw new Failure(5, "fatal: cannot perform upload-archive operation");
+        throw new Failure(5, "fatal: no permission to read tree" + options.treeIsh);
       }
 
       // The archive is sent in DATA sideband channel
diff --git a/java/com/google/gerrit/testing/FakeAccountCache.java b/java/com/google/gerrit/testing/FakeAccountCache.java
index 8256765..7668912 100644
--- a/java/com/google/gerrit/testing/FakeAccountCache.java
+++ b/java/com/google/gerrit/testing/FakeAccountCache.java
@@ -17,6 +17,7 @@
 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,6 +80,7 @@
         new AllUsersName(AllUsersNameProvider.DEFAULT),
         account,
         ImmutableSet.of(),
-        new HashMap<>());
+        new HashMap<>(),
+        GeneralPreferencesInfo.defaults());
   }
 }
diff --git a/java/com/google/gerrit/truth/ListSubject.java b/java/com/google/gerrit/truth/ListSubject.java
index bcd8dcf..cccf51b 100644
--- a/java/com/google/gerrit/truth/ListSubject.java
+++ b/java/com/google/gerrit/truth/ListSubject.java
@@ -30,8 +30,7 @@
   @SuppressWarnings("unchecked")
   public static <S extends Subject<S, E>, E> ListSubject<S, E> assertThat(
       List<E> list, Function<E, S> elementAssertThatFunction) {
-    // The ListSubjectFactory always returns ListSubjects.
-    // -> Casting is appropriate.
+    // The ListSubjectFactory always returns ListSubjects. -> Casting is appropriate.
     return (ListSubject<S, E>)
         assertAbout(new ListSubjectFactory<>(elementAssertThatFunction)).that(list);
   }
@@ -44,11 +43,8 @@
 
   public S element(int index) {
     checkArgument(index >= 0, "index(%s) must be >= 0", index);
-    // The constructor only accepts lists.
-    // -> Casting is appropriate.
-    @SuppressWarnings("unchecked")
-    List<E> list = (List<E>) actual();
     isNotNull();
+    List<E> list = getActualList();
     if (index >= list.size()) {
       fail("has an element at index " + index);
     }
@@ -61,11 +57,23 @@
     return element(0);
   }
 
+  public S lastElement() {
+    isNotNull();
+    isNotEmpty();
+    List<E> list = getActualList();
+    return element(list.size() - 1);
+  }
+
+  @SuppressWarnings("unchecked")
+  private List<E> getActualList() {
+    // The constructor only accepts lists. -> Casting is appropriate.
+    return (List<E>) actual();
+  }
+
   @SuppressWarnings("unchecked")
   @Override
   public ListSubject<S, E> named(String s, Object... objects) {
-    // This object is returned which is of type ListSubject.
-    // -> Casting is appropriate.
+    // This object is returned which is of type ListSubject. -> Casting is appropriate.
     return (ListSubject<S, E>) super.named(s, objects);
   }
 
@@ -81,8 +89,7 @@
     @SuppressWarnings("unchecked")
     @Override
     public ListSubject<S, T> createSubject(FailureMetadata failureMetadata, Iterable<?> objects) {
-      // The constructor of ListSubject only accepts lists.
-      // -> Casting is appropriate.
+      // The constructor of ListSubject only accepts lists. -> Casting is appropriate.
       return new ListSubject<>(failureMetadata, (List<T>) objects, elementAssertThatFunction);
     }
   }
diff --git a/java/gerrit/PRED_project_default_submit_type_1.java b/java/gerrit/PRED_project_default_submit_type_1.java
index 91db57e..d70a9e4 100644
--- a/java/gerrit/PRED_project_default_submit_type_1.java
+++ b/java/gerrit/PRED_project_default_submit_type_1.java
@@ -47,7 +47,7 @@
     Term a1 = arg1.dereference();
 
     ProjectState projectState = StoredValues.PROJECT_STATE.get(engine);
-    SubmitType submitType = projectState.getProject().getSubmitType();
+    SubmitType submitType = projectState.getSubmitType();
     if (!a1.unify(term[submitType.ordinal()], engine.trail)) {
       return engine.fail();
     }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 4b34ade..4b1b5d0 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -104,7 +104,6 @@
 import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.index.account.StalenessChecker;
 import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
 import com.google.gerrit.server.project.RefPattern;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
@@ -191,8 +190,6 @@
 
   @Inject private AccountIndexer accountIndexer;
 
-  @Inject private OutgoingEmailValidator emailValidator;
-
   @Inject private GitReferenceUpdated gitReferenceUpdated;
 
   @Inject private RetryHelper.Metrics retryMetrics;
@@ -716,14 +713,24 @@
   }
 
   @Test
-  public void getEmailsOfOtherAccount() throws Exception {
+  public void cannotGetEmailsOfOtherAccountWithoutModifyAccount() throws Exception {
     String email = "preferred2@example.com";
-    String secondaryEmail = "secondary2@example.com";
+    TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
+
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage("modify account not permitted");
+    gApi.accounts().id(foo.id.get()).getEmails();
+  }
+
+  @Test
+  public void getEmailsOfOtherAccount() throws Exception {
+    String email = "preferred3@example.com";
+    String secondaryEmail = "secondary3@example.com";
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
     EmailInput input = newEmailInput(secondaryEmail);
     gApi.accounts().id(foo.id.hashCode()).addEmail(input);
 
-    setApiUser(user);
     assertThat(
             gApi.accounts()
                 .id(foo.id.get())
@@ -1338,7 +1345,7 @@
             WatchConfig.WATCH_CONFIG,
             wc.toText());
     PushOneCommit.Result r = push.to(RefNames.REFS_USERS_SELF);
-    r.assertErrorStatus("invalid watch configuration");
+    r.assertErrorStatus("invalid account configuration");
     r.assertMessage(
         String.format(
             "%s: Invalid project watch of account %d for project %s: %s",
@@ -1928,7 +1935,8 @@
   @Test
   public void checkMetaId() throws Exception {
     // metaId is set when account is loaded
-    assertThat(accounts.get(admin.getId()).getMetaId()).isEqualTo(getMetaId(admin.getId()));
+    assertThat(accounts.get(admin.getId()).getAccount().getMetaId())
+        .isEqualTo(getMetaId(admin.getId()));
 
     // metaId is set when account is created
     AccountsUpdate au = accountsUpdate.create();
@@ -2013,7 +2021,6 @@
             gitReferenceUpdated,
             null,
             allUsers,
-            emailValidator,
             metaDataUpdateInternalFactory,
             new RetryHelper(
                 cfg,
@@ -2062,7 +2069,6 @@
             gitReferenceUpdated,
             null,
             allUsers,
-            emailValidator,
             metaDataUpdateInternalFactory,
             new RetryHelper(
                 cfg,
@@ -2101,7 +2107,7 @@
     }
     assertThat(bgCounter.get()).isEqualTo(status.size());
 
-    Account updatedAccount = accounts.get(admin.id);
+    Account updatedAccount = accounts.get(admin.id).getAccount();
     assertThat(updatedAccount.getStatus()).isEqualTo(Iterables.getLast(status));
     assertThat(updatedAccount.getFullName()).isEqualTo(admin.fullName);
 
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
index 40bb08a..ba340eb 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
@@ -16,51 +16,16 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.AssertUtil.assertPrefs;
-import static com.google.gerrit.acceptance.GitUtil.fetch;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.client.Theme;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.account.VersionedAccountPreferences;
-import org.eclipse.jgit.api.errors.TransportException;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.junit.TestRepository;
-import org.junit.After;
 import org.junit.Test;
 
 @NoHttpd
 public class DiffPreferencesIT extends AbstractDaemonTest {
-  @After
-  public void cleanUp() throws Exception {
-    gApi.accounts().id(admin.getId().toString()).setDiffPreferences(DiffPreferencesInfo.defaults());
-
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    try {
-      fetch(allUsersRepo, RefNames.REFS_USERS_DEFAULT + ":defaults");
-    } catch (TransportException e) {
-      if (e.getMessage()
-          .equals(
-              "Remote does not have " + RefNames.REFS_USERS_DEFAULT + " available for fetch.")) {
-        return;
-      }
-      throw e;
-    }
-    allUsersRepo.reset("defaults");
-    PushOneCommit push =
-        pushFactory.create(
-            db,
-            admin.getIdent(),
-            allUsersRepo,
-            "Delete default preferences",
-            VersionedAccountPreferences.PREFERENCES,
-            "");
-    push.rm(RefNames.REFS_USERS_DEFAULT).assertOkStatus();
-  }
-
   @Test
   public void getDiffPreferences() throws Exception {
     DiffPreferencesInfo d = DiffPreferencesInfo.defaults();
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index 3cc040c..946e15c 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -31,12 +31,8 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.TimeFormat;
 import com.google.gerrit.extensions.client.MenuItem;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.reviewdb.client.RefNames;
 import java.util.ArrayList;
 import java.util.HashMap;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -50,20 +46,6 @@
     user42 = accountCreator.create(name, name + "@example.com", "User 42");
   }
 
-  @After
-  public void cleanUp() throws Exception {
-    gApi.accounts().id(user42.getId().toString()).setPreferences(GeneralPreferencesInfo.defaults());
-
-    try (Repository git = repoManager.openRepository(allUsers)) {
-      if (git.exactRef(RefNames.REFS_USERS_DEFAULT) != null) {
-        RefUpdate u = git.updateRef(RefNames.REFS_USERS_DEFAULT);
-        u.setForceUpdate(true);
-        assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED);
-      }
-    }
-    accountCache.evictAllNoReindex();
-  }
-
   @Test
   public void getAndSetPreferences() throws Exception {
     GeneralPreferencesInfo o = gApi.accounts().id(user42.id.toString()).getPreferences();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index fa683cf..b770064 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -91,6 +91,7 @@
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.Comment.Range;
 import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -237,6 +238,15 @@
   }
 
   @Test
+  public void skipMergeable() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String triplet = project.get() + "~master~" + r.getChangeId();
+    ChangeInfo c =
+        gApi.changes().id(triplet).get(ImmutableList.of(ListChangesOption.SKIP_MERGEABLE));
+    assertThat(c.mergeable).isNull();
+  }
+
+  @Test
   public void setPrivateByOwner() throws Exception {
     TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
     PushOneCommit.Result result =
diff --git a/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java b/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
new file mode 100644
index 0000000..287434d
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
@@ -0,0 +1,118 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+public class DisablePrivateChangesIT extends AbstractDaemonTest {
+
+  @Test
+  @GerritConfig(name = "change.disablePrivateChanges", value = "true")
+  public void createPrivateChangeWithDisablePrivateChangesTrue() throws Exception {
+    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
+    input.isPrivate = true;
+    exception.expect(MethodNotAllowedException.class);
+    exception.expectMessage("private changes are disabled");
+    gApi.changes().create(input);
+  }
+
+  @Test
+  @GerritConfig(name = "change.disablePrivateChanges", value = "true")
+  public void createNonPrivateChangeWithDisablePrivateChangesTrue() throws Exception {
+    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
+    assertThat(gApi.changes().create(input).get().isPrivate).isNull();
+  }
+
+  @Test
+  public void createPrivateChangeWithDisablePrivateChangesFalse() throws Exception {
+    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
+    input.isPrivate = true;
+    assertThat(gApi.changes().create(input).get().isPrivate).isTrue();
+  }
+
+  @Test
+  @GerritConfig(name = "change.disablePrivateChanges", value = "true")
+  public void pushPrivatesWithDisablePrivateChangesTrue() throws Exception {
+    PushOneCommit.Result result =
+        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%private");
+    result.assertErrorStatus();
+  }
+
+  @Test
+  @GerritConfig(name = "change.disablePrivateChanges", value = "true")
+  public void pushDraftsWithDisablePrivateChangesTrue() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result result =
+        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%draft");
+    result.assertErrorStatus();
+
+    testRepo.reset(initialHead);
+    result = pushFactory.create(db, admin.getIdent(), testRepo).to("refs/drafts/master");
+    result.assertErrorStatus();
+  }
+
+  @Test
+  @GerritConfig(name = "change.disablePrivateChanges", value = "true")
+  public void pushWithDisablePrivateChangesTrue() throws Exception {
+    PushOneCommit.Result result =
+        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master");
+    result.assertOkStatus();
+    assertThat(result.getChange().change().isPrivate()).isFalse();
+  }
+
+  @Test
+  public void pushPrivatesWithDisablePrivateChangesFalse() throws Exception {
+    PushOneCommit.Result result =
+        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%private");
+    assertThat(result.getChange().change().isPrivate()).isTrue();
+  }
+
+  @Test
+  public void pushDraftsWithDisablePrivateChangesFalse() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result result =
+        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%draft");
+    assertThat(result.getChange().change().isPrivate()).isTrue();
+
+    testRepo.reset(initialHead);
+    result = pushFactory.create(db, admin.getIdent(), testRepo).to("refs/drafts/master");
+    assertThat(result.getChange().change().isPrivate()).isTrue();
+  }
+
+  @Test
+  @GerritConfig(name = "change.disablePrivateChanges", value = "true")
+  public void setPrivateWithDisablePrivateChangesTrue() throws Exception {
+    PushOneCommit.Result result = createChange();
+
+    exception.expect(MethodNotAllowedException.class);
+    exception.expectMessage("private changes are disabled");
+    gApi.changes().id(result.getChangeId()).setPrivate(true, "set private");
+  }
+
+  @Test
+  public void setPrivateWithDisablePrivateChangesFalse() throws Exception {
+    PushOneCommit.Result result = createChange();
+    gApi.changes().id(result.getChangeId()).setPrivate(true, "set private");
+    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java
index 23041de..c606982 100644
--- a/javatests/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java
@@ -20,30 +20,14 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
-import com.google.gerrit.reviewdb.client.RefNames;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.junit.After;
 import org.junit.Test;
 
 @NoHttpd
 public class GeneralPreferencesIT extends AbstractDaemonTest {
-  @After
-  public void cleanUp() throws Exception {
-    try (Repository git = repoManager.openRepository(allUsers)) {
-      if (git.exactRef(RefNames.REFS_USERS_DEFAULT) != null) {
-        RefUpdate u = git.updateRef(RefNames.REFS_USERS_DEFAULT);
-        u.setForceUpdate(true);
-        assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED);
-      }
-    }
-    accountCache.evictAllNoReindex();
-  }
-
   @Test
   public void getGeneralPreferences() throws Exception {
     GeneralPreferencesInfo result = gApi.config().server().getDefaultPreferences();
-    assertPrefs(result, GeneralPreferencesInfo.defaults(), "my");
+    assertPrefs(result, GeneralPreferencesInfo.defaults(), "changeTable", "my");
   }
 
   @Test
@@ -57,6 +41,6 @@
     result = gApi.config().server().getDefaultPreferences();
     GeneralPreferencesInfo expected = GeneralPreferencesInfo.defaults();
     expected.signedOffBy = newSignedOffBy;
-    assertPrefs(result, expected, "my");
+    assertPrefs(result, expected, "changeTable", "my");
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index aba5c7d..c986c5e 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -219,13 +219,13 @@
     RevCommit initialHead = getRemoteHead(project, RefNames.REFS_CONFIG);
 
     ConfigInfo info = gApi.projects().name(project.get()).config();
-    assertThat(info.submitType).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+    assertThat(info.defaultSubmitType.value).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
     ConfigInput input = new ConfigInput();
     input.submitType = SubmitType.CHERRY_PICK;
     info = gApi.projects().name(project.get()).config(input);
-    assertThat(info.submitType).isEqualTo(SubmitType.CHERRY_PICK);
+    assertThat(info.defaultSubmitType.value).isEqualTo(SubmitType.CHERRY_PICK);
     info = gApi.projects().name(project.get()).config();
-    assertThat(info.submitType).isEqualTo(SubmitType.CHERRY_PICK);
+    assertThat(info.defaultSubmitType.value).isEqualTo(SubmitType.CHERRY_PICK);
 
     RevCommit updatedHead = getRemoteHead(project, RefNames.REFS_CONFIG);
     eventRecorder.assertRefUpdatedEvents(
@@ -233,6 +233,7 @@
   }
 
   @Test
+  @SuppressWarnings("deprecation")
   public void setConfig() throws Exception {
     ConfigInput input = createTestConfigInput();
     ConfigInfo info = gApi.projects().name(project.get()).config(input);
@@ -250,9 +251,13 @@
         .isEqualTo(input.createNewChangeForAllNotInTarget);
     assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo(input.maxObjectSizeLimit);
     assertThat(info.submitType).isEqualTo(input.submitType);
+    assertThat(info.defaultSubmitType.value).isEqualTo(input.submitType);
+    assertThat(info.defaultSubmitType.inheritedValue).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+    assertThat(info.defaultSubmitType.configuredValue).isEqualTo(input.submitType);
     assertThat(info.state).isEqualTo(input.state);
   }
 
+  @SuppressWarnings("deprecation")
   @Test
   public void setPartialConfig() throws Exception {
     ConfigInput input = createTestConfigInput();
@@ -276,6 +281,9 @@
         .isEqualTo(input.createNewChangeForAllNotInTarget);
     assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo(input.maxObjectSizeLimit);
     assertThat(info.submitType).isEqualTo(input.submitType);
+    assertThat(info.defaultSubmitType.value).isEqualTo(input.submitType);
+    assertThat(info.defaultSubmitType.inheritedValue).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+    assertThat(info.defaultSubmitType.configuredValue).isEqualTo(input.submitType);
     assertThat(info.state).isEqualTo(input.state);
   }
 
@@ -323,6 +331,24 @@
     gApi.projects().name(project.get()).head("test");
   }
 
+  @Test
+  public void nonActiveProjectCanBeMadeActive() throws Exception {
+    for (ProjectState nonActiveState :
+        ImmutableList.of(ProjectState.READ_ONLY, ProjectState.HIDDEN)) {
+      // ACTIVE => NON_ACTIVE
+      ConfigInput ci1 = new ConfigInput();
+      ci1.state = nonActiveState;
+      gApi.projects().name(project.get()).config(ci1);
+      assertThat(gApi.projects().name(project.get()).config().state).isEqualTo(nonActiveState);
+      // NON_ACTIVE => ACTIVE
+      ConfigInput ci2 = new ConfigInput();
+      ci2.state = ProjectState.ACTIVE;
+      gApi.projects().name(project.get()).config(ci2);
+      // ACTIVE is represented as null in the API
+      assertThat(gApi.projects().name(project.get()).config().state).isNull();
+    }
+  }
+
   private ConfigInput createTestConfigInput() {
     ConfigInput input = new ConfigInput();
     input.description = "some description";
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index df274ee..f7ca2f2 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -75,9 +75,11 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.git.receive.ReceiveConstants;
+import com.google.gerrit.server.git.validators.CommitValidators.ChangeIdValidator;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.project.testing.Util;
@@ -1321,6 +1323,28 @@
   }
 
   @Test
+  public void testPushWithChangedChangeId() throws Exception {
+    PushOneCommit.Result r = pushTo("refs/for/master");
+    r.assertOkStatus();
+    PushOneCommit push =
+        pushFactory.create(
+            db,
+            admin.getIdent(),
+            testRepo,
+            PushOneCommit.SUBJECT
+                + "\n\n"
+                + "Change-Id: I55eab7c7a76e95005fa9cc469aa8f9fc16da9eba\n",
+            "b.txt",
+            "anotherContent",
+            r.getChangeId());
+    r = push.to("refs/changes/" + r.getChange().change().getId().get());
+    r.assertErrorStatus(
+        String.format(
+            ChangeIdValidator.CHANGE_ID_MISMATCH_MSG,
+            r.getCommit().abbreviate(RevId.ABBREV_LEN).name()));
+  }
+
+  @Test
   public void pushWithMultipleChangeIds() throws Exception {
     testPushWithMultipleChangeIds();
   }
@@ -1923,8 +1947,8 @@
 
     assertThat(info1.status).isEqualTo(ChangeStatus.NEW);
     assertThat(info2.status).isEqualTo(ChangeStatus.NEW);
-    assertThat(info1.isPrivate).isEqualTo(true);
-    assertThat(info2.isPrivate).isEqualTo(true);
+    assertThat(info1.isPrivate).isTrue();
+    assertThat(info2.isPrivate).isTrue();
     assertThat(info1.revisions).hasSize(1);
     assertThat(info2.revisions).hasSize(1);
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index 9ccc138..2aa8d58 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -26,7 +26,6 @@
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
 
-import com.github.rholder.retry.StopStrategies;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
@@ -41,20 +40,15 @@
 import com.google.gerrit.extensions.common.AccountExternalIdInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountsUpdate;
-import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.ExternalIdReader;
 import com.google.gerrit.server.account.externalids.ExternalIds;
-import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.update.RetryHelper;
 import com.google.gson.reflect.TypeToken;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.inject.Inject;
@@ -67,8 +61,6 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.api.errors.TransportException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -92,8 +84,6 @@
   @Inject private AccountsUpdate.Server accountsUpdate;
   @Inject private ExternalIds externalIds;
   @Inject private ExternalIdReader externalIdReader;
-  @Inject private MetricMaker metricMaker;
-  @Inject private RetryHelper.Metrics retryMetrics;
   @Inject private ExternalIdNotes.Factory externalIdNotesFactory;
 
   @Test
@@ -715,91 +705,6 @@
   }
 
   @Test
-  public void retryOnLockFailure() throws Exception {
-    ExternalId.Key fooId = ExternalId.Key.create("foo", "foo");
-    ExternalId.Key barId = ExternalId.Key.create("bar", "bar");
-
-    final AtomicBoolean doneBgUpdate = new AtomicBoolean(false);
-    ExternalIdsUpdate update =
-        new ExternalIdsUpdate(
-            repoManager,
-            () -> metaDataUpdateFactory.create(allUsers),
-            accountCache,
-            allUsers,
-            metricMaker,
-            externalIds,
-            new DisabledExternalIdCache(),
-            new RetryHelper(
-                cfg,
-                retryMetrics,
-                null,
-                null,
-                null,
-                r -> r.withBlockStrategy(noSleepBlockStrategy)),
-            () -> {
-              if (!doneBgUpdate.getAndSet(true)) {
-                try {
-                  insertExtId(ExternalId.create(barId, admin.id));
-                } catch (Exception e) {
-                  // Ignore, the successful insertion of the external ID is asserted later
-                }
-              }
-            });
-    assertThat(doneBgUpdate.get()).isFalse();
-    update.insert(ExternalId.create(fooId, admin.id));
-    assertThat(doneBgUpdate.get()).isTrue();
-
-    assertThat(externalIds.get(fooId)).isNotNull();
-    assertThat(externalIds.get(barId)).isNotNull();
-  }
-
-  @Test
-  public void failAfterRetryerGivesUp() throws Exception {
-    ExternalId.Key[] extIdsKeys = {
-      ExternalId.Key.create("foo", "foo"),
-      ExternalId.Key.create("bar", "bar"),
-      ExternalId.Key.create("baz", "baz")
-    };
-    final AtomicInteger bgCounter = new AtomicInteger(0);
-    ExternalIdsUpdate update =
-        new ExternalIdsUpdate(
-            repoManager,
-            () -> metaDataUpdateFactory.create(allUsers),
-            accountCache,
-            allUsers,
-            metricMaker,
-            externalIds,
-            new DisabledExternalIdCache(),
-            new RetryHelper(
-                cfg,
-                retryMetrics,
-                null,
-                null,
-                null,
-                r ->
-                    r.withStopStrategy(StopStrategies.stopAfterAttempt(extIdsKeys.length))
-                        .withBlockStrategy(noSleepBlockStrategy)),
-            () -> {
-              try {
-                insertExtId(ExternalId.create(extIdsKeys[bgCounter.getAndAdd(1)], admin.id));
-              } catch (Exception e) {
-                // Ignore, the successful insertion of the external ID is asserted later
-              }
-            });
-    assertThat(bgCounter.get()).isEqualTo(0);
-    try {
-      update.insert(ExternalId.create(ExternalId.Key.create("abc", "abc"), admin.id));
-      fail("expected LockFailureException");
-    } catch (LockFailureException e) {
-      // Ignore, expected
-    }
-    assertThat(bgCounter.get()).isEqualTo(extIdsKeys.length);
-    for (ExternalId.Key extIdKey : extIdsKeys) {
-      assertThat(externalIds.get(extIdKey)).isNotNull();
-    }
-  }
-
-  @Test
   public void readExternalIdWithAccountIdThatCanBeExpressedInKiB() throws Exception {
     ExternalId.Key extIdKey = ExternalId.Key.parse("foo:bar");
     Account.Id accountId = new Account.Id(1024 * 100);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 67307e2..e45f271 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
@@ -47,6 +48,7 @@
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
@@ -236,6 +238,7 @@
           break;
         case MERGE_ALWAYS:
         case MERGE_IF_NECESSARY:
+        case INHERIT:
           assertThat(e.getMessage())
               .isEqualTo(
                   "Failed to submit 3 changes due to the following problems:\n"
@@ -988,6 +991,78 @@
     assertAuthorAndCommitDateEquals(getRemoteHead());
   }
 
+  @Test
+  @TestProjectInput(rejectEmptyCommit = InheritableBoolean.FALSE)
+  public void submitEmptyCommitPatchSetCanNotFastForward_emptyCommitAllowed() throws Exception {
+    assume().that(getSubmitType()).isNotEqualTo(SubmitType.FAST_FORWARD_ONLY);
+
+    PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    ChangeApi revert1 = gApi.changes().id(change.getChangeId()).revert();
+    approve(revert1.id());
+    revert1.current().submit();
+
+    ChangeApi revert2 = gApi.changes().id(change.getChangeId()).revert();
+    approve(revert2.id());
+    revert2.current().submit();
+  }
+
+  @Test
+  @TestProjectInput(rejectEmptyCommit = InheritableBoolean.TRUE)
+  public void submitEmptyCommitPatchSetCanNotFastForward_emptyCommitNotAllowed() throws Exception {
+    assume().that(getSubmitType()).isNotEqualTo(SubmitType.FAST_FORWARD_ONLY);
+
+    PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    ChangeApi revert1 = gApi.changes().id(change.getChangeId()).revert();
+    approve(revert1.id());
+    revert1.current().submit();
+
+    ChangeApi revert2 = gApi.changes().id(change.getChangeId()).revert();
+    approve(revert2.id());
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage(
+        "Change "
+            + revert2.get()._number
+            + ": Change could not be merged because the commit is empty. "
+            + "Project policy requires all commits to contain modifications to at least one file.");
+    revert2.current().submit();
+  }
+
+  @Test
+  @TestProjectInput(rejectEmptyCommit = InheritableBoolean.FALSE)
+  public void submitEmptyCommitPatchSetCanFastForward_emptyCommitAllowed() throws Exception {
+    ChangeInput ci = new ChangeInput();
+    ci.subject = "Empty change";
+    ci.project = project.get();
+    ci.branch = "master";
+    ChangeApi change = gApi.changes().create(ci);
+    approve(change.id());
+    change.current().submit();
+  }
+
+  @Test
+  @TestProjectInput(rejectEmptyCommit = InheritableBoolean.TRUE)
+  public void submitEmptyCommitPatchSetCanFastForward_emptyCommitNotAllowed() throws Exception {
+    ChangeInput ci = new ChangeInput();
+    ci.subject = "Empty change";
+    ci.project = project.get();
+    ci.branch = "master";
+    ChangeApi change = gApi.changes().create(ci);
+    approve(change.id());
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage(
+        "Change "
+            + change.get()._number
+            + ": Change could not be merged because the commit is empty. "
+            + "Project policy requires all commits to contain modifications to at least one file.");
+    change.current().submit();
+  }
+
   private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Exception {
     for (PushOneCommit.Result change : changes) {
       try (BatchUpdate bu =
@@ -1084,9 +1159,7 @@
   }
 
   protected void assertSubmittable(String changeId) throws Exception {
-    assertThat(get(changeId, SUBMITTABLE).submittable)
-        .named("submit bit on ChangeInfo")
-        .isEqualTo(true);
+    assertThat(get(changeId, SUBMITTABLE).submittable).named("submit bit on ChangeInfo").isTrue();
     RevisionResource rsrc = parseCurrentRevisionResource(changeId);
     UiAction.Description desc = submitHandler.getDescription(rsrc);
     assertThat(desc.isVisible()).named("visible bit on submit action").isTrue();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
index a10062c..0ece00a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
@@ -17,14 +17,17 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.reviewdb.client.Project;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -43,7 +46,7 @@
   public void createChangeWithPrivateByDefaultEnabled() throws Exception {
     setPrivateByDefault(project2, InheritableBoolean.TRUE);
     ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
-    assertThat(gApi.changes().create(input).get().isPrivate).isEqualTo(true);
+    assertThat(gApi.changes().create(input).get().isPrivate).isTrue();
   }
 
   @Test
@@ -70,9 +73,20 @@
   }
 
   @Test
+  @GerritConfig(name = "change.disablePrivateChanges", value = "true")
+  public void createChangeWithPrivateByDefaultAndDisablePrivateChangesTrue() throws Exception {
+    setPrivateByDefault(project2, InheritableBoolean.TRUE);
+
+    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+    exception.expect(MethodNotAllowedException.class);
+    exception.expectMessage("private changes are disabled");
+    gApi.changes().create(input);
+  }
+
+  @Test
   public void pushWithPrivateByDefaultEnabled() throws Exception {
     setPrivateByDefault(project2, InheritableBoolean.TRUE);
-    assertThat(createChange(project2).getChange().change().isPrivate()).isEqualTo(true);
+    assertThat(createChange(project2).getChange().change().isPrivate()).isTrue();
   }
 
   @Test
@@ -83,18 +97,45 @@
                 .getChange()
                 .change()
                 .isPrivate())
-        .isEqualTo(false);
+        .isFalse();
   }
 
   @Test
   public void pushWithPrivateByDefaultDisabled() throws Exception {
-    assertThat(createChange(project2).getChange().change().isPrivate()).isEqualTo(false);
+    assertThat(createChange(project2).getChange().change().isPrivate()).isFalse();
   }
 
   @Test
   public void pushBypassPrivateByDefaultInherited() throws Exception {
     setPrivateByDefault(project1, InheritableBoolean.TRUE);
-    assertThat(createChange(project2).getChange().change().isPrivate()).isEqualTo(true);
+    assertThat(createChange(project2).getChange().change().isPrivate()).isTrue();
+  }
+
+  @Test
+  @GerritConfig(name = "change.disablePrivateChanges", value = "true")
+  public void pushPrivatesWithPrivateByDefaultAndDisablePrivateChangesTrue() throws Exception {
+    setPrivateByDefault(project2, InheritableBoolean.TRUE);
+
+    TestRepository<InMemoryRepository> testRepo = cloneProject(project2);
+    PushOneCommit.Result result =
+        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%private");
+    result.assertErrorStatus();
+  }
+
+  @Test
+  @GerritConfig(name = "change.disablePrivateChanges", value = "true")
+  public void pushDraftsWithPrivateByDefaultAndDisablePrivateChangesTrue() throws Exception {
+    setPrivateByDefault(project2, InheritableBoolean.TRUE);
+
+    RevCommit initialHead = getRemoteHead();
+    TestRepository<InMemoryRepository> testRepo = cloneProject(project2);
+    PushOneCommit.Result result =
+        pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%draft");
+    result.assertErrorStatus();
+
+    testRepo.reset(initialHead);
+    result = pushFactory.create(db, admin.getIdent(), testRepo).to("refs/drafts/master");
+    result.assertErrorStatus();
   }
 
   private void setPrivateByDefault(Project.NameKey proj, InheritableBoolean value)
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 21a1e76..c188d63 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.GroupInfo;
@@ -413,6 +414,60 @@
     assertReviewers(suggestReviewers(changeId, name), ImmutableList.of(foo1), ImmutableList.of());
   }
 
+  @Test
+  public void suggestBySecondaryEmailWithModifyAccount() throws Exception {
+    String secondaryEmail = "foo.secondary@example.com";
+    TestAccount foo = createAccountWithSecondaryEmail("foo", secondaryEmail);
+
+    List<SuggestedReviewerInfo> reviewers =
+        suggestReviewers(createChange().getChangeId(), secondaryEmail, 4);
+    assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
+
+    reviewers = suggestReviewers(createChange().getChangeId(), "secondary", 4);
+    assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
+  }
+
+  @Test
+  public void cannotSuggestBySecondaryEmailWithoutModifyAccount() throws Exception {
+    String secondaryEmail = "foo.secondary@example.com";
+    createAccountWithSecondaryEmail("foo", secondaryEmail);
+
+    setApiUser(user);
+    List<SuggestedReviewerInfo> reviewers =
+        suggestReviewers(createChange().getChangeId(), secondaryEmail, 4);
+    assertThat(reviewers).isEmpty();
+
+    reviewers = suggestReviewers(createChange().getChangeId(), "secondary2", 4);
+    assertThat(reviewers).isEmpty();
+  }
+
+  @Test
+  public void secondaryEmailsInSuggestions() throws Exception {
+    String secondaryEmail = "foo.secondary@example.com";
+    TestAccount foo = createAccountWithSecondaryEmail("foo", secondaryEmail);
+
+    List<SuggestedReviewerInfo> reviewers =
+        suggestReviewers(createChange().getChangeId(), "foo", 4);
+    assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
+    assertThat(Iterables.getOnlyElement(reviewers).account.secondaryEmails)
+        .containsExactly(secondaryEmail);
+
+    setApiUser(user);
+    reviewers = suggestReviewers(createChange().getChangeId(), "foo", 4);
+    assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
+    assertThat(Iterables.getOnlyElement(reviewers).account.secondaryEmails).isNull();
+  }
+
+  private TestAccount createAccountWithSecondaryEmail(String name, String secondaryEmail)
+      throws Exception {
+    TestAccount foo = accountCreator.create(name(name), "foo.primary@example.com", "Foo");
+    EmailInput input = new EmailInput();
+    input.email = secondaryEmail;
+    input.noConfirmation = true;
+    gApi.accounts().id(foo.id.get()).addEmail(input);
+    return foo;
+  }
+
   private List<SuggestedReviewerInfo> suggestReviewers(String changeId, String query)
       throws Exception {
     return gApi.changes().id(changeId).suggestReviewers(query).get();
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 38462c0..8fc5312 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -61,6 +61,7 @@
   @GerritConfig(name = "change.replyTooltip", value = "Publish votes and draft comments")
   @GerritConfig(name = "change.replyLabel", value = "Vote")
   @GerritConfig(name = "change.updateDelay", value = "50s")
+  @GerritConfig(name = "change.disablePrivateChanges", value = "true")
 
   // download
   @GerritConfig(
@@ -105,6 +106,7 @@
     assertThat(i.change.replyTooltip).startsWith("Publish votes and draft comments");
     assertThat(i.change.replyLabel).isEqualTo("Vote\u2026");
     assertThat(i.change.updateDelay).isEqualTo(50);
+    assertThat(i.change.disablePrivateChanges).isTrue();
 
     // download
     assertThat(i.download.archives).containsExactly("tar", "tbz2", "tgz", "txz");
@@ -178,6 +180,7 @@
     assertThat(i.change.replyTooltip).startsWith("Reply and score");
     assertThat(i.change.replyLabel).isEqualTo("Reply\u2026");
     assertThat(i.change.updateDelay).isEqualTo(300);
+    assertThat(i.change.disablePrivateChanges).isNull();
 
     // download
     assertThat(i.download.archives).containsExactly("tar", "tbz2", "tgz", "txz");
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 9d811b2..8cbe1e7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -24,9 +24,12 @@
 import com.google.common.collect.Sets;
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.api.projects.ConfigInfo;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -181,7 +184,7 @@
     Project project = projectCache.get(new Project.NameKey(newProjectName)).getProject();
     assertProjectInfo(project, p);
     assertThat(project.getDescription()).isEqualTo(in.description);
-    assertThat(project.getSubmitType()).isEqualTo(in.submitType);
+    assertThat(project.getConfiguredSubmitType()).isEqualTo(in.submitType);
     assertThat(project.getBooleanConfig(BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS))
         .isEqualTo(in.useContributorAgreements);
     assertThat(project.getBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY))
@@ -331,6 +334,84 @@
     }
   }
 
+  @SuppressWarnings("deprecation")
+  @Test
+  public void createProjectWithDefaultInheritedSubmitType() throws Exception {
+    String parent = name("parent");
+    ProjectInput pin = new ProjectInput();
+    pin.name = parent;
+    ConfigInfo cfg = gApi.projects().create(pin).config();
+    assertThat(cfg.submitType).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+    assertThat(cfg.defaultSubmitType.value).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+    assertThat(cfg.defaultSubmitType.configuredValue).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+    assertThat(cfg.defaultSubmitType.inheritedValue).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+
+    ConfigInput cin = new ConfigInput();
+    cin.submitType = SubmitType.CHERRY_PICK;
+    gApi.projects().name(parent).config(cin);
+    cfg = gApi.projects().name(parent).config();
+    assertThat(cfg.submitType).isEqualTo(SubmitType.CHERRY_PICK);
+    assertThat(cfg.defaultSubmitType.value).isEqualTo(SubmitType.CHERRY_PICK);
+    assertThat(cfg.defaultSubmitType.configuredValue).isEqualTo(SubmitType.CHERRY_PICK);
+    assertThat(cfg.defaultSubmitType.inheritedValue).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+
+    String child = name("child");
+    pin = new ProjectInput();
+    pin.submitType = SubmitType.INHERIT;
+    pin.parent = parent;
+    pin.name = child;
+    cfg = gApi.projects().create(pin).config();
+    assertThat(cfg.submitType).isEqualTo(SubmitType.CHERRY_PICK);
+    assertThat(cfg.defaultSubmitType.value).isEqualTo(SubmitType.CHERRY_PICK);
+    assertThat(cfg.defaultSubmitType.configuredValue).isEqualTo(SubmitType.INHERIT);
+    assertThat(cfg.defaultSubmitType.inheritedValue).isEqualTo(SubmitType.CHERRY_PICK);
+
+    cin = new ConfigInput();
+    cin.submitType = SubmitType.REBASE_IF_NECESSARY;
+    gApi.projects().name(parent).config(cin);
+    cfg = gApi.projects().name(parent).config();
+    assertThat(cfg.submitType).isEqualTo(SubmitType.REBASE_IF_NECESSARY);
+    assertThat(cfg.defaultSubmitType.value).isEqualTo(SubmitType.REBASE_IF_NECESSARY);
+    assertThat(cfg.defaultSubmitType.configuredValue).isEqualTo(SubmitType.REBASE_IF_NECESSARY);
+    assertThat(cfg.defaultSubmitType.inheritedValue).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+
+    cfg = gApi.projects().name(child).config();
+    assertThat(cfg.submitType).isEqualTo(SubmitType.REBASE_IF_NECESSARY);
+    assertThat(cfg.defaultSubmitType.value).isEqualTo(SubmitType.REBASE_IF_NECESSARY);
+    assertThat(cfg.defaultSubmitType.configuredValue).isEqualTo(SubmitType.INHERIT);
+    assertThat(cfg.defaultSubmitType.inheritedValue).isEqualTo(SubmitType.REBASE_IF_NECESSARY);
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  @GerritConfig(
+    name = "repository.testinheritedsubmittype/*.defaultSubmitType",
+    value = "CHERRY_PICK"
+  )
+  public void repositoryConfigTakesPrecedenceOverInheritedSubmitType() throws Exception {
+    // Can't use name() since we need to specify this project name in gerrit.config prior to
+    // startup. Pick something reasonably unique instead.
+    String parent = "testinheritedsubmittype";
+    ProjectInput pin = new ProjectInput();
+    pin.name = parent;
+    pin.submitType = SubmitType.MERGE_ALWAYS;
+    ConfigInfo cfg = gApi.projects().create(pin).config();
+    assertThat(cfg.submitType).isEqualTo(SubmitType.MERGE_ALWAYS);
+    assertThat(cfg.defaultSubmitType.value).isEqualTo(SubmitType.MERGE_ALWAYS);
+    assertThat(cfg.defaultSubmitType.configuredValue).isEqualTo(SubmitType.MERGE_ALWAYS);
+    assertThat(cfg.defaultSubmitType.inheritedValue).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+
+    String child = parent + "/child";
+    pin = new ProjectInput();
+    pin.parent = parent;
+    pin.name = child;
+    cfg = gApi.projects().create(pin).config();
+    assertThat(cfg.submitType).isEqualTo(SubmitType.CHERRY_PICK);
+    assertThat(cfg.defaultSubmitType.value).isEqualTo(SubmitType.CHERRY_PICK);
+    assertThat(cfg.defaultSubmitType.configuredValue).isEqualTo(SubmitType.CHERRY_PICK);
+    assertThat(cfg.defaultSubmitType.inheritedValue).isEqualTo(SubmitType.MERGE_ALWAYS);
+  }
+
   private void assertHead(String projectName, String expectedRef) throws Exception {
     try (Repository repo = repoManager.openRepository(new Project.NameKey(projectName))) {
       assertThat(repo.exactRef(Constants.HEAD).getTarget().getName()).isEqualTo(expectedRef);
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index f1385dd..f8b7652 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -41,7 +41,6 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ConsistencyChecker;
 import com.google.gerrit.server.change.PatchSetInserter;
@@ -67,7 +66,10 @@
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
@@ -90,8 +92,6 @@
 
   @Inject private Sequences sequences;
 
-  @Inject private AccountsUpdate.Server accountsUpdate;
-
   private RevCommit tip;
   private Account.Id adminId;
   private ConsistencyChecker checker;
@@ -126,7 +126,7 @@
   public void missingOwner() throws Exception {
     TestAccount owner = accountCreator.create("missing");
     ChangeNotes notes = insertChange(owner);
-    accountsUpdate.create().deleteByKey(owner.getId());
+    deleteUserBranch(owner.getId());
 
     assertProblems(notes, null, problem("Missing change owner: " + owner.getId()));
   }
@@ -958,4 +958,23 @@
   private void assertNoProblems(ChangeNotes notes, @Nullable FixInput fix) throws Exception {
     assertThat(checker.check(notes, fix).problems()).isEmpty();
   }
+
+  private void deleteUserBranch(Account.Id accountId) throws IOException {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      String refName = RefNames.refsUsers(accountId);
+      Ref ref = repo.exactRef(refName);
+      if (ref == null) {
+        return;
+      }
+
+      RefUpdate ru = repo.updateRef(refName);
+      ru.setExpectedOldObjectId(ref.getObjectId());
+      ru.setNewObjectId(ObjectId.zeroId());
+      ru.setForceUpdate(true);
+      Result result = ru.delete();
+      if (result != Result.FORCED) {
+        throw new IOException(String.format("Failed to delete ref %s: %s", refName, result.name()));
+      }
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index 79fed8b..c0dcc9c 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -1195,7 +1195,7 @@
     Map<String, List<CommentInfo>> comments = gApi.changes().id(id.get()).current().drafts();
     for (List<CommentInfo> cList : comments.values()) {
       for (CommentInfo ci : cList) {
-        assertThat(ci.unresolved).isEqualTo(true);
+        assertThat(ci.unresolved).isTrue();
       }
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 228beef..1236826 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -26,30 +26,21 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.common.GroupInfo;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.account.WatchConfig;
 import com.google.gerrit.server.account.WatchConfig.NotifyType;
-import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.inject.Inject;
 import java.util.EnumSet;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.junit.Test;
 
 @NoHttpd
 public class ProjectWatchIT extends AbstractDaemonTest {
-  @Inject private WatchConfig.Accessor watchConfig;
-
   @Test
   public void newPatchSetsNotifyConfig() throws Exception {
     Address addr = new Address("Watcher", "watcher@example.com");
@@ -494,34 +485,6 @@
   }
 
   @Test
-  public void deleteAllProjectWatches() throws Exception {
-    Map<ProjectWatchKey, Set<NotifyType>> watches = new HashMap<>();
-    watches.put(ProjectWatchKey.create(project, "*"), ImmutableSet.of(NotifyType.ALL));
-    watchConfig.upsertProjectWatches(admin.getId(), watches);
-    assertThat(watchConfig.getProjectWatches(admin.getId())).isNotEmpty();
-
-    watchConfig.deleteAllProjectWatches(admin.getId());
-    assertThat(watchConfig.getProjectWatches(admin.getId())).isEmpty();
-  }
-
-  @Test
-  public void deleteAllProjectWatchesIfWatchConfigIsTheOnlyFileInUserBranch() throws Exception {
-    // Create account that has no files in its refs/users/ branch.
-    Account.Id id = accountCreator.create().id;
-
-    // Add a project watch so that a watch.config file in the refs/users/ branch is created.
-    Map<ProjectWatchKey, Set<NotifyType>> watches = new HashMap<>();
-    watches.put(ProjectWatchKey.create(project, "*"), ImmutableSet.of(NotifyType.ALL));
-    watchConfig.upsertProjectWatches(id, watches);
-    assertThat(watchConfig.getProjectWatches(id)).isNotEmpty();
-
-    // Delete all project watches so that the watch.config file in the refs/users/ branch is
-    // deleted.
-    watchConfig.deleteAllProjectWatches(id);
-    assertThat(watchConfig.getProjectWatches(id)).isEmpty();
-  }
-
-  @Test
   public void watchProjectNoNotificationForPrivateChange() throws Exception {
     // watch project
     String watchedProject = createProject("watchedProject").get();
diff --git a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
index a64818d..f4f81f8 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -127,7 +127,7 @@
     tmp = in.readString();
     tmp = in.readString();
     tmp = tmp.substring(1);
-    assertThat(tmp).isEqualTo("fatal: upload-archive not permitted");
+    assertThat(tmp).isEqualTo("fatal: upload-archive not permitted for format zip");
   }
 
   private InputStream argumentsToInputStream(String c) throws Exception {
diff --git a/javatests/com/google/gerrit/server/group/db/BUILD b/javatests/com/google/gerrit/server/group/db/BUILD
index 1ba0ce9..9816603 100644
--- a/javatests/com/google/gerrit/server/group/db/BUILD
+++ b/javatests/com/google/gerrit/server/group/db/BUILD
@@ -6,12 +6,14 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/common/data/testing:common-data-test-util",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/extensions/common/testing:common-test-util",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/group/db/testing",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/truth",
         "//lib:gwtorm",
         "//lib:truth",
         "//lib/jgit/org.eclipse.jgit:jgit",
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
index d4ddbcf..c059da9 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -18,24 +18,38 @@
 import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_GROUPNAMES;
+import static com.google.gerrit.truth.OptionalSubject.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.testing.GroupReferenceSubject;
 import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.testing.CommitInfoSubject;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.group.db.testing.GroupTestUtil;
 import com.google.gerrit.server.update.RefUpdateUtil;
-import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.TestTimeUtil;
+import com.google.gerrit.truth.ListSubject;
+import com.google.gerrit.truth.OptionalSubject;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.StandardKeyEncoder;
+import java.io.IOException;
 import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -47,16 +61,28 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
-public class GroupNameNotesTest extends GerritBaseTests {
+public class GroupNameNotesTest {
+  static {
+    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+  }
+
   private static final String SERVER_NAME = "Gerrit Server";
   private static final String SERVER_EMAIL = "noreply@gerritcodereview.com";
   private static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
 
+  @Rule public ExpectedException expectedException = ExpectedException.none();
+
+  private final AccountGroup.UUID groupUuid = new AccountGroup.UUID("users-XYZ");
+  private final AccountGroup.NameKey groupName = new AccountGroup.NameKey("users");
+
   private AtomicInteger idCounter;
   private Repository repo;
 
@@ -73,12 +99,311 @@
   }
 
   @Test
+  public void newGroupCanBeCreated() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    Optional<GroupReference> groupReference = loadGroup(groupName);
+    assertThatGroup(groupReference).value().groupUuid().isEqualTo(groupUuid);
+    assertThatGroup(groupReference).value().name().isEqualTo(groupName.get());
+  }
+
+  @Test
+  public void uuidOfNewGroupMustNotBeNull() throws Exception {
+    expectedException.expect(NullPointerException.class);
+    GroupNameNotes.forNewGroup(repo, null, groupName);
+  }
+
+  @Test
+  public void nameOfNewGroupMustNotBeNull() throws Exception {
+    expectedException.expect(NullPointerException.class);
+    GroupNameNotes.forNewGroup(repo, groupUuid, null);
+  }
+
+  @Test
+  public void nameOfNewGroupMayBeEmpty() throws Exception {
+    AccountGroup.NameKey emptyName = new AccountGroup.NameKey("");
+    createGroup(groupUuid, emptyName);
+
+    Optional<GroupReference> groupReference = loadGroup(emptyName);
+    assertThatGroup(groupReference).value().name().isEqualTo("");
+  }
+
+  @Test
+  public void newGroupMustNotReuseNameOfAnotherGroup() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    AccountGroup.UUID anotherGroupUuid = new AccountGroup.UUID("AnotherGroup");
+    expectedException.expect(OrmDuplicateKeyException.class);
+    expectedException.expectMessage(groupName.get());
+    GroupNameNotes.forNewGroup(repo, anotherGroupUuid, groupName);
+  }
+
+  @Test
+  public void newGroupMayReuseUuidOfAnotherGroup() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    createGroup(groupUuid, anotherName);
+
+    Optional<GroupReference> group1 = loadGroup(groupName);
+    assertThatGroup(group1).value().groupUuid().isEqualTo(groupUuid);
+    Optional<GroupReference> group2 = loadGroup(anotherName);
+    assertThatGroup(group2).value().groupUuid().isEqualTo(groupUuid);
+  }
+
+  @Test
+  public void groupCanBeRenamed() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    renameGroup(groupUuid, groupName, anotherName);
+
+    Optional<GroupReference> groupReference = loadGroup(anotherName);
+    assertThatGroup(groupReference).value().groupUuid().isEqualTo(groupUuid);
+    assertThatGroup(groupReference).value().name().isEqualTo(anotherName.get());
+  }
+
+  @Test
+  public void previousNameOfGroupCannotBeUsedAfterRename() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    renameGroup(groupUuid, groupName, anotherName);
+
+    Optional<GroupReference> group = loadGroup(groupName);
+    assertThatGroup(group).isAbsent();
+  }
+
+  @Test
+  public void groupCannotBeRenamedToNull() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    expectedException.expect(NullPointerException.class);
+    GroupNameNotes.forRename(repo, groupUuid, groupName, null);
+  }
+
+  @Test
+  public void oldNameOfGroupMustBeSpecifiedForRename() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    expectedException.expect(NullPointerException.class);
+    GroupNameNotes.forRename(repo, groupUuid, null, anotherName);
+  }
+
+  @Test
+  public void groupCannotBeRenamedWhenOldNameIsWrong() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    AccountGroup.NameKey anotherOldName = new AccountGroup.NameKey("contributors");
+    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    expectedException.expect(ConfigInvalidException.class);
+    expectedException.expectMessage(anotherOldName.get());
+    GroupNameNotes.forRename(repo, groupUuid, anotherOldName, anotherName);
+  }
+
+  @Test
+  public void groupCannotBeRenamedToNameOfAnotherGroup() throws Exception {
+    createGroup(groupUuid, groupName);
+    AccountGroup.UUID anotherGroupUuid = new AccountGroup.UUID("admins-ABC");
+    AccountGroup.NameKey anotherGroupName = new AccountGroup.NameKey("admins");
+    createGroup(anotherGroupUuid, anotherGroupName);
+
+    expectedException.expect(OrmDuplicateKeyException.class);
+    expectedException.expectMessage(anotherGroupName.get());
+    GroupNameNotes.forRename(repo, groupUuid, groupName, anotherGroupName);
+  }
+
+  @Test
+  public void groupCannotBeRenamedWithoutSpecifiedUuid() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    expectedException.expect(NullPointerException.class);
+    GroupNameNotes.forRename(repo, null, groupName, anotherName);
+  }
+
+  @Test
+  public void groupCannotBeRenamedWhenUuidIsWrong() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    AccountGroup.UUID anotherGroupUuid = new AccountGroup.UUID("admins-ABC");
+    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    expectedException.expect(ConfigInvalidException.class);
+    expectedException.expectMessage(groupUuid.get());
+    GroupNameNotes.forRename(repo, anotherGroupUuid, groupName, anotherName);
+  }
+
+  @Test
+  public void firstGroupCreationCreatesARootCommit() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    Ref ref = repo.exactRef(RefNames.REFS_GROUPNAMES);
+    assertThat(ref.getObjectId()).isNotNull();
+
+    try (RevWalk revWalk = new RevWalk(repo)) {
+      RevCommit revCommit = revWalk.parseCommit(ref.getObjectId());
+      assertThat(revCommit.getParentCount()).isEqualTo(0);
+    }
+  }
+
+  @Test
+  public void furtherGroupCreationAppendsACommit() throws Exception {
+    createGroup(groupUuid, groupName);
+    ImmutableList<CommitInfo> commitsAfterCreation = log();
+
+    AccountGroup.UUID anotherGroupUuid = new AccountGroup.UUID("admins-ABC");
+    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    createGroup(anotherGroupUuid, anotherName);
+
+    ImmutableList<CommitInfo> commitsAfterFurtherGroup = log();
+    assertThatCommits(commitsAfterFurtherGroup).containsAllIn(commitsAfterCreation);
+    assertThatCommits(commitsAfterFurtherGroup).lastElement().isNotIn(commitsAfterCreation);
+  }
+
+  @Test
+  public void groupRenamingAppendsACommit() throws Exception {
+    createGroup(groupUuid, groupName);
+    ImmutableList<CommitInfo> commitsAfterCreation = log();
+
+    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    renameGroup(groupUuid, groupName, anotherName);
+
+    ImmutableList<CommitInfo> commitsAfterRename = log();
+    assertThatCommits(commitsAfterRename).containsAllIn(commitsAfterCreation);
+    assertThatCommits(commitsAfterRename).lastElement().isNotIn(commitsAfterCreation);
+  }
+
+  @Test
+  public void newCommitIsNotCreatedForRedundantNameUpdate() throws Exception {
+    createGroup(groupUuid, groupName);
+    ImmutableList<CommitInfo> commitsAfterCreation = log();
+
+    renameGroup(groupUuid, groupName, groupName);
+
+    ImmutableList<CommitInfo> commitsAfterRename = log();
+    assertThatCommits(commitsAfterRename).isEqualTo(commitsAfterCreation);
+  }
+
+  @Test
+  public void newCommitIsNotCreatedWhenCommittingGroupCreationTwice() throws Exception {
+    GroupNameNotes groupNameNotes = GroupNameNotes.forNewGroup(repo, groupUuid, groupName);
+
+    commit(groupNameNotes);
+    ImmutableList<CommitInfo> commitsAfterFirstCommit = log();
+    commit(groupNameNotes);
+    ImmutableList<CommitInfo> commitsAfterSecondCommit = log();
+
+    assertThatCommits(commitsAfterSecondCommit).isEqualTo(commitsAfterFirstCommit);
+  }
+
+  @Test
+  public void newCommitIsNotCreatedWhenCommittingGroupRenamingTwice() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    GroupNameNotes groupNameNotes =
+        GroupNameNotes.forRename(repo, groupUuid, groupName, anotherName);
+
+    commit(groupNameNotes);
+    ImmutableList<CommitInfo> commitsAfterFirstCommit = log();
+    commit(groupNameNotes);
+    ImmutableList<CommitInfo> commitsAfterSecondCommit = log();
+
+    assertThatCommits(commitsAfterSecondCommit).isEqualTo(commitsAfterFirstCommit);
+  }
+
+  @Test
+  public void commitMessageMentionsGroupCreation() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    ImmutableList<CommitInfo> commits = log();
+    assertThatCommits(commits).lastElement().message().contains("Create");
+    assertThatCommits(commits).lastElement().message().contains(groupName.get());
+  }
+
+  @Test
+  public void commitMessageMentionsGroupRenaming() throws Exception {
+    createGroup(groupUuid, groupName);
+
+    AccountGroup.NameKey anotherName = new AccountGroup.NameKey("admins");
+    renameGroup(groupUuid, groupName, anotherName);
+
+    ImmutableList<CommitInfo> commits = log();
+    assertThatCommits(commits).lastElement().message().contains("Rename");
+    assertThatCommits(commits).lastElement().message().contains(groupName.get());
+    assertThatCommits(commits).lastElement().message().contains(anotherName.get());
+  }
+
+  @Test
+  public void nonExistentNotesRefIsEquivalentToNonExistentGroup() throws Exception {
+    Optional<GroupReference> group = loadGroup(groupName);
+
+    assertThatGroup(group).isAbsent();
+  }
+
+  @Test
+  public void nonExistentGroupCannotBeLoaded() throws Exception {
+    createGroup(new AccountGroup.UUID("contributors-MN"), new AccountGroup.NameKey("contributors"));
+    createGroup(groupUuid, groupName);
+
+    Optional<GroupReference> group = loadGroup(new AccountGroup.NameKey("admins"));
+    assertThatGroup(group).isAbsent();
+  }
+
+  @Test
+  public void specificGroupCanBeLoaded() throws Exception {
+    createGroup(new AccountGroup.UUID("contributors-MN"), new AccountGroup.NameKey("contributors"));
+    createGroup(groupUuid, groupName);
+    createGroup(new AccountGroup.UUID("admins-ABC"), new AccountGroup.NameKey("admins"));
+
+    Optional<GroupReference> group = loadGroup(groupName);
+    assertThatGroup(group).value().groupUuid().isEqualTo(groupUuid);
+  }
+
+  @Test
+  public void nonExistentNotesRefIsEquivalentToNotAnyExistingGroups() throws Exception {
+    ImmutableList<GroupReference> allGroups = GroupNameNotes.loadAllGroups(repo);
+
+    assertThat(allGroups).isEmpty();
+  }
+
+  @Test
+  public void allGroupsCanBeLoaded() throws Exception {
+    AccountGroup.UUID groupUuid1 = new AccountGroup.UUID("contributors-MN");
+    AccountGroup.NameKey groupName1 = new AccountGroup.NameKey("contributors");
+    createGroup(groupUuid1, groupName1);
+    AccountGroup.UUID groupUuid2 = new AccountGroup.UUID("admins-ABC");
+    AccountGroup.NameKey groupName2 = new AccountGroup.NameKey("admins");
+    createGroup(groupUuid2, groupName2);
+
+    ImmutableList<GroupReference> allGroups = GroupNameNotes.loadAllGroups(repo);
+
+    GroupReference group1 = new GroupReference(groupUuid1, groupName1.get());
+    GroupReference group2 = new GroupReference(groupUuid2, groupName2.get());
+    assertThat(allGroups).containsExactly(group1, group2);
+  }
+
+  @Test
+  public void loadedGroupsContainGroupsWithDuplicateGroupUuids() throws Exception {
+    createGroup(groupUuid, groupName);
+    AccountGroup.NameKey anotherGroupName = new AccountGroup.NameKey("admins");
+    createGroup(groupUuid, anotherGroupName);
+
+    ImmutableList<GroupReference> allGroups = GroupNameNotes.loadAllGroups(repo);
+
+    GroupReference group1 = new GroupReference(groupUuid, groupName.get());
+    GroupReference group2 = new GroupReference(groupUuid, anotherGroupName.get());
+    assertThat(allGroups).containsExactly(group1, group2);
+  }
+
+  @Test
   public void updateGroupNames() throws Exception {
     GroupReference g1 = newGroup("a");
     GroupReference g2 = newGroup("b");
 
     PersonIdent ident = newPersonIdent();
-    updateGroupNames(ident, g1, g2);
+    updateAllGroups(ident, g1, g2);
 
     ImmutableList<CommitInfo> log = log();
     assertThat(log).hasSize(1);
@@ -87,11 +412,11 @@
     assertThat(log.get(0)).author().matches(ident);
     assertThat(log.get(0)).committer().matches(ident);
 
-    assertThat(GroupTestUtil.readNameToUuidMap(repo)).containsExactly("a", "a-1", "b", "b-2");
+    assertThat(GroupNameNotes.loadAllGroups(repo)).containsExactly(g1, g2);
 
     // Updating the same set of names is a no-op.
     String commit = log.get(0).commit;
-    updateGroupNames(newPersonIdent(), g1, g2);
+    updateAllGroups(newPersonIdent(), g1, g2);
     log = log();
     assertThat(log).hasSize(1);
     assertThat(log.get(0)).commit().isEqualTo(commit);
@@ -120,9 +445,9 @@
             .copy();
 
     ident = newPersonIdent();
-    updateGroupNames(ident, g1, g2);
+    updateAllGroups(ident, g1, g2);
 
-    assertThat(GroupTestUtil.readNameToUuidMap(repo)).containsExactly("a", "a-1", "b", "b-2");
+    assertThat(GroupNameNotes.loadAllGroups(repo)).containsExactly(g1, g2);
 
     ImmutableList<CommitInfo> log = log();
     assertThat(log).hasSize(2);
@@ -142,13 +467,13 @@
     GroupReference g2 = newGroup("b");
 
     PersonIdent ident = newPersonIdent();
-    updateGroupNames(ident, g1, g2);
+    updateAllGroups(ident, g1, g2);
 
-    assertThat(GroupTestUtil.readNameToUuidMap(repo)).containsExactly("a", "a-1", "b", "b-2");
+    assertThat(GroupNameNotes.loadAllGroups(repo)).containsExactly(g1, g2);
 
-    updateGroupNames(ident);
+    updateAllGroups(ident);
 
-    assertThat(GroupTestUtil.readNameToUuidMap(repo)).isEmpty();
+    assertThat(GroupNameNotes.loadAllGroups(repo)).isEmpty();
 
     ImmutableList<CommitInfo> log = log();
     assertThat(log).hasSize(2);
@@ -171,12 +496,46 @@
   @Test
   public void emptyGroupName() throws Exception {
     GroupReference g = newGroup("");
-    updateGroupNames(newPersonIdent(), g);
+    updateAllGroups(newPersonIdent(), g);
 
-    assertThat(GroupTestUtil.readNameToUuidMap(repo)).containsExactly("", "-1");
+    assertThat(GroupNameNotes.loadAllGroups(repo)).containsExactly(g);
     assertThat(readNameNote(g)).isEqualTo("[group]\n\tuuid = -1\n\tname = \n");
   }
 
+  private void createGroup(AccountGroup.UUID groupUuid, AccountGroup.NameKey groupName)
+      throws Exception {
+    GroupNameNotes groupNameNotes = GroupNameNotes.forNewGroup(repo, groupUuid, groupName);
+    commit(groupNameNotes);
+  }
+
+  private void renameGroup(
+      AccountGroup.UUID groupUuid, AccountGroup.NameKey oldName, AccountGroup.NameKey newName)
+      throws Exception {
+    GroupNameNotes groupNameNotes = GroupNameNotes.forRename(repo, groupUuid, oldName, newName);
+    commit(groupNameNotes);
+  }
+
+  private Optional<GroupReference> loadGroup(AccountGroup.NameKey groupName) throws Exception {
+    return GroupNameNotes.loadGroup(repo, groupName);
+  }
+
+  private void commit(GroupNameNotes groupNameNotes) throws IOException {
+    try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
+      groupNameNotes.commit(metaDataUpdate);
+    }
+  }
+
+  private MetaDataUpdate createMetaDataUpdate() {
+    PersonIdent serverIdent = newPersonIdent();
+
+    MetaDataUpdate metaDataUpdate =
+        new MetaDataUpdate(
+            GitReferenceUpdated.DISABLED, new Project.NameKey("Test Repository"), repo);
+    metaDataUpdate.getCommitBuilder().setCommitter(serverIdent);
+    metaDataUpdate.getCommitBuilder().setAuthor(serverIdent);
+    return metaDataUpdate;
+  }
+
   private GroupReference newGroup(String name) {
     int id = idCounter.incrementAndGet();
     return new GroupReference(new AccountGroup.UUID(name + "-" + id), name);
@@ -190,10 +549,10 @@
     return GroupNameNotes.getNoteKey(new AccountGroup.NameKey(g.getName()));
   }
 
-  private void updateGroupNames(PersonIdent ident, GroupReference... groupRefs) throws Exception {
+  private void updateAllGroups(PersonIdent ident, GroupReference... groupRefs) throws Exception {
     try (ObjectInserter inserter = repo.newObjectInserter()) {
       BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
-      GroupNameNotes.updateGroupNames(repo, inserter, bru, Arrays.asList(groupRefs), ident);
+      GroupNameNotes.updateAllGroups(repo, inserter, bru, Arrays.asList(groupRefs), ident);
       inserter.flush();
       RefUpdateUtil.executeChecked(bru, repo);
     }
@@ -204,7 +563,7 @@
       BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
       PersonIdent ident = newPersonIdent();
       try {
-        GroupNameNotes.updateGroupNames(repo, inserter, bru, Arrays.asList(groupRefs), ident);
+        GroupNameNotes.updateAllGroups(repo, inserter, bru, Arrays.asList(groupRefs), ident);
         assert_().fail("Expected IllegalArgumentException");
       } catch (IllegalArgumentException e) {
         assertThat(e).hasMessageThat().isEqualTo(GroupNameNotes.UNIQUE_REF_ERROR);
@@ -225,4 +584,14 @@
       return new String(reader.open(noteMap.get(k), OBJ_BLOB).getCachedBytes(), UTF_8);
     }
   }
+
+  private static OptionalSubject<GroupReferenceSubject, GroupReference> assertThatGroup(
+      Optional<GroupReference> group) {
+    return assertThat(group, GroupReferenceSubject::assertThat);
+  }
+
+  private static ListSubject<CommitInfoSubject, CommitInfo> assertThatCommits(
+      List<CommitInfo> commits) {
+    return ListSubject.assertThat(commits, CommitInfoSubject::assertThat);
+  }
 }
diff --git a/javatests/com/google/gerrit/server/group/db/GroupRebuilderTest.java b/javatests/com/google/gerrit/server/group/db/GroupRebuilderTest.java
index 51cf987..048eaba 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupRebuilderTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupRebuilderTest.java
@@ -536,7 +536,7 @@
     try (ObjectInserter inserter = repo.newObjectInserter()) {
       ImmutableList<GroupReference> refs =
           ImmutableList.of(GroupReference.forGroup(g1), GroupReference.forGroup(g2));
-      GroupNameNotes.updateGroupNames(repo, inserter, bru, refs, newPersonIdent());
+      GroupNameNotes.updateAllGroups(repo, inserter, bru, refs, newPersonIdent());
       inserter.flush();
     }
 
@@ -552,7 +552,9 @@
     assertMigratedCleanly(reload(g1), b1);
     assertMigratedCleanly(reload(g2), b2);
 
-    assertThat(GroupTestUtil.readNameToUuidMap(repo)).containsExactly("a", "a-1", "b", "b-2");
+    GroupReference group1 = GroupReference.forGroup(g1);
+    GroupReference group2 = GroupReference.forGroup(g2);
+    assertThat(GroupNameNotes.loadAllGroups(repo)).containsExactly(group1, group2);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java b/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
index eff755f..a5b04ee 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
@@ -30,7 +30,7 @@
   public void groupNamesRefIsMissing() throws Exception {
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, "g-1", new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
     assertThat(problems)
         .containsExactly(warning("Group with name 'g-1' doesn't exist in the list of all names"));
   }
@@ -40,7 +40,7 @@
     updateGroupNamesRef("g-2", "[group]\n\tuuid = uuid-2\n\tname = g-2\n");
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, "g-1", new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
     assertThat(problems)
         .containsExactly(warning("Group with name 'g-1' doesn't exist in the list of all names"));
   }
@@ -50,7 +50,7 @@
     updateGroupNamesRef("g-1", "[group]\n\tuuid = uuid-1\n\tname = g-1\n");
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, "g-1", new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
     assertThat(problems).isEmpty();
   }
 
@@ -59,7 +59,7 @@
     updateGroupNamesRef("g-1", "[group]\n\tuuid = uuid-2\n\tname = g-1\n");
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, "g-1", new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
     assertThat(problems)
         .containsExactly(
             warning(
@@ -72,7 +72,7 @@
     updateGroupNamesRef("g-1", "[group]\n\tuuid = uuid-1\n\tname = g-2\n");
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, "g-1", new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
     assertThat(problems)
         .containsExactly(warning("group note of name 'g-1' claims to represent name of 'g-2'"));
   }
@@ -82,7 +82,7 @@
     updateGroupNamesRef("g-1", "[group]\n\tuuid = uuid-2\n\tname = g-2\n");
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, "g-1", new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
     assertThat(problems)
         .containsExactly(
             warning(
@@ -97,7 +97,7 @@
     updateGroupNamesRef("g-1", "[invalid");
     List<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
-            allUsersRepo, "g-1", new AccountGroup.UUID("uuid-1"));
+            allUsersRepo, new AccountGroup.NameKey("g-1"), new AccountGroup.UUID("uuid-1"));
     assertThat(problems)
         .containsExactly(
             warning(
diff --git a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
index 6bba51b..1542fe5 100644
--- a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
@@ -23,6 +23,7 @@
 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;
@@ -44,7 +45,12 @@
     List<String> values =
         toStrings(
             AccountField.REF_STATE.get(
-                new AccountState(allUsersName, account, ImmutableSet.of(), ImmutableMap.of())));
+                new AccountState(
+                    allUsersName,
+                    account,
+                    ImmutableSet.of(),
+                    ImmutableMap.of(),
+                    GeneralPreferencesInfo.defaults())));
     assertThat(values).hasSize(1);
     String expectedValue =
         allUsersName.get() + ":" + RefNames.refsUsers(account.getId()) + ":" + metaId;
@@ -73,7 +79,11 @@
         toStrings(
             AccountField.EXTERNAL_ID_STATE.get(
                 new AccountState(
-                    null, account, ImmutableSet.of(extId1, extId2), ImmutableMap.of())));
+                    null,
+                    account,
+                    ImmutableSet.of(extId1, extId2),
+                    ImmutableMap.of(),
+                    GeneralPreferencesInfo.defaults())));
     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 d65dd47..01e8225 100644
--- a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -22,6 +22,7 @@
 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;
@@ -388,6 +389,7 @@
         new AllUsersName(AllUsersNameProvider.DEFAULT),
         account,
         Collections.emptySet(),
-        new HashMap<>());
+        new HashMap<>(),
+        GeneralPreferencesInfo.defaults());
   }
 }
diff --git a/javatests/com/google/gerrit/server/project/RefControlTest.java b/javatests/com/google/gerrit/server/project/RefControlTest.java
index 9f70e3d..8892a50 100644
--- a/javatests/com/google/gerrit/server/project/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/project/RefControlTest.java
@@ -34,6 +34,7 @@
 
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.PermissionRange;
@@ -207,6 +208,7 @@
   @Inject private SingleVersionListener singleVersionListener;
   @Inject private InMemoryDatabase schemaFactory;
   @Inject private ThreadLocalRequestContext requestContext;
+  @Inject private ProjectControl.Factory projectControlFactory;
 
   @Before
   public void setUp() throws Exception {
@@ -235,13 +237,16 @@
           public void remove(Project p) {}
 
           @Override
-          public Iterable<Project.NameKey> all() {
-            return Collections.emptySet();
+          public void remove(Project.NameKey name) {}
+
+          @Override
+          public ImmutableSortedSet<Project.NameKey> all() {
+            return ImmutableSortedSet.of();
           }
 
           @Override
-          public Iterable<Project.NameKey> byName(String prefix) {
-            return Collections.emptySet();
+          public ImmutableSortedSet<Project.NameKey> byName(String prefix) {
+            return ImmutableSortedSet.of();
           }
 
           @Override
@@ -831,7 +836,6 @@
 
   private InMemoryRepository add(ProjectConfig pc) {
     PrologEnvironment.Factory envFactory = null;
-    ProjectControl.AssistedFactory projectControlFactory = null;
     RulesCache rulesCache = null;
     SitePaths sitePaths = null;
     List<CommentLinkInfo> commentLinks = null;
@@ -871,7 +875,6 @@
         Collections.<AccountGroup.UUID>emptySet(),
         Collections.<AccountGroup.UUID>emptySet(),
         sectionSorter,
-        null, // commitsCollection
         changeControlFactory,
         permissionBackend,
         new MockUser(name, memberOf),
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 77c139e..262701c 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -26,10 +26,14 @@
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.common.AccountExternalIdInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+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.index.FieldDef;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
@@ -121,7 +125,7 @@
   protected Injector injector;
   protected ReviewDb db;
   protected AccountInfo currentUserInfo;
-  protected CurrentUser user;
+  protected CurrentUser admin;
 
   protected abstract Injector createInjector();
 
@@ -145,10 +149,10 @@
     db = schemaFactory.open();
     schemaCreator.create(db);
 
-    Account.Id userId = createAccount("user", "User", "user@example.com", true);
-    user = userFactory.create(userId);
-    requestContext.setContext(newRequestContext(userId));
-    currentUserInfo = gApi.accounts().id(userId.get()).get();
+    Account.Id adminId = createAccount("admin", "Administrator", "admin@example.com", true);
+    admin = userFactory.create(adminId);
+    requestContext.setContext(newRequestContext(adminId));
+    currentUserInfo = gApi.accounts().id(adminId.get()).get();
   }
 
   protected RequestContext newRequestContext(Account.Id requestUserId) {
@@ -238,6 +242,52 @@
   }
 
   @Test
+  public void bySecondaryEmail() throws Exception {
+    String prefix = name("secondary");
+    String domain = name("test.com");
+    String secondaryEmail = prefix + "@" + domain;
+    AccountInfo user1 = newAccountWithEmail("user1", name("user1@example.com"));
+    addEmails(user1, secondaryEmail);
+
+    AccountInfo user2 = newAccountWithEmail("user2", name("user2@example.com"));
+    addEmails(user2, name("other@" + domain));
+
+    assertQuery(secondaryEmail, user1);
+    assertQuery("email:" + secondaryEmail, user1);
+    assertQuery("email:" + prefix, user1);
+    assertQuery(domain, user1, user2);
+  }
+
+  @Test
+  public void byEmailWithoutModifyAccountCapability() throws Exception {
+    String preferredEmail = name("primary@test.com");
+    String secondaryEmail = name("secondary@test.com");
+    AccountInfo user1 = newAccountWithEmail("user1", preferredEmail);
+    addEmails(user1, secondaryEmail);
+
+    AccountInfo user2 = newAccount("user");
+    requestContext.setContext(newRequestContext(new Account.Id(user2._accountId)));
+
+    if (getSchemaVersion() < 5) {
+      assertMissingField(AccountField.PREFERRED_EMAIL);
+      assertFailingQuery("email:foo", "'email' operator is not supported by account index version");
+      return;
+    }
+
+    // This at least needs the PREFERRED_EMAIL field which is available from schema version 5.
+    if (getSchemaVersion() >= 5) {
+      assertQuery(preferredEmail, user1);
+    } else {
+      assertQuery(preferredEmail);
+    }
+
+    assertQuery(secondaryEmail);
+
+    assertQuery("email:" + preferredEmail, user1);
+    assertQuery("email:" + secondaryEmail);
+  }
+
+  @Test
   public void byUsername() throws Exception {
     AccountInfo user1 = newAccount("myuser");
 
@@ -298,6 +348,49 @@
   }
 
   @Test
+  public void byNameWithoutModifyAccountCapability() throws Exception {
+    AccountInfo user1 = newAccountWithFullName("jdoe", "John Doe");
+    AccountInfo user2 = newAccountWithFullName("jroe", "Jane Roe");
+
+    AccountInfo user3 = newAccount("user");
+    requestContext.setContext(newRequestContext(new Account.Id(user3._accountId)));
+
+    assertQuery("notexisting");
+    assertQuery("Not Existing");
+
+    // by full name works with any index version
+    assertQuery(quote(user1.name), user1);
+    assertQuery("name:" + quote(user1.name), user1);
+    assertQuery(quote(user2.name), user2);
+    assertQuery("name:" + quote(user2.name), user2);
+
+    // by self/me works with any index version
+    assertQuery("self", user3);
+    assertQuery("me", user3);
+
+    if (getSchemaVersion() < 8) {
+      assertMissingField(AccountField.NAME_PART_NO_SECONDARY_EMAIL);
+
+      // prefix queries only work if the NAME_PART_NO_SECONDARY_EMAIL field is available
+      assertQuery("john");
+      return;
+    }
+
+    assertQuery("John", user1);
+    assertQuery("john", user1);
+    assertQuery("Doe", user1);
+    assertQuery("doe", user1);
+    assertQuery("DOE", user1);
+    assertQuery("Jo Do", user1);
+    assertQuery("jo do", user1);
+    assertQuery("name:John", user1);
+    assertQuery("name:john", user1);
+    assertQuery("name:Doe", user1);
+    assertQuery("name:doe", user1);
+    assertQuery("name:DOE", user1);
+  }
+
+  @Test
   public void byWatchedProject() throws Exception {
     Project.NameKey p = createProject(name("p"));
     Project.NameKey p2 = createProject(name("p2"));
@@ -375,6 +468,11 @@
     List<AccountInfo> result = assertQuery(user1.username, user1);
     assertThat(result.get(0).secondaryEmails).isNull();
 
+    result = assertQuery(newQuery(user1.username).withSuggest(true), user1);
+    assertThat(result.get(0).secondaryEmails)
+        .containsExactlyElementsIn(Arrays.asList(secondaryEmails))
+        .inOrder();
+
     result = assertQuery(newQuery(user1.username).withOption(ListAccountsOption.DETAILS), user1);
     assertThat(result.get(0).secondaryEmails).isNull();
 
@@ -394,6 +492,21 @@
   }
 
   @Test
+  public void withSecondaryEmailsWithoutModifyAccountCapability() throws Exception {
+    AccountInfo user = newAccount("myuser", "My User", "abc@example.com", true);
+    String[] secondaryEmails = new String[] {"dfg@example.com", "hij@example.com"};
+    addEmails(user, secondaryEmails);
+
+    requestContext.setContext(newRequestContext(new Account.Id(user._accountId)));
+
+    List<AccountInfo> result = newQuery(user.username).withSuggest(true).get();
+    assertThat(result.get(0).secondaryEmails).isNull();
+
+    exception.expect(AuthException.class);
+    newQuery(user.username).withOption(ListAccountsOption.ALL_EMAILS).get();
+  }
+
+  @Test
   public void asAnonymous() throws Exception {
     AccountInfo user1 = newAccount("user1");
 
@@ -416,10 +529,10 @@
       PersonIdent ident = serverIdent.get();
       md.getCommitBuilder().setAuthor(ident);
       md.getCommitBuilder().setCommitter(ident);
-      AccountConfig accountConfig = new AccountConfig(null, accountId);
-      accountConfig.load(repo);
-      accountConfig.setAccountUpdate(InternalAccountUpdate.builder().setFullName(newName).build());
-      accountConfig.commit(md);
+      new AccountConfig(accountId, repo)
+          .load()
+          .setAccountUpdate(InternalAccountUpdate.builder().setFullName(newName).build())
+          .commit(md);
     }
 
     assertQuery("name:" + quote(user1.name), user1);
@@ -432,7 +545,7 @@
 
   @Test
   public void rawDocument() throws Exception {
-    AccountInfo userInfo = gApi.accounts().id(user.getAccountId().get()).get();
+    AccountInfo userInfo = gApi.accounts().id(admin.getAccountId().get()).get();
 
     Optional<FieldBundle> rawFields =
         indexes
@@ -633,6 +746,29 @@
     return accounts.stream().map(a -> a._accountId).collect(toList());
   }
 
+  protected void assertMissingField(FieldDef<AccountState, ?> field) {
+    assertThat(getSchema().hasField(field))
+        .named("schema %s has field %s", getSchemaVersion(), field.getName())
+        .isFalse();
+  }
+
+  protected void assertFailingQuery(String query, String expectedMessage) throws Exception {
+    try {
+      assertQuery(query);
+      fail("expected BadRequestException for query '" + query + "'");
+    } catch (BadRequestException e) {
+      assertThat(e.getMessage()).isEqualTo(expectedMessage);
+    }
+  }
+
+  protected int getSchemaVersion() {
+    return getSchema().getVersion();
+  }
+
+  protected Schema<AccountState> getSchema() {
+    return indexes.getSearchIndex().getSchema();
+  }
+
   /** Boiler plate code to check two byte arrays for equality */
   private static class ByteArrayWrapper {
     private byte[] arr;
@@ -654,8 +790,4 @@
       return Arrays.hashCode(arr);
     }
   }
-
-  protected int getSchemaVersion() {
-    return indexes.getSearchIndex().getSchema().getVersion();
-  }
 }
diff --git a/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java b/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
index 0bf9399..9b86c0e 100644
--- a/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
+++ b/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
@@ -106,7 +106,7 @@
     assertThat(myMenusFromApi(accountId).keySet()).containsExactlyElementsIn(newNames).inOrder();
   }
 
-  // Raw config values, bypassing the defaults set by GeneralPreferencesLoader.
+  // Raw config values, bypassing the defaults set by PreferencesConfig.
   private ImmutableMap<String, String> myMenusFromNoteDb(Account.Id id) throws Exception {
     try (Repository repo = repoManager.openRepository(allUsersName)) {
       VersionedAccountPreferences prefs = VersionedAccountPreferences.forUser(id);
diff --git a/javatests/com/google/gerrit/server/util/RegexListSearcherTest.java b/javatests/com/google/gerrit/server/util/RegexListSearcherTest.java
index dc8c0d8..01964a8 100644
--- a/javatests/com/google/gerrit/server/util/RegexListSearcherTest.java
+++ b/javatests/com/google/gerrit/server/util/RegexListSearcherTest.java
@@ -14,12 +14,10 @@
 
 package com.google.gerrit.server.util;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Ordering;
 import java.util.List;
 import org.junit.Test;
 
@@ -32,13 +30,6 @@
   }
 
   @Test
-  public void hasMatch() {
-    List<String> list = ImmutableList.of("bar", "foo", "quux");
-    assertTrue(RegexListSearcher.ofStrings("foo").hasMatch(list));
-    assertFalse(RegexListSearcher.ofStrings("xyz").hasMatch(list));
-  }
-
-  @Test
   public void anchors() {
     List<String> list = ImmutableList.of("foo");
     assertSearchReturns(list, "^f.*", list);
@@ -66,7 +57,9 @@
   }
 
   private void assertSearchReturns(List<?> expected, String re, List<String> inputs) {
-    assertTrue(Ordering.natural().isOrdered(inputs));
-    assertEquals(expected, ImmutableList.copyOf(RegexListSearcher.ofStrings(re).search(inputs)));
+    assertThat(inputs).isOrdered();
+    assertThat(RegexListSearcher.ofStrings(re).search(inputs))
+        .containsExactlyElementsIn(expected)
+        .inOrder();
   }
 }
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl
index b914b63..c035793 100644
--- a/lib/js/bower_archives.bzl
+++ b/lib/js/bower_archives.bzl
@@ -54,7 +54,7 @@
     sha1 = "01c485fbf898307029bbb72ac7e132db1570a842")
   bower_archive(
     name = "iron-flex-layout",
-    package = "polymerelements/iron-flex-layout",
+    package = "PolymerElements/iron-flex-layout",
     version = "1.3.7",
     sha1 = "4d4cf3232cf750a17a7df0a37476117f831ac633")
   bower_archive(
@@ -99,17 +99,12 @@
     sha1 = "588d289f779d02b21ce5b676e257bbd6155649e8")
   bower_archive(
     name = "paper-behaviors",
-    package = "polymerelements/paper-behaviors",
+    package = "PolymerElements/paper-behaviors",
     version = "1.0.13",
     sha1 = "a81eab28a952e124c208430e17508d9a1aae4ee7")
   bower_archive(
-    name = "paper-material",
-    package = "polymerelements/paper-material",
-    version = "1.0.7",
-    sha1 = "159b7fb6b13b181c4276b25f9c6adbeaacb0d42b")
-  bower_archive(
     name = "paper-ripple",
-    package = "polymerelements/paper-ripple",
+    package = "PolymerElements/paper-ripple",
     version = "1.0.10",
     sha1 = "21199db50d02b842da54bd6f4f1d1b10b474e893")
   bower_archive(
diff --git a/lib/js/bower_components.bzl b/lib/js/bower_components.bzl
index 79f40bb..fb40855 100644
--- a/lib/js/bower_components.bzl
+++ b/lib/js/bower_components.bzl
@@ -223,8 +223,7 @@
     deps = [
       ":iron-flex-layout",
       ":paper-behaviors",
-      ":paper-material",
-      ":paper-ripple",
+      ":paper-styles",
       ":polymer",
     ],
     seed = True,
@@ -266,14 +265,6 @@
     seed = True,
   )
   bower_component(
-    name = "paper-material",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":paper-styles",
-      ":polymer",
-    ],
-  )
-  bower_component(
     name = "paper-ripple",
     license = "//lib:LICENSE-polymer",
     deps = [
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 27f2c73..b33196a 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 27f2c73897da46d22b73c12cfdad282edb4e687c
+Subproject commit b33196a3da70e75ad00b5ac787620b29d20fed65
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
index b36733d..4792c30 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
@@ -69,6 +69,7 @@
           <input
               is="iron-input"
               id="tagNameInput"
+              maxlength="1024"
               bind-value="{{topic}}">
         </section>
         <section>
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
index c38a62c..4013b37 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
@@ -16,6 +16,7 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
+<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-menu-page-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -37,19 +38,14 @@
         justify-content: space-between;
         margin: .3em .7em;
       }
-      #deletedContainer {
-        border: 1px solid #d1d2d3;
-        padding: .7em;
-      }
       .rules {
         background: #fafafa;
         border: 1px solid #d1d2d3;
         border-bottom: 0;
       }
-      /* TODO @beckysiegel add back */
-      /* .editing .rules {
+      .editing .rules {
         border-bottom: 1px solid #d1d2d3;
-      } */
+      }
       .title {
         margin-bottom: .3em;
       }
@@ -57,19 +53,29 @@
       #removeBtn {
         display: none;
       }
-      /* TODO @beckysiegel add back */
-      /* .editing #removeBtn {
+      .right {
+        display: flex;
+        align-items: center;
+      }
+      .editing #removeBtn {
         display: block;
+        margin-left: 1.5em;
       }
       .editing #addRule {
         display: block;
         padding: .7em;
-      } */
+      }
       #deletedContainer,
       .deleted #mainContainer {
         display: none;
       }
-      .deleted #deletedContainer,
+      .deleted #deletedContainer {
+        align-items: baseline;
+        border: 1px solid #d1d2d3;
+        display: flex;
+        justify-content: space-between;
+        padding: .7em;
+      }
       #mainContainer {
         display: block;
       }
@@ -82,9 +88,17 @@
       <div id="mainContainer">
         <div class="header">
           <span class="title">[[name]]</span>
-          <gr-button
-              id="removeBtn"
-              on-tap="_handleRemovePermission">Remove</gr-button>
+          <div class="right">
+            <paper-toggle-button
+                id="exclusiveToggle"
+                checked="{{permission.value.exclusive}}"
+                on-change="_handleValueChange"
+                disabled$="[[!editing]]"></paper-toggle-button>Exclusive
+            <gr-button
+                link
+                id="removeBtn"
+                on-tap="_handleRemovePermission">Remove</gr-button>
+          </div>
         </div><!-- end header -->
         <div class="rules">
           <template
@@ -112,8 +126,9 @@
         </div> <!-- end rules -->
       </div><!-- end mainContainer -->
       <div id="deletedContainer">
-        [[name]] was deleted
+        <span>[[name]] was deleted</span>
         <gr-button
+            link
             id="undoRemoveBtn"
             on-tap="_handleUndoRemove">Undo</gr-button>
       </div><!-- end deletedContainer -->
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
index 2ba1eed..f9c04e60 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -16,6 +16,12 @@
 
   const MAX_AUTOCOMPLETE_RESULTS = 20;
 
+  /**
+   * Fired when the permission has been modified or removed.
+   *
+   * @event access-modified
+   */
+
   Polymer({
     is: 'gr-permission',
 
@@ -33,6 +39,7 @@
       editing: {
         type: Boolean,
         value: false,
+        observer: '_handleEditingChanged',
       },
       _label: {
         type: Object,
@@ -51,6 +58,7 @@
         type: Boolean,
         value: false,
       },
+      _originalExclusiveValue: Boolean,
     },
 
     behaviors: [
@@ -61,6 +69,53 @@
       '_handleRulesChanged(_rules.splices)',
     ],
 
+    listeners: {
+      'access-saved': '_handleAccessSaved',
+    },
+
+    ready() {
+      this._setupValues();
+    },
+
+    _setupValues() {
+      if (!this.permission) { return; }
+      this._originalExclusiveValue = !!this.permission.value.exclusive;
+      Polymer.dom.flush();
+    },
+
+    _handleAccessSaved() {
+      // Set a new 'original' value to keep track of after the value has been
+      // saved.
+      this._setupValues();
+    },
+
+    _handleEditingChanged(editing, editingOld) {
+      // Ignore when editing gets set initially.
+      if (!editingOld) { return; }
+      // Restore original values if no longer editing.
+      if (!editing) {
+        this._deleted = false;
+        this._groupFilter = '';
+        this._rules = this._rules.filter(rule => !rule.value.added);
+
+        // Restore exclusive bit to original.
+        this.set(['permission', 'value', 'exclusive'],
+            this._originalExclusiveValue);
+      }
+    },
+
+    _handleValueChange() {
+      this.permission.value.modified = true;
+      // Allows overall access page to know a change has been made.
+      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+    },
+
+    _handleRemovePermission() {
+      this._deleted = true;
+      this.permission.value.deleted = true;
+      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+    },
+
     _handleRulesChanged(changeRecord) {
       // Update the groups to exclude in the autocomplete.
       this._groupsWithRules = this._computeGroupsWithRules(this._rules);
@@ -70,11 +125,6 @@
       this._rules = this.toSortedArray(permission.value.rules);
     },
 
-    _handleRemovePermission() {
-      this._deleted = true;
-      this.set('permission.value.deleted', true);
-    },
-
     _computeSectionClass(editing, deleted) {
       const classList = [];
       if (editing) {
@@ -164,21 +214,26 @@
      * gr-rule-editor handles setting the default values.
      */
     _handleAddRuleItem(e) {
-      this.set(['permission', 'value', 'rules', e.detail.value.id], {});
+      // The group id is encoded, but have to decode in order for the access
+      // API to work as expected.
+      const groupId = decodeURIComponent(e.detail.value.id);
+      this.set(['permission', 'value', 'rules', groupId], {});
 
       // Purposely don't recompute sorted array so that the newly added rule
       // is the last item of the array.
       this.push('_rules', {
-        id: e.detail.value.id,
+        id: groupId,
       });
 
-      // Wait for new rule to get value populated via gr-rule editor, and then
+      // Wait for new rule to get value populated via gr-rule-editor, and then
       // add to permission values as well, so that the change gets propogated
       // back to the section. Since the rule is inside a dom-repeat, a flush
       // is needed.
       Polymer.dom.flush();
-      this.set(['permission', 'value', 'rules', e.detail.value.id],
-          this._rules[this._rules.length - 1].value);
+      const value = this._rules[this._rules.length - 1].value;
+      value.added = true;
+      this.set(['permission', 'value', 'rules', groupId], value);
+      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
     },
   });
 })();
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
index 179d221..b67d705 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -288,6 +288,7 @@
             },
           },
         };
+        element._setupValues();
         flushAsynchronousOperations();
       });
 
@@ -309,7 +310,7 @@
         assert.equal(element._rules.length, 3);
         assert.equal(Object.keys(element._groupsWithRules).length, 3);
         assert.deepEqual(element.permission.value.rules['newUserGroupId'],
-            {action: 'ALLOW', min: -2, max: 2});
+            {action: 'ALLOW', min: -2, max: 2, added: true});
       });
 
       test('removing the permission', () => {
@@ -326,6 +327,32 @@
         assert.isFalse(element.$.permission.classList.contains('deleted'));
         assert.isFalse(element._deleted);
       });
+
+      test('modify a permission', () => {
+        element.editing = true;
+        element.name = 'Priority';
+        element.section = 'refs/*';
+
+        assert.isFalse(element._originalExclusiveValue);
+        assert.isNotOk(element.permission.value.modified);
+        MockInteractions.tap(element.$.exclusiveToggle);
+        flushAsynchronousOperations();
+        assert.isTrue(element.permission.value.exclusive);
+        assert.isTrue(element.permission.value.modified);
+        assert.isFalse(element._originalExclusiveValue);
+        element.editing = false;
+        assert.isFalse(element.permission.value.exclusive);
+      });
+
+      test('_handleValueChange', () => {
+        const modifiedHandler = sandbox.stub();
+        element.permission = {value: {rules: {}}};
+        element.addEventListener('access-modified', modifiedHandler);
+        assert.isNotOk(element.permission.value.modified);
+        element._handleValueChange();
+        assert.isTrue(element.permission.value.modified);
+        assert.isTrue(modifiedHandler.called);
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
index 6f63843..0494e2c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
@@ -50,8 +50,9 @@
     </style>
     <style include="gr-menu-page-styles"></style>
     <main class$="[[_computeAdminClass(_isAdmin)]]">
-      <div class="gwtLink">This is currently in read only mode.  To modify content, go to the
+      <div class="gwtLink">Editing access in the new UI is a work in progress. Visit the
         <a href$="[[computeGwtUrl(path)]]" rel="external">Old UI</a>
+        if you need a feature that is not yet supported.
       </div>
       <template is="dom-if" if="[[_inheritsFrom]]">
         <h3 id="inheritsFrom">Rights Inherit From
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 2110aaa..d903404 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
@@ -156,7 +156,14 @@
       for (const item of path) {
         if (!curPos[item]) {
           if (item === path[path.length - 1] && type === 'remove') {
-            curPos[item] = null;
+            // 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 {
+              curPos[item] = null;
+            }
           } else if (item === path[path.length - 1] && type === 'add') {
             curPos[item] = opt_value;
           } else {
@@ -181,6 +188,9 @@
                 path.concat(k), 'remove');
             this._updateAddRemoveObj(addRemoveObj,
                 path.concat(k), 'add', obj[k]);
+          } else if (obj[k].added) {
+            this._updateAddRemoveObj(addRemoveObj,
+                path.concat(k), 'add', obj[k]);
           }
           this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj,
               path.concat(k));
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 cfa20d9..b26121c 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
@@ -76,46 +76,6 @@
         'Code-Review': {},
       },
     };
-    const repoAccessInput = {
-      add: {
-        'refs/*': {
-          permissions: {
-            owner: {
-              rules: {
-                123: {action: 'DENY', modified: true},
-              },
-            },
-          },
-        },
-      },
-      remove: {
-        'refs/*': {
-          permissions: {
-            owner: {
-              rules: {
-                123: null,
-              },
-            },
-          },
-        },
-      },
-    };
-
-    const repoAccessInputRemoved = {
-      add: {},
-      remove: {
-        'refs/*': {
-          permissions: {
-            owner: {
-              rules: {
-                123: null,
-              },
-            },
-          },
-        },
-      },
-    };
-
     setup(() => {
       sandbox = sinon.sandbox.create();
       element = fixture('basic');
@@ -142,14 +102,13 @@
           name: 'Create Account',
         },
       };
-
       const accessStub = sandbox.stub(element.$.restAPI,
           'getRepoAccessRights');
 
-
-      accessStub.withArgs('New Repo').returns(Promise.resolve(accessRes));
+      accessStub.withArgs('New Repo').returns(
+          Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
       accessStub.withArgs('Another New Repo')
-          .returns(Promise.resolve(accessRes2));
+          .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
       const capabilitiesStub = sandbox.stub(element.$.restAPI,
           'getCapabilities');
       capabilitiesStub.returns(Promise.resolve(capabilitiesRes));
@@ -184,26 +143,13 @@
           name: 'Access Database',
         },
       };
-      const accessRes = {
-        local: {
-          GLOBAL_CAPABILITIES: {
-            permissions: {
-              accessDatabase: {
-                rules: {
-                  123: {},
-                },
-              },
-            },
-          },
-        },
-      };
       const repoRes = {
         labels: {
           'Code-Review': {},
         },
       };
-      const accessStub = sandbox.stub(element.$.restAPI,
-          'getRepoAccessRights').returns(Promise.resolve(accessRes));
+      const accessStub = sandbox.stub(element.$.restAPI, 'getRepoAccessRights')
+          .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
       const capabilitiesStub = sandbox.stub(element.$.restAPI,
           'getCapabilities').returns(Promise.resolve(capabilitiesRes));
       const repoStub = sandbox.stub(element.$.restAPI, 'getRepo').returns(
@@ -244,7 +190,8 @@
 
     suite('with defined sections', () => {
       setup(() => {
-        element._sections = element.toSortedArray(accessRes.local);
+        element._sections =
+            element.toSortedArray(JSON.parse(JSON.stringify(accessRes.local)));
         flushAsynchronousOperations();
       });
 
@@ -280,20 +227,201 @@
         assert.isTrue(element._handleAccessModified.called);
       });
 
-      test('_computeAddAndRemove', () => {
-        // With nothing modified
-        element._local = accessRes.local;
+      test('_computeAddAndRemove rules', () => {
+        element._local = JSON.parse(JSON.stringify(accessRes.local));
         assert.deepEqual(element._computeAddAndRemove(), {add: {}, remove: {}});
         element._local['refs/*'].permissions.owner.rules[123].deleted = true;
-        assert.deepEqual(element._computeAddAndRemove(), repoAccessInputRemoved);
+        let expectedInput = {
+          add: {},
+          remove: {
+            'refs/*': {
+              permissions: {
+                owner: {
+                  rules: {
+                    123: null,
+                  },
+                },
+              },
+            },
+          },
+        };
+        assert.deepEqual(element._computeAddAndRemove(), expectedInput);
         delete element._local['refs/*'].permissions.owner.rules[123].deleted;
         element._local['refs/*'].permissions.owner.rules[123].modified = true;
-        assert.deepEqual(element._computeAddAndRemove(), repoAccessInput);
+        expectedInput = {
+          add: {
+            'refs/*': {
+              permissions: {
+                owner: {
+                  rules: {
+                    123: {action: 'DENY', modified: true},
+                  },
+                },
+              },
+            },
+          },
+          remove: {
+            'refs/*': {
+              permissions: {
+                owner: {
+                  rules: {
+                    123: null,
+                  },
+                },
+              },
+            },
+          },
+        };
+        assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+      });
+
+      test('_computeAddAndRemove permissions', () => {
+        element._local = JSON.parse(JSON.stringify(accessRes.local));
+        assert.deepEqual(element._computeAddAndRemove(), {add: {}, remove: {}});
+        element._local['refs/*'].permissions.owner.deleted = true;
+        let expectedInput = {
+          add: {},
+          remove: {
+            'refs/*': {
+              permissions: {
+                owner: {rules: {}},
+              },
+            },
+          },
+        };
+        assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+        delete element._local['refs/*'].permissions.owner.deleted;
+        element._local['refs/*'].permissions.owner.modified = true;
+        expectedInput = {
+          add: {
+            'refs/*': {
+              permissions: {
+                owner: {
+                  modified: true,
+                  rules: {
+                    234: {action: 'ALLOW'},
+                    123: {action: 'DENY'},
+                  },
+                },
+              },
+            },
+          },
+          remove: {
+            'refs/*': {
+              permissions: {
+                owner: {rules: {}},
+              },
+            },
+          },
+        };
+        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));
+        element._local['refs/*'].permissions.owner.rules[123].modified = true;
+        element._local['refs/*'].permissions.owner.deleted = true;
+        let expectedInput = {
+          add: {},
+          remove: {
+            'refs/*': {
+              permissions: {
+                owner: {rules: {}},
+              },
+            },
+          },
+        };
+        assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+        // Delete rule and delete permission that it is inside of.
+        element._local['refs/*'].permissions.owner.rules[123].modified = false;
+        element._local['refs/*'].permissions.owner.rules[123].deleted = true;
+        assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+        // Also modify a different rule inside of another permission.
+        element._local['refs/*'].permissions.read.modified = true;
+        expectedInput = {
+          add: {
+            'refs/*': {
+              permissions: {
+                read: {
+                  modified: true,
+                  rules: {
+                    234: {action: 'ALLOW'},
+                  },
+                },
+              },
+            },
+          },
+          remove: {
+            'refs/*': {
+              permissions: {
+                owner: {rules: {}},
+                read: {rules: {}},
+              },
+            },
+          },
+        };
+        assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+        // Modify both permissions with an exclusive bit. Owner is still
+        // deleted.
+        element._local['refs/*'].permissions.owner.exclusive = true;
+        element._local['refs/*'].permissions.owner.modified = true;
+        element._local['refs/*'].permissions.read.exclusive = true;
+        element._local['refs/*'].permissions.read.modified = true;
+        expectedInput = {
+          add: {
+            'refs/*': {
+              permissions: {
+                read: {
+                  exclusive: true,
+                  modified: true,
+                  rules: {
+                    234: {action: 'ALLOW'},
+                  },
+                },
+              },
+            },
+          },
+          remove: {
+            'refs/*': {
+              permissions: {
+                owner: {rules: {}},
+                read: {rules: {}},
+              },
+            },
+          },
+        };
+        assert.deepEqual(element._computeAddAndRemove(), expectedInput);
       });
 
       test('_handleSaveForReview', done => {
-        sandbox.stub(element.$.restAPI, 'getRepoAccessRights')
-            .returns(Promise.resolve(accessRes));
+        const repoAccessInput = {
+          add: {
+            'refs/*': {
+              permissions: {
+                owner: {
+                  rules: {
+                    123: {action: 'DENY', modified: true},
+                  },
+                },
+              },
+            },
+          },
+          remove: {
+            'refs/*': {
+              permissions: {
+                owner: {
+                  rules: {
+                    123: null,
+                  },
+                },
+              },
+            },
+          },
+        };
+        sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns(
+            Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
         sandbox.stub(element.$.restAPI, 'getRepo')
             .returns(Promise.resolve({}));
         sandbox.stub(Gerrit.Nav, 'navigateToChange');
@@ -302,8 +430,7 @@
             .returns(Promise.resolve({_number: 1}));
 
         element.repo = 'test-repo';
-        sandbox.stub(element, '_computeAddAndRemove')
-            .returns(repoAccessInput);
+        sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
 
         element._handleSaveForReview().then(() => {
           assert.isTrue(saveForReviewStub.called);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
index 96f229d..05d5d5c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
@@ -100,7 +100,7 @@
         }
       });
 
-      this.detailType = params.detailType;
+      this.detailType = params.detail;
 
       this._filter = this.getFilterValue(params);
       this._offset = this.getOffsetValue(params);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
index be6861c..695ecc7 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
@@ -98,7 +98,7 @@
 
         const params = {
           repo: 'test',
-          detailType: 'branches',
+          detail: 'branches',
         };
 
         element._paramsChanged(params).then(() => { flush(done); });
@@ -281,7 +281,7 @@
 
         const params = {
           repo: 'test',
-          detailType: 'branches',
+          detail: 'branches',
         };
 
         element._paramsChanged(params).then(() => { flush(done); });
@@ -298,7 +298,7 @@
           return Promise.resolve(branches);
         });
         const params = {
-          detailType: 'branches',
+          detail: 'branches',
           repo: 'test',
           filter: 'test',
           offset: 25,
@@ -341,7 +341,7 @@
 
         const params = {
           repo: 'test',
-          detailType: 'tags',
+          detail: 'tags',
         };
 
         element._paramsChanged(params).then(() => { flush(done); });
@@ -416,7 +416,7 @@
 
         const params = {
           repo: 'test',
-          detailType: 'tags',
+          detail: 'tags',
         };
 
         element._paramsChanged(params).then(() => { flush(done); });
@@ -434,7 +434,7 @@
         });
         const params = {
           repo: 'test',
-          detailType: 'tags',
+          detail: 'tags',
           filter: 'test',
           offset: 25,
         };
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
index ea0c0a4..b1964ff 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
@@ -103,7 +103,8 @@
                       id="submitTypeSelect"
                       bind-value="{{_repoConfig.submit_type}}">
                     <select disabled$="[[_readOnly]]">
-                      <template is="dom-repeat" items="[[_submitTypes]]">
+                      <template is="dom-repeat"
+                          items="[[_formatSubmitTypeSelect(_repoConfig)]]">
                         <option value="[[item.value]]">[[item.label]]</option>
                       </template>
                     </select>
@@ -265,6 +266,21 @@
                   </gr-select>
                 </span>
               </section>
+              <section>
+                <span class="title">Reject empty commit upon submit</span>
+                <span class="value">
+                  <gr-select
+                      id="rejectEmptyCommitSelect"
+                      bind-value="{{_repoConfig.reject_empty_commit.configured_value}}">
+                    <select disabled$="[[_readOnly]]">
+                      <template is="dom-repeat"
+                                items="[[_formatBooleanSelect(_repoConfig.reject_empty_commit)]]">
+                        <option value="[[item.value]]">[[item.label]]</option>
+                      </template>
+                  </select>
+                  </gr-select>
+                </span>
+              </section>
             </fieldset>
             <h3 id="Options">Contributor Agreements</h3>
             <fieldset id="agreements">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
index 5019f45..2febb25 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -21,6 +21,7 @@
   };
 
   const SUBMIT_TYPES = {
+    // Exclude INHERIT, which is handled specially.
     mergeIfNecessary: {
       value: 'MERGE_IF_NECESSARY',
       label: 'Merge if necessary',
@@ -129,6 +130,15 @@
 
       promises.push(this.$.restAPI.getProjectConfig(this.repo).then(
           config => {
+            if (config.default_submit_type) {
+              // The gr-select is bound to submit_type, which needs to be the
+              // *configured* submit type. When default_submit_type is
+              // present, the server reports the *effective* submit type in
+              // submit_type, so we need to overwrite it before storing the
+              // config in this.
+              config.submit_type =
+                  config.default_submit_type.configured_value;
+            }
             if (!config.state) {
               config.state = STATES.active.value;
             }
@@ -183,6 +193,36 @@
       ];
     },
 
+    _formatSubmitTypeSelect(projectConfig) {
+      if (!projectConfig) { return; }
+      const allValues = Object.values(SUBMIT_TYPES);
+      const type = projectConfig.default_submit_type;
+      if (!type) {
+        // Server is too old to report default_submit_type, so assume INHERIT
+        // is not a valid value.
+        return allValues;
+      }
+
+      let inheritLabel = 'Inherit';
+      if (type.inherited_value) {
+        let inherited = type.inherited_value;
+        for (const val of allValues) {
+          if (val.value === type.inherited_value) {
+            inherited = val.label;
+            break;
+          }
+        }
+        inheritLabel = `Inherit (${inherited})`;
+      }
+      return [
+        {
+          label: inheritLabel,
+          value: 'INHERIT',
+        },
+        ...allValues,
+      ];
+    },
+
     _isLoading() {
       return this._loading || this._loading === undefined;
     },
@@ -195,6 +235,12 @@
       const configInputObj = {};
       for (const key in p) {
         if (p.hasOwnProperty(key)) {
+          if (key === 'default_submit_type') {
+            // default_submit_type is not in the input type, and the
+            // configured value was already copied to submit_type by
+            // _loadProject. Omit this property when saving.
+            continue;
+          }
           if (typeof p[key] === 'object') {
             configInputObj[key] = p[key].configured_value;
           } else {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
index f44beae..9ea0177 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
@@ -93,12 +93,21 @@
               value: false,
               configured_value: 'FALSE',
             },
+            reject_empty_commit: {
+              value: false,
+              configured_value: 'FALSE',
+            },
             enable_reviewer_by_email: {
               value: false,
               configured_value: 'FALSE',
             },
             max_object_size_limit: {},
             submit_type: 'MERGE_IF_NECESSARY',
+            default_submit_type: {
+              value: 'MERGE_IF_NECESSARY',
+              configured_value: 'INHERIT',
+              inherited_value: 'MERGE_IF_NECESSARY',
+            },
           });
         },
         getConfig() {
@@ -252,7 +261,16 @@
         });
       });
 
-      test('fields update and save correctly', done => {
+      test('inherited submit type value is calculated correctly', () => {
+        return element._loadRepo().then(() => {
+          const sel = element.$.submitTypeSelect;
+          assert.equal(sel.bindValue, 'INHERIT');
+          assert.equal(
+              sel.nativeSelect.options[0].text, 'Inherit (Merge if necessary)');
+        });
+      });
+
+      test('fields update and save correctly', () => {
         // test notedb
         element._noteDbEnabled = false;
 
@@ -276,6 +294,7 @@
           reject_implicit_merges: 'TRUE',
           private_by_default: 'TRUE',
           match_author_to_committer_date: 'TRUE',
+          reject_empty_commit: 'TRUE',
           max_object_size_limit: 10,
           submit_type: 'FAST_FORWARD_ONLY',
           state: 'READ_ONLY',
@@ -289,7 +308,7 @@
 
         const button = Polymer.dom(element.root).querySelector('gr-button');
 
-        element._loadRepo().then(() => {
+        return element._loadRepo().then(() => {
           assert.isTrue(button.hasAttribute('disabled'));
           assert.isFalse(element.$.Title.classList.contains('edited'));
           element.$.descriptionInput.bindValue = configInputObj.description;
@@ -317,6 +336,8 @@
               configInputObj.use_contributor_agreements;
           element.$.useSignedOffBySelect.bindValue =
               configInputObj.use_signed_off_by;
+          element.$.rejectEmptyCommitSelect.bindValue =
+              configInputObj.reject_empty_commit;
           element.$.unRegisteredCcSelect.bindValue =
               configInputObj.enable_reviewer_by_email;
 
@@ -327,12 +348,11 @@
               element._formatRepoConfigForSave(element._repoConfig);
           assert.deepEqual(formattedObj, configInputObj);
 
-          element._handleSaveRepoConfig().then(() => {
+          return element._handleSaveRepoConfig().then(() => {
             assert.isTrue(button.hasAttribute('disabled'));
             assert.isFalse(element.$.Title.classList.contains('edited'));
             assert.isTrue(saveStub.lastCall.calledWithExactly(REPO,
                 configInputObj));
-            done();
           });
         });
       });
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index de33fa1..a3b0272 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -196,6 +196,9 @@
     },
 
     _handleUndoChange() {
+      // gr-permission will take care of removing rules that were added but
+      // unsaved. We need to keep the added bit for the filter.
+      if (this.rule.value.added) { return; }
       this.set('rule.value', Object.assign({}, this._originalRuleValues));
       this._deleted = false;
       delete this.rule.value.deleted;
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index b076c83..66ab290 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -28,7 +28,9 @@
 <link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
 <link rel="import" href="../../shared/gr-label/gr-label.html">
 <link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html">
+<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-commit-info/gr-commit-info.html">
 <link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
 
 <dom-module id="gr-change-metadata">
@@ -127,6 +129,20 @@
       #externalStyle {
         display: block;
       }
+      .parentList.merge {
+        list-style-type: decimal;
+        padding-left: 1em;
+      }
+      .parentList gr-commit-info {
+        display: inline-block;
+      }
+      #parentNotCurrentMessage {
+        display: none;
+      }
+      .parentList.notCurrent.nonMerge #parentNotCurrentMessage {
+        --arrow-color: #ffa62f;
+        display: inline-block;
+      }
       @media screen and (max-width: 50em), screen and (min-width: 75em) {
         :host {
           display: table;
@@ -225,6 +241,26 @@
           <a href$="[[_computeBranchURL(change.project, change.branch)]]">[[change.branch]]</a>
         </span>
       </section>
+      <section>
+        <span class="title">[[_computeParentsLabel(_currentParents)]]</span>
+        <span class="value">
+          <ol class$="[[_computeParentListClass(_currentParents, parentIsCurrent)]]">
+            <template is="dom-repeat" items="[[_currentParents]]" as="parent">
+              <li>
+                <gr-commit-info
+                    change="[[change]]"
+                    commit-info="[[parent]]"
+                    server-config="[[serverConfig]]"></gr-commit-info>
+                <gr-tooltip-content
+                    id="parentNotCurrentMessage"
+                    has-tooltip
+                    show-icon
+                    title$="[[_notCurrentMessage]]"></gr-tooltip-content>
+              </li>
+            </template>
+          </ol>
+        </span>
+      </section>
       <section class="topic">
         <span class="title">Topic</span>
         <span class="value">
@@ -244,6 +280,7 @@
             <gr-editable-label
                 label-text="Add a topic"
                 value="[[change.topic]]"
+                max-length="1024"
                 placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]"
                 read-only="[[_topicReadOnly]]"
                 on-changed="_handleTopicChanged"></gr-editable-label>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index fb5771c..ddee577 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -25,6 +25,8 @@
     CHERRY_PICK: 'Cherry Pick',
   };
 
+  const NOT_CURRENT_MESSAGE = 'Not current - rebase possible';
+
   Polymer({
     is: 'gr-change-metadata',
 
@@ -46,6 +48,12 @@
        * @type {{ note_db_enabled: string }}
        */
       serverConfig: Object,
+      parentIsCurrent: Boolean,
+      _notCurrentMessage: {
+        type: String,
+        value: NOT_CURRENT_MESSAGE,
+        readOnly: true,
+      },
       _topicReadOnly: {
         type: Boolean,
         computed: '_computeTopicReadOnly(mutable, change)',
@@ -74,6 +82,11 @@
         type: Boolean,
         value: false,
       },
+
+      _currentParents: {
+        type: Array,
+        computed: '_computeParents(change)',
+      },
     },
 
     behaviors: [
@@ -406,5 +419,26 @@
 
       return rev.uploader;
     },
+
+    _computeParents(change) {
+      if (!change.current_revision ||
+          !change.revisions[change.current_revision] ||
+          !change.revisions[change.current_revision].commit) {
+        return undefined;
+      }
+      return change.revisions[change.current_revision].commit.parents;
+    },
+
+    _computeParentsLabel(parents) {
+      return parents.length > 1 ? 'Parents' : 'Parent';
+    },
+
+    _computeParentListClass(parents, parentIsCurrent) {
+      return [
+        'parentList',
+        parents.length > 1 ? 'merge' : 'nonMerge',
+        parentIsCurrent ? 'current' : 'notCurrent',
+      ].join(' ');
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 53e896b..9ee09ea 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -318,6 +318,36 @@
       assert.equal(actual, '');
     });
 
+    test('_computeParents', () => {
+      const parents = [{commit: '123', subject: 'abc'}];
+      assert.isUndefined(element._computeParents(
+          {revisions: {456: {commit: {parents}}}}));
+      assert.isUndefined(element._computeParents(
+          {current_revision: '789', revisions: {456: {commit: {parents}}}}));
+      assert.equal(element._computeParents(
+          {current_revision: '456', revisions: {456: {commit: {parents}}}}),
+          parents);
+    });
+
+    test('_computeParentsLabel', () => {
+      const parent = {commit: 'abc123', subject: 'My parent commit'};
+      assert.equal(element._computeParentsLabel([parent]), 'Parent');
+      assert.equal(element._computeParentsLabel([parent, parent]),
+          'Parents');
+    });
+
+    test('_computeParentListClass', () => {
+      const parent = {commit: 'abc123', subject: 'My parent commit'};
+      assert.equal(element._computeParentListClass([parent], true),
+          'parentList nonMerge current');
+      assert.equal(element._computeParentListClass([parent], false),
+          'parentList nonMerge notCurrent');
+      assert.equal(element._computeParentListClass([parent, parent], false),
+          'parentList merge notCurrent');
+      assert.equal(element._computeParentListClass([parent, parent], true),
+          'parentList merge current');
+    });
+
     test('_showAddTopic', () => {
       assert.isTrue(element._showAddTopic(null, false));
       assert.isTrue(element._showAddTopic({base: {topic: null}}, false));
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 3bfde5c..7e661c7 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
@@ -104,7 +104,7 @@
          https://github.com/Polymer/polymer/issues/2531 */
       .container section.changeInfo {
         display: flex;
-        padding: 1em var(--default-horizontal-margin);
+        padding: 0 var(--default-horizontal-margin);
       }
       .changeId {
         color: #666;
@@ -116,7 +116,9 @@
         padding-right: 1em;
       }
       .changeMetadata {
+        border-right: 1px solid #ddd;
         font-size: .95em;
+        padding: 1em 0;
       }
       /* Prevent plugin text from overflowing. */
       #change_plugins {
@@ -164,6 +166,7 @@
       .relatedChanges {
         flex: 1 1 auto;
         overflow: hidden;
+        padding: 1em 0;
       }
       .mobile {
         display: none;
@@ -193,6 +196,7 @@
         display: flex;
         flex-direction: column;
         flex-shrink: 0;
+        margin: 1em 0;
       }
       .collapseToggleContainer {
         display: flex;
@@ -366,6 +370,7 @@
               server-config="[[_serverConfig]]"
               missing-labels="[[_missingLabels]]"
               mutable="[[_loggedIn]]"
+              parent-is-current="[[!_rebaseOriginallyEnabled]]"
               on-show-reply-dialog="_handleShowReplyDialog">
           </gr-change-metadata>
           <!-- Plugins insert content into following container.
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 9cf029b..fd9a490 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
@@ -224,6 +224,7 @@
         value: false,
         observer: '_updateToggleContainerClass',
       },
+      _rebaseOriginallyEnabled: Boolean,
     },
 
     behaviors: [
@@ -937,6 +938,7 @@
       if (revisionActions && revisionActions.rebase) {
         revisionActions.rebase.rebaseOnCurrent =
             !!revisionActions.rebase.enabled;
+        this._rebaseOriginallyEnabled = !!revisionActions.rebase.enabled;
         revisionActions.rebase.enabled = true;
       }
       return revisionActions;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index 3189cd4..c92919d 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -118,15 +118,6 @@
       .fileViewActions > *:not(:last-child) {
         margin-right: 5px;
       }
-      .separator {
-        background-color: rgba(0, 0, 0, .3);
-        height: 20px;
-        margin: 0 8px;
-        width: 1px;
-      }
-      .separator.transparent {
-        background-color: transparent;
-      }
       .editLoaded .hideOnEdit {
         display: none;
       }
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
index 3c09797..df9ce54 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -15,6 +15,7 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../gr-message/gr-message.html">
@@ -57,15 +58,6 @@
       #messageControlsContainer gr-button {
         padding: 0.4em 0;
       }
-      .separator {
-        background-color: rgba(0, 0, 0, .3);
-        height: 1.5em;
-        margin: 0 .6em;
-        width: 1px;
-      }
-      .separator.transparent {
-        background-color: transparent;
-      }
       .container {
         align-items: center;
         display: flex;
@@ -78,12 +70,9 @@
             id="automatedMessageToggleContainer"
             class="container"
             hidden$="[[!_hasAutomatedMessages(messages)]]">
-          <gr-button
+          <paper-toggle-button
               id="automatedMessageToggle"
-              link
-              on-tap="_handleAutomatedMessageToggleTap">
-            [[_computeAutomatedToggleText(_hideAutomated)]]
-          </gr-button>
+              checked="{{_hideAutomated}}"></paper-toggle-button>Only comments
           <span class="transparent separator"></span>
         </span>
         <gr-button
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index e2c62f3..9ccadf7 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -175,12 +175,6 @@
       this.handleExpandCollapse(!this._expanded);
     },
 
-    _handleAutomatedMessageToggleTap(e) {
-      e.preventDefault();
-
-      this._hideAutomated = !this._hideAutomated;
-    },
-
     _handleScrollTo(e) {
       this.scrollToMessage(e.detail.message.id);
     },
@@ -199,10 +193,6 @@
       return expanded ? 'Collapse all' : 'Expand all';
     },
 
-    _computeAutomatedToggleText(hideAutomated) {
-      return hideAutomated ? 'Show all messages' : 'Show comments only';
-    },
-
     /**
      * Computes message author's file comments for change's message.
      * Method uses this.messages to find next message and relies on messages
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index d0ed22f..453d97a 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -484,7 +484,7 @@
       assert.isFalse(!!allHiddenMessageEls.length);
     });
 
-    test('autogenerated messages hidden after hide button tap', () => {
+    test('autogenerated messages hidden after comments only toggle', () => {
       let allHiddenMessageEls = getHiddenMessages();
 
       element._hideAutomated = false;
@@ -497,16 +497,17 @@
       assert.equal(allHiddenMessageEls.length, allMessageEls.length);
     });
 
-    test('autogenerated messages not hidden after show button tap', () => {
-      let allHiddenMessageEls = getHiddenMessages();
+    test('autogenerated messages not hidden after comments only toggle',
+        () => {
+          let allHiddenMessageEls = getHiddenMessages();
 
-      element._hideAutomated = true;
-      MockInteractions.tap(element.$.automatedMessageToggle);
-      allHiddenMessageEls = getHiddenMessages();
+          element._hideAutomated = true;
+          MockInteractions.tap(element.$.automatedMessageToggle);
+          allHiddenMessageEls = getHiddenMessages();
 
-      // Autogenerated messages are now hidden.
-      assert.isFalse(!!allHiddenMessageEls.length);
-    });
+          // Autogenerated messages are now hidden.
+          assert.isFalse(!!allHiddenMessageEls.length);
+        });
 
     test('_getDelta', () => {
       let messages = [randomMessage()];
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 42c6147..46b803f 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -266,7 +266,7 @@
           <span
               id="notLatestLabel"
               hidden$="[[!_isState(knownLatestState, 'not-latest')]]">
-            Patch [[patchNum]] is not latest.
+            [[_computePatchSetWarning(patchNum, _labelsChanged)]]
             <gr-button link on-tap="_reload">Reload</gr-button>
           </span>
         </div>
@@ -279,7 +279,7 @@
           <gr-button
               link
               primary
-              disabled="[[_computeSendButtonDisabled(knownLatestState, _sendButtonLabel, diffDrafts, draft, _reviewersMutated, _labelsChanged, _includeComments)]]"
+              disabled="[[_computeSendButtonDisabled(_sendButtonLabel, diffDrafts, draft, _reviewersMutated, _labelsChanged, _includeComments)]]"
               class="action send"
               has-tooltip
               title$="[[_computeSendButtonTooltip(canBeStarted)]]"
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 1a14ae1..35bdfb0 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -406,12 +406,6 @@
     },
 
     send(includeComments, startReview) {
-      if (this.knownLatestState === 'not-latest') {
-        this.fire('show-alert',
-            {message: 'Cannot reply to non-latest patch.'});
-        return Promise.resolve({});
-      }
-
       const labels = this.$.labelScores.getLabelValues();
 
       const obj = {
@@ -835,16 +829,21 @@
       return savingComments ? 'saving' : '';
     },
 
-    _computeSendButtonDisabled(knownLatestState, buttonLabel, drafts, text,
-        reviewersMutated, labelsChanged, includeComments) {
-      if (this._isState(knownLatestState, LatestPatchState.NOT_LATEST)) {
-        return true;
-      }
+    _computeSendButtonDisabled(buttonLabel, drafts, text, reviewersMutated,
+        labelsChanged, includeComments) {
       if (buttonLabel === ButtonLabels.START_REVIEW) {
         return false;
       }
       const hasDrafts = includeComments && Object.keys(drafts).length;
       return !hasDrafts && !text.length && !reviewersMutated && !labelsChanged;
     },
+
+    _computePatchSetWarning(patchNum, labelsChanged) {
+      let str = `Patch ${patchNum} is not latest.`;
+      if (labelsChanged) {
+        str += ' Voting on a non-latest patch will have no effect.';
+      }
+      return str;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 1b88ec0..8d91ef8 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -1083,21 +1083,20 @@
 
     test('_computeSendButtonDisabled', () => {
       const fn = element._computeSendButtonDisabled.bind(element);
-      assert.isTrue(fn('not-latest'));
-      assert.isFalse(fn('latest', 'Start review'));
-      assert.isTrue(fn('latest', 'Send', {}, '', false, false, false));
+      assert.isFalse(fn('Start review'));
+      assert.isTrue(fn('Send', {}, '', false, false, false));
       // Mock nonempty comment draft array, with seding comments.
-      assert.isFalse(fn('latest', 'Send', {file: ['draft']}, '', false, false,
+      assert.isFalse(fn('Send', {file: ['draft']}, '', false, false,
           true));
       // Mock nonempty comment draft array, without seding comments.
-      assert.isTrue(fn('latest', 'Send', {file: ['draft']}, '', false, false,
+      assert.isTrue(fn('Send', {file: ['draft']}, '', false, false,
           false));
       // Mock nonempty change message.
-      assert.isFalse(fn('latest', 'Send', {}, 'test', false, false, false));
+      assert.isFalse(fn('Send', {}, 'test', false, false, false));
       // Mock reviewers mutated.
-      assert.isFalse(fn('latest', 'Send', {}, '', true, false, false));
+      assert.isFalse(fn('Send', {}, '', true, false, false));
       // Mock labels changed.
-      assert.isFalse(fn('latest', 'Send', {}, '', false, true, false));
+      assert.isFalse(fn('Send', {}, '', false, true, false));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index af2b9e6..dc264f2 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -21,7 +21,8 @@
     CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
     PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/,
 
-    AGREEMENTS: /^\/settings\/(agreements|new-agreement)/,
+    AGREEMENTS: /^\/settings\/agreements\/?/,
+    NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/,
     REGISTER: /^\/register(\/.*)?$/,
 
     // Pattern for login and logout URLs intended to be passed-through. May
@@ -771,6 +772,9 @@
 
       this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
 
+      this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute',
+          true);
+
       this._mapRoute(RoutePattern.SETTINGS_LEGACY,
           '_handleSettingsLegacyRoute', true);
 
@@ -1032,7 +1036,6 @@
       this._setParams({
         view: Gerrit.Nav.View.REPO,
         detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-        detailType: 'branches',
         repo: data.params[0],
         offset: data.params[2] || 0,
         filter: null,
@@ -1043,7 +1046,6 @@
       this._setParams({
         view: Gerrit.Nav.View.REPO,
         detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-        detailType: 'branches',
         repo: data.params.repo,
         offset: data.params.offset,
         filter: data.params.filter,
@@ -1054,7 +1056,6 @@
       this._setParams({
         view: Gerrit.Nav.View.REPO,
         detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-        detailType: 'branches',
         repo: data.params.repo,
         filter: data.params.filter || null,
       });
@@ -1064,7 +1065,6 @@
       this._setParams({
         view: Gerrit.Nav.View.REPO,
         detail: Gerrit.Nav.RepoDetailView.TAGS,
-        detailType: 'tags',
         repo: data.params[0],
         offset: data.params[2] || 0,
         filter: null,
@@ -1075,7 +1075,6 @@
       this._setParams({
         view: Gerrit.Nav.View.REPO,
         detail: Gerrit.Nav.RepoDetailView.TAGS,
-        detailType: 'tags',
         repo: data.params.repo,
         offset: data.params.offset,
         filter: data.params.filter,
@@ -1086,7 +1085,6 @@
       this._setParams({
         view: Gerrit.Nav.View.REPO,
         detail: Gerrit.Nav.RepoDetailView.TAGS,
-        detailType: 'tags',
         repo: data.params.repo,
         filter: data.params.filter || null,
       });
@@ -1272,7 +1270,13 @@
       }
     },
 
+    // TODO fix this so it properly redirects
+    // to /settings#Agreements (Scrolls down)
     _handleAgreementsRoute(data) {
+      this._redirect('/settings/#Agreements');
+    },
+
+    _handleNewAgreementsRoute(data) {
       data.params.view = Gerrit.Nav.View.AGREEMENTS;
       this._setParams(data.params);
     },
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index ae002af..7011c65 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -134,6 +134,7 @@
         '_handleGroupListOffsetRoute',
         '_handleGroupMembersRoute',
         '_handleGroupRoute',
+        '_handleNewAgreementsRoute',
         '_handlePluginListFilterOffsetRoute',
         '_handlePluginListFilterRoute',
         '_handlePluginListOffsetRoute',
@@ -532,7 +533,14 @@
       });
 
       test('_handleAgreementsRoute', () => {
-        element._handleAgreementsRoute({params: {}});
+        const data = {params: {}};
+        element._handleAgreementsRoute(data);
+        assert.isTrue(redirectStub.calledOnce);
+        assert.equal(redirectStub.lastCall.args[0], '/settings/#Agreements');
+      });
+
+      test('_handleNewAgreementsRoute', () => {
+        element._handleNewAgreementsRoute({params: {}});
         assert.isTrue(setParamsStub.calledOnce);
         assert.equal(setParamsStub.lastCall.args[0].view,
             Gerrit.Nav.View.AGREEMENTS);
@@ -956,7 +964,6 @@
             assertDataToParams(data, '_handleBranchListOffsetRoute', {
               view: Gerrit.Nav.View.REPO,
               detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-              detailType: 'branches',
               repo: 4321,
               offset: 0,
               filter: null,
@@ -966,7 +973,6 @@
             assertDataToParams(data, '_handleBranchListOffsetRoute', {
               view: Gerrit.Nav.View.REPO,
               detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-              detailType: 'branches',
               repo: 4321,
               offset: 42,
               filter: null,
@@ -978,7 +984,6 @@
             assertDataToParams(data, '_handleBranchListFilterOffsetRoute', {
               view: Gerrit.Nav.View.REPO,
               detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-              detailType: 'branches',
               repo: 4321,
               offset: 42,
               filter: 'foo',
@@ -990,7 +995,6 @@
             assertDataToParams(data, '_handleBranchListFilterRoute', {
               view: Gerrit.Nav.View.REPO,
               detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-              detailType: 'branches',
               repo: 4321,
               filter: 'foo',
             });
@@ -1003,7 +1007,6 @@
             assertDataToParams(data, '_handleTagListOffsetRoute', {
               view: Gerrit.Nav.View.REPO,
               detail: Gerrit.Nav.RepoDetailView.TAGS,
-              detailType: 'tags',
               repo: 4321,
               offset: 0,
               filter: null,
@@ -1015,7 +1018,6 @@
             assertDataToParams(data, '_handleTagListFilterOffsetRoute', {
               view: Gerrit.Nav.View.REPO,
               detail: Gerrit.Nav.RepoDetailView.TAGS,
-              detailType: 'tags',
               repo: 4321,
               offset: 42,
               filter: 'foo',
@@ -1027,7 +1029,6 @@
             assertDataToParams(data, '_handleTagListFilterRoute', {
               view: Gerrit.Nav.View.REPO,
               detail: Gerrit.Nav.RepoDetailView.TAGS,
-              detailType: 'tags',
               repo: 4321,
               filter: null,
             });
@@ -1036,7 +1037,6 @@
             assertDataToParams(data, '_handleTagListFilterRoute', {
               view: Gerrit.Nav.View.REPO,
               detail: Gerrit.Nav.RepoDetailView.TAGS,
-              detailType: 'tags',
               repo: 4321,
               filter: 'foo',
             });
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index 1ca745e..a6f27489 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -19,6 +19,7 @@
     'added:',
     'age:',
     'age:1week', // Give an example age
+    'assignee:',
     'author:',
     'branch:',
     'bug:',
@@ -44,6 +45,7 @@
     'intopic:',
     'is:',
     'is:abandoned',
+    'is:assigned',
     'is:closed',
     'is:ignored',
     'is:mergeable',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
index 7e6d54d..ab2077d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -148,7 +148,7 @@
         <gr-button id="saveButton" link primary on-tap="_handleSave">
             Save</gr-button>
       </div>
-    </overlay>
+    </gr-overlay>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
   </template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index e5f525e..0952ebd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -73,8 +73,13 @@
         color: #999;
       }
       .navLinks {
+        align-items: center;
+        display: flex;
         white-space: nowrap;
       }
+      .navLink {
+        padding: 0 .25em;
+      }
       .reviewed {
         display: inline-block;
         margin: 0 .25em;
@@ -107,9 +112,6 @@
       .prefsButton {
         text-align: right;
       }
-      .separator {
-        margin: 0 .25em;
-      }
       .noOverflow {
         display: block;
         overflow: auto;
@@ -120,8 +122,12 @@
       .blameLoader {
         display: none;
       }
-      .blameLoader.show {
-        display: inline;
+      .blameLoader.show,
+      .download,
+      .preferences,
+      .rightControls {
+        align-items: center;
+        display: flex;
       }
       gr-dropdown-list {
         --trigger-style: {
@@ -211,11 +217,11 @@
           <a class="navLink"
               href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
             Prev</a>
-          /
+          <span class="separator"></span>
           <a class="navLink"
               href$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">
             Up</a>
-          /
+          <span class="separator"></span>
           <a class="navLink"
               href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
             Next</a>
@@ -236,7 +242,7 @@
               on-patch-range-change="_handlePatchChange">
           </gr-patch-range-select>
           <span class="download desktop">
-            <span class="separator">/</span>
+            <span class="separator"></span>
             <a
               class="downloadLink"
               download
@@ -245,7 +251,7 @@
             </a>
           </span>
         </div>
-        <div>
+        <div class="rightControls">
           <gr-select
               id="modeSelect"
               bind-value="{{changeViewState.diffMode}}"
@@ -258,15 +264,14 @@
           <span id="diffPrefsContainer"
               hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]" hidden>
             <span class="preferences desktop">
-              <span
-                  hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">/</span>
+              <span class="separator" hidden$="[[_computeModeSelectHidden(_isImageDiff)]]"></span>
               <gr-button link
                   class="prefsButton"
                   on-tap="_handlePrefsTap">Preferences</gr-button>
             </span>
           </span>
           <span class$="blameLoader [[_computeBlameLoaderClass(_isImageDiff, _isBlameSupported)]]">
-            <span class="separator">/</span>
+            <span class="separator"></span>
             <gr-button
                 link
                 disabled="[[_isBlameLoading]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 8c2954b..de9905a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -307,17 +307,30 @@
     },
 
     _moveToPreviousFileWithComment() {
-      if (this._commentSkips && this._commentSkips.previous) {
-        Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.previous,
-            this._patchRange.patchNum, this._patchRange.basePatchNum);
+      if (!this._commentSkips) { return; }
+
+      // If there is no previous diff with comments, then return to the change
+      // view.
+      if (!this._commentSkips.previous) {
+        this._navToChangeView();
+        return;
       }
+
+      Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.previous,
+          this._patchRange.patchNum, this._patchRange.basePatchNum);
     },
 
     _moveToNextFileWithComment() {
-      if (this._commentSkips && this._commentSkips.next) {
-        Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.next,
-            this._patchRange.patchNum, this._patchRange.basePatchNum);
+      if (!this._commentSkips) { return; }
+
+      // If there is no next diff with comments, then return to the change view.
+      if (!this._commentSkips.next) {
+        this._navToChangeView();
+        return;
       }
+
+      Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.next,
+          this._patchRange.patchNum, this._patchRange.basePatchNum);
     },
 
     _handleCKey(e) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 8fe8c40..979f253 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -769,6 +769,88 @@
         assert.equal(result.previous, fileList[1]);
         assert.isNull(result.next);
       });
+
+      suite('skip next/previous', () => {
+        let navToChangeStub;
+        let navToDiffStub;
+
+        setup(() => {
+          navToChangeStub = sandbox.stub(element, '_navToChangeView');
+          navToDiffStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+          element._fileList = [
+            'path/one.jpg', 'path/two.m4v', 'path/three.wav',
+          ];
+          element._patchRange = {patchNum: '2', basePatchNum: '1'};
+        });
+
+        suite('_moveToPreviousFileWithComment', () => {
+          test('no skips', () => {
+            element._moveToPreviousFileWithComment();
+            assert.isFalse(navToChangeStub.called);
+            assert.isFalse(navToDiffStub.called);
+          });
+
+          test('no previous', () => {
+            const commentMap = {};
+            commentMap[element._fileList[0]] = false;
+            commentMap[element._fileList[1]] = false;
+            commentMap[element._fileList[2]] = true;
+            element._commentMap = commentMap;
+            element._path = element._fileList[1];
+
+            element._moveToPreviousFileWithComment();
+            assert.isTrue(navToChangeStub.calledOnce);
+            assert.isFalse(navToDiffStub.called);
+          });
+
+          test('w/ previous', () => {
+            const commentMap = {};
+            commentMap[element._fileList[0]] = true;
+            commentMap[element._fileList[1]] = false;
+            commentMap[element._fileList[2]] = true;
+            element._commentMap = commentMap;
+            element._path = element._fileList[1];
+
+            element._moveToPreviousFileWithComment();
+            assert.isFalse(navToChangeStub.called);
+            assert.isTrue(navToDiffStub.calledOnce);
+          });
+        });
+
+        suite('_moveToNextFileWithComment', () => {
+          test('no skips', () => {
+            element._moveToNextFileWithComment();
+            assert.isFalse(navToChangeStub.called);
+            assert.isFalse(navToDiffStub.called);
+          });
+
+          test('no previous', () => {
+            const commentMap = {};
+            commentMap[element._fileList[0]] = true;
+            commentMap[element._fileList[1]] = false;
+            commentMap[element._fileList[2]] = false;
+            element._commentMap = commentMap;
+            element._path = element._fileList[1];
+
+            element._moveToNextFileWithComment();
+            assert.isTrue(navToChangeStub.calledOnce);
+            assert.isFalse(navToDiffStub.called);
+          });
+
+          test('w/ previous', () => {
+            const commentMap = {};
+            commentMap[element._fileList[0]] = true;
+            commentMap[element._fileList[1]] = false;
+            commentMap[element._fileList[2]] = true;
+            element._commentMap = commentMap;
+            element._path = element._fileList[1];
+
+            element._moveToNextFileWithComment();
+            assert.isFalse(navToChangeStub.called);
+            assert.isTrue(navToDiffStub.calledOnce);
+          });
+        });
+      });
     });
 
     test('_computeEditLoaded', () => {
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index cca723e..ce0cd21 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -141,6 +141,7 @@
           this._newContent).then(res => {
             this._saving = false;
             this._showAlert(res.ok ? SAVED_MESSAGE : SAVE_FAILED_MSG);
+            if (res.ok) { this._content = this._newContent; }
           });
     },
 
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
index d00dcc3..3cd5608 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
@@ -167,6 +167,7 @@
             [mockParams.changeNum, mockParams.path, newText]);
         assert.isFalse(navigateStub.called);
         assert.isFalse(element.$.save.hasAttribute('disabled'));
+        assert.notEqual(element._content, element._newContent);
       });
     });
 
@@ -191,7 +192,8 @@
         assert.isFalse(element._saving);
         assert.equal(alertStub.lastCall.args[0], 'All changes saved');
         assert.isFalse(navigateStub.called);
-        assert.isFalse(element.$.save.hasAttribute('disabled'));
+        assert.isTrue(element.$.save.hasAttribute('disabled'));
+        assert.equal(element._content, element._newContent);
       });
     });
 
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 95cddab..ef93794 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -179,7 +179,7 @@
         </gr-endpoint-decorator>
       </template>
       <template is="dom-if" if="[[_showCLAView]]" restamp="true">
-        <gr-cla-view path="[[_path]]"></gr-cla-view>
+        <gr-cla-view></gr-cla-view>
       </template>
       <div id="errorView" class="errorView">
         <div class="errorEmoji">[[_lastError.emoji]]</div>
@@ -211,6 +211,7 @@
     </gr-overlay>
     <gr-overlay id="registration" with-backdrop>
       <gr-registration-dialog
+          settings-url="[[_settingsUrl]]"
           on-account-detail-update="_handleAccountDetailUpdate"
           on-close="_handleRegistrationDialogClose">
       </gr-registration-dialog>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index ad5c62e..e98aaac 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -84,6 +84,7 @@
         type: String,
         computed: '_computePluginScreenName(params)',
       },
+      _settingsUrl: String,
     },
 
     listeners: {
@@ -122,6 +123,10 @@
         this._version = version;
       });
 
+      // Note: this is evaluated here to ensure that it only happens after the
+      // router has been initialized. @see Issue 7837
+      this._settingsUrl = Gerrit.Nav.getUrlForSettings();
+
       this.$.reporting.appStarted();
       this._viewState = {
         changeView: {
@@ -150,6 +155,7 @@
       // Preferences are cached when a user is logged in; warm them.
       this.$.restAPI.getPreferences();
       this.$.restAPI.getDiffPreferences();
+      this.$.restAPI.getEditPreferences();
       this.$.errorManager.knownAccountId =
           this._account && this._account._account_id || null;
     },
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
index c665df4..307a2b4 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
@@ -53,8 +53,7 @@
           </template>
         </tbody>
       </table>
-      <!-- TODO: Renable this when supported in polygerrit -->
-      <!-- <a href$="[[getUrl()]]">New Contributor Agreement</a> -->
+      <a href$="[[getUrl()]]">New Contributor Agreement</a>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
index b667d66..a1f5dc5 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
@@ -1,5 +1,5 @@
 <!--
-Copyright (C) 2017 The Android Open Source Project
+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.
@@ -14,12 +14,94 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-placeholder/gr-placeholder.html">
+<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-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-cla-view">
   <template>
-    <gr-placeholder title="Agreements" path="[[path]]"></gr-placeholder>
+    <style include="shared-styles">
+      h1 {
+        margin-bottom: .6em;
+      }
+      h3 {
+        margin-bottom: .5em;
+      }
+      .agreementsUrl {
+        border: 0.1em solid #b0bdcc;
+        margin-bottom: 1.25em;
+        margin-left: 1.25em;
+        margin-right: 1.25em;
+        padding: 0.3em;
+      }
+      #claNewAgreementsLabel {
+        font-family: var(--font-family-bold);
+      }
+      #claNewAgreement {
+        display: none;
+      }
+      #claNewAgreement.show {
+        display: block;
+      }
+      .contributorAgreementButton {
+        font-family: var(--font-family-bold);
+      }
+      .contributorAgreementAlreadySubmitted {
+        color: red;
+        margin: 0 2em;
+        padding: .5em;
+      }
+      .agreementsSubmitted,
+      .hideAgreementsTextBox {
+        display: none;
+      }
+      main {
+        margin: 2em auto;
+        max-width: 50em;
+      }
+    </style>
+    <style include="gr-form-styles"></style>
+    <main>
+      <h1>New Contributor Agreement</h1>
+      <h3>Select an agreement type:</h3>
+      <template is="dom-repeat" items="[[_serverConfig.auth.contributor_agreements]]">
+        <span class="contributorAgreementButton">
+          <input id$="claNewAgreementsInput[[item.name]]"
+              name="claNewAgreementsRadio"
+              type="radio"
+              data-name$="[[item.name]]"
+              data-url$="[[item.url]]"
+              on-tap="_handleShowAgreement"
+              disabled$="[[_disableAggreements(item, _groups)]]">
+          <label id="claNewAgreementsLabel">[[item.name]]</label>
+        </span>
+        <div class$="contributorAgreementAlreadySubmitted [[_hideAggreements(item, _groups)]]">
+          Agreement already submitted.
+        </div>
+        <div class="agreementsUrl">
+          [[item.description]]
+        </div>
+      </template>
+      <div id="claNewAgreement" class$="[[_computeShowAgreementsClass(_showAgreements)]]">
+        <h3 class="smallHeading">Review the agreement:</h3>
+        <div id="agreementsUrl" class="agreementsUrl">
+          <a href$="[[_agreementsUrl]]" target="blank" rel="noopener">
+            Please review the agreement.</a>
+        </div>
+        <div class$="agreementsTextBox [[_computeHideAgreementClass(_agreementName, _serverConfig.auth.contributor_agreements)]]">
+          <h3 class="smallHeading">Complete the agreement:</h3>
+          <input id="input-agreements" is="iron-input" bind-value="{{_agreementsText}}" placeholder="Enter 'I agree' here" />
+          <gr-button on-tap="_handleSaveAgreements" disabled="[[_disableAgreementsText(_agreementsText)]]">
+            Submit
+          </gr-button>
+        </div>
+      </div>
+    </main>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-cla-view.js"></script>
-</dom-module>
+</dom-module>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
index 71dc71b..39400c64 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// 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.
@@ -18,7 +18,121 @@
     is: 'gr-cla-view',
 
     properties: {
-      path: String,
+      _groups: Object,
+      /** @type {?} */
+      _serverConfig: Object,
+      _agreementsText: String,
+      _agreementName: String,
+      _showAgreements: {
+        type: Boolean,
+        value: false,
+      },
+      _agreementsUrl: String,
+    },
+
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+    ],
+
+    attached() {
+      this.loadData();
+
+      this.fire('title-change', {title: 'New Contributor Agreement'});
+    },
+
+    loadData() {
+      const promises = [];
+      promises.push(this.$.restAPI.getConfig(true).then(config => {
+        this._serverConfig = config;
+      }));
+
+      promises.push(this.$.restAPI.getAccountGroups().then(groups => {
+        this._groups = groups.sort((a, b) => {
+          return a.name.localeCompare(b.name);
+        });
+      }));
+
+      return Promise.all(promises);
+    },
+
+    _getAgreementsUrl(configUrl) {
+      let url;
+      if (!configUrl) { return ''; }
+      if (configUrl.startsWith('http:') || configUrl.startsWith('https:')) {
+        url = configUrl;
+      } else {
+        url = this.getBaseUrl() + '/' + configUrl;
+      }
+
+      return url;
+    },
+
+    _handleShowAgreement(e) {
+      this._agreementName = e.target.getAttribute('data-name');
+      this._agreementsUrl =
+          this._getAgreementsUrl(e.target.getAttribute('data-url'));
+      this._showAgreements = true;
+    },
+
+    _handleSaveAgreements(e) {
+      this._createToast('Agreement saving...');
+
+      const name = this._agreementName;
+      return this.$.restAPI.saveAccountAgreement({name}).then(res => {
+        let message = 'Agreement failed to be submitted, please try again';
+        if (res.status === 200) {
+          message = 'Agreement has been successfully submited.';
+        }
+        this._createToast(message);
+        this.loadData();
+        this._agreementsText = '';
+        this._showAgreements = false;
+      });
+    },
+
+    _createToast(message) {
+      this.dispatchEvent(new CustomEvent('show-alert',
+          {detail: {message}, bubbles: true}));
+    },
+
+    _computeShowAgreementsClass(agreements) {
+      return agreements ? 'show' : '';
+    },
+
+    _disableAggreements(item, groups) {
+      for (const value of groups) {
+        if (item && item.auto_verify_group &&
+            item.auto_verify_group.name === value.name) {
+          return true;
+        }
+      }
+
+      return false;
+    },
+
+    _hideAggreements(item, groups) {
+      return this._disableAggreements(item, groups) ?
+          '' : 'agreementsSubmitted';
+    },
+
+    _disableAgreementsText(text) {
+      return text.toLowerCase() === 'i agree' ? false : true;
+    },
+
+    // This checks for auto_verify_group,
+    // if specified it returns 'hideAgreementsTextBox' which
+    // then hides the text box and submit button.
+    _computeHideAgreementClass(name, config) {
+      for (const key in config) {
+        if (!config.hasOwnProperty(key)) { return; }
+        for (const prop in config[key]) {
+          if (!config[key].hasOwnProperty(prop)) { return; }
+          if (name === config[key].name &&
+              !config[key].auto_verify_group) {
+            return 'hideAgreementsTextBox';
+          }
+        }
+      }
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
new file mode 100644
index 0000000..985fbfa
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-cla-view</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-cla-view.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-cla-view></gr-cla-view>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-cla-view tests', () => {
+    let element;
+    let agreements;
+    const auth = {
+      name: 'Individual',
+      description: 'test-description',
+      url: 'static/cla_individual.html',
+      auto_verify_group: {
+        url: '#/admin/groups/uuid-bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
+        options: {
+          visible_to_all: true,
+        },
+        group_id: 20,
+        owner: 'CLA Accepted - Individual',
+        owner_id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
+        created_on: '2017-07-31 15:11:04.000000000',
+        id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
+        name: 'CLA Accepted - Individual',
+      },
+    };
+    const auth2 = {
+      name: 'Individual2',
+      description: 'test-description2',
+      url: 'static/cla_individual2.html',
+      auto_verify_group: {
+        url: '#/admin/groups/uuid-e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
+        options: {},
+        group_id: 21,
+        owner: 'CLA Accepted - Individual2',
+        owner_id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
+        created_on: '2017-07-31 15:25:42.000000000',
+        id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
+        name: 'CLA Accepted - Individual2',
+      },
+    };
+    const config = {
+      auth: {
+        use_contributor_agreements: true,
+        contributor_agreements: [
+          {
+            name: 'Individual',
+            description: 'test-description',
+            url: 'static/cla_individual.html',
+          },
+        ],
+      },
+    };
+    const config2 = {
+      auth: {
+        use_contributor_agreements: true,
+        contributor_agreements: [
+          {
+            name: 'Individual2',
+            description: 'test-description2',
+            url: 'static/cla_individual2.html',
+          },
+        ],
+      },
+    };
+    const groups = [
+      {
+        url: 'some url',
+        options: {},
+        description: 'Group 1 description',
+        group_id: 1,
+        owner: 'Administrators',
+        owner_id: '123',
+        id: 'abc',
+        name: 'Individual',
+      },
+      {
+        options: {visible_to_all: true},
+        id: '456',
+        group_id: 2,
+        name: 'Individual 2',
+      },
+      {
+        options: {visible_to_all: true},
+        id: '457',
+        group_id: 3,
+        name: 'CLA Accepted - Individual',
+      },
+    ];
+
+    setup(done => {
+      agreements = [{
+        url: 'test-agreements.html',
+        description: 'Agreements 1 description',
+        name: 'Agreements 1',
+      }];
+
+      stub('gr-rest-api-interface', {
+        getAccountGroups() { return Promise.resolve(agreements); },
+      });
+
+      element = fixture('basic');
+
+      element.loadData().then(() => { flush(done); });
+    });
+
+    test('_disableAggreements equals true', () => {
+      assert.isTrue(element._disableAggreements(auth, groups));
+    });
+
+    test('_disableAggreements equals false', () => {
+      assert.isFalse(element._disableAggreements(auth2, groups));
+    });
+
+    test('_hideAggreements equals string', () => {
+      assert.equal(element._hideAggreements(auth, groups), '');
+    });
+
+    test('_hideAggreements equals agreementsSubmitted', () => {
+      assert.equal(element._hideAggreements(auth2, groups),
+          'agreementsSubmitted');
+    });
+
+    test('_disableAgreementsText equals true', () => {
+      assert.isFalse(element._disableAgreementsText('I AGREE'));
+    });
+
+    test('_disableAgreementsText equals true', () => {
+      assert.isTrue(element._disableAgreementsText('I DO NOT AGREE'));
+    });
+
+    test('_computeHideAgreementClass returns true', () => {
+      assert.equal(
+          element._computeHideAgreementClass(
+              auth.name, config.auth.contributor_agreements),
+          'hideAgreementsTextBox');
+    });
+
+    test('_computeHideAgreementClass returns undefined', () => {
+      assert.isUndefined(
+          element._computeHideAgreementClass(
+              auth.name, config2.auth.contributor_agreements));
+    });
+
+    test('_getAgreementsUrl has http', () => {
+      assert.equal(element._getAgreementsUrl(
+          'http://test.org/test.html'), 'http://test.org/test.html');
+    });
+
+    test('_getAgreementsUrl does not have http://', () => {
+      assert.equal(element._getAgreementsUrl(
+          'test_cla.html'), '/test_cla.html');
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
new file mode 100644
index 0000000..bbd7396
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
@@ -0,0 +1,170 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../shared/gr-select/gr-select.html">
+
+<dom-module id="gr-edit-preferences">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles"></style>
+    <div id="editPreferences" class="gr-form-styles">
+      <section>
+        <span class="title">Tab width</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              type="number"
+              prevent-invalid-input
+              allowed-pattern="[0-9]"
+              bind-value="{{editPrefs.tab_size}}"
+              on-change="_handleEditPrefsChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Columns</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              type="number"
+              prevent-invalid-input
+              allowed-pattern="[0-9]"
+              bind-value="{{editPrefs.line_length}}"
+              on-change="_handleEditPrefsChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Indent unit</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              type="number"
+              prevent-invalid-input
+              allowed-pattern="[0-9]"
+              bind-value="{{editPrefs.indent_unit}}"
+              on-change="_handleEditPrefsChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Cursor blink rate</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              type="number"
+              prevent-invalid-input
+              allowed-pattern="[0-9]"
+              bind-value="{{editPrefs.cursor_blink_rate}}"
+              on-change="_handleEditPrefsChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Top menu</span>
+        <span class="value">
+          <input
+              id="showTopMenu"
+              type="checkbox"
+              checked$="[[editPrefs.hide_top_menu]]"
+              on-change="_handleTopMenuChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Syntax highlighting</span>
+        <span class="value">
+          <input
+              id="editSyntaxHighlighting"
+              type="checkbox"
+              checked$="[[editPrefs.syntax_highlighting]]"
+              on-change="_handleEditSyntaxHighlightingChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Show tabs</span>
+        <span class="value">
+          <input
+              id="editShowTabs"
+              type="checkbox"
+              checked$="[[editPrefs.show_tabs]]"
+              on-change="_handleEditShowTabsChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Whitespace errors</span>
+        <span class="value">
+          <input
+              id="whitespaceErrors"
+              type="checkbox"
+              checked$="[[editPrefs.show_whitespace_errors]]"
+              on-change="_handleWhitespaceErrorsChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Line numbers</span>
+        <span class="value">
+          <input
+              id="showLineNumbers"
+              type="checkbox"
+              checked$="[[editPrefs.hide_line_numbers]]"
+              on-change="_handleLineNumbersChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Match brackets</span>
+        <span class="value">
+          <input
+              id="showMatchBrackets"
+              type="checkbox"
+              checked$="[[editPrefs.match_brackets]]"
+              on-change="_handleMatchBracketsChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Line wrapping</span>
+        <span class="value">
+          <input
+              id="editShowLineWrapping"
+              type="checkbox"
+              checked$="[[editPrefs.line_wrapping]]"
+              on-change="_handleEditLineWrappingChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Indent with tabs</span>
+        <span class="value">
+          <input
+              id="showIndentWithTabs"
+              type="checkbox"
+              checked$="[[editPrefs.indent_with_tabs]]"
+              on-change="_handleIndentWithTabsChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Auto close brackets</span>
+        <span class="value">
+          <input
+              id="showAutoCloseBrackets"
+              type="checkbox"
+              checked$="[[editPrefs.auto_close_brackets]]"
+              on-change="_handleAutoCloseBracketsChanged">
+        </span>
+      </section>
+    </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-edit-preferences.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
new file mode 100644
index 0000000..01b45f8
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
@@ -0,0 +1,105 @@
+// 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.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-edit-preferences',
+
+    properties: {
+      hasUnsavedChanges: {
+        type: Boolean,
+        notify: true,
+        value: false,
+      },
+
+      /** @type {?} */
+      editPrefs: Object,
+    },
+
+    loadData() {
+      return this.$.restAPI.getEditPreferences().then(prefs => {
+        this.editPrefs = prefs;
+      });
+    },
+
+    _handleEditPrefsChanged() {
+      this.hasUnsavedChanges = true;
+    },
+
+    _handleTopMenuChanged() {
+      this.set('editPrefs.hide_top_menu', this.$.showTopMenu.checked);
+      this._handleEditPrefsChanged();
+    },
+
+    _handleEditSyntaxHighlightingChanged() {
+      this.set('editPrefs.syntax_highlighting',
+          this.$.editSyntaxHighlighting.checked);
+      this._handleEditPrefsChanged();
+    },
+
+    _handleEditShowTabsChanged() {
+      this.set('editPrefs.show_tabs', this.$.editShowTabs.checked);
+      this._handleEditPrefsChanged();
+    },
+
+    _handleWhitespaceErrorsChanged() {
+      this.set('editPrefs.show_whitespace_errors',
+          this.$.whitespaceErrors.checked);
+      this._handleEditPrefsChanged();
+    },
+
+    _handleLineNumbersChanged() {
+      this.set('editPrefs.hide_line_numbers',
+          this.$.showLineNumbers.checked);
+      this._handleEditPrefsChanged();
+    },
+
+    _handleMatchBracketsChanged() {
+      this.set('editPrefs.match_brackets', this.$.showMatchBrackets.checked);
+      this._handleEditPrefsChanged();
+    },
+
+    _handleEditLineWrappingChanged() {
+      this.set('editPrefs.line_wrapping',
+          this.$.editShowLineWrapping.checked);
+      this._handleEditPrefsChanged();
+    },
+
+    _handleIndentWithTabsChanged() {
+      this.set('editPrefs.indent_with_tabs',
+          this.$.showIndentWithTabs.checked);
+      this._handleEditPrefsChanged();
+    },
+
+    _handleAutoCloseBracketsChanged() {
+      this.set('editPrefs.auto_close_brackets',
+          this.$.showAutoCloseBrackets.checked);
+      this._handleEditPrefsChanged();
+    },
+
+    _handleShowBaseVersionChanged() {
+      this.set('editPrefs.show_base',
+          this.$.showShowBaseVersion.checked);
+      this._handleEditPrefsChanged();
+    },
+
+    save() {
+      return this.$.restAPI.saveEditPreferences(this.editPrefs)
+          .then(() => {
+            this.hasUnsavedChanges = false;
+          });
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
new file mode 100644
index 0000000..0305c84
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-edit-preferences</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-edit-preferences.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-edit-preferences></gr-edit-preferences>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-edit-preferences tests', () => {
+    let element;
+    let editPreferences;
+
+    function valueOf(title, fieldsetid) {
+      const sections = element.$[fieldsetid].querySelectorAll('section');
+      let titleEl;
+      for (let i = 0; i < sections.length; i++) {
+        titleEl = sections[i].querySelector('.title');
+        if (titleEl.textContent.trim() === title) {
+          return sections[i].querySelector('.value');
+        }
+      }
+    }
+
+    setup(done => {
+      editPreferences = {
+        auto_close_brackets: false,
+        cursor_blink_rate: 0,
+        hide_line_numbers: false,
+        hide_top_menu: false,
+        indent_unit: 2,
+        indent_with_tabs: false,
+        key_map_type: 'DEFAULT',
+        line_length: 100,
+        line_wrapping: false,
+        match_brackets: true,
+        show_base: false,
+        show_tabs: true,
+        show_whitespace_errors: true,
+        syntax_highlighting: true,
+        tab_size: 8,
+        theme: 'DEFAULT',
+      };
+
+      stub('gr-rest-api-interface', {
+        getEditPreferences() {
+          return Promise.resolve(editPreferences);
+        },
+      });
+
+      element = fixture('basic');
+
+      element.loadData().then(done);
+    });
+
+    test('renders', () => {
+      // Rendered with the expected preferences selected.
+      assert.equal(valueOf('Tab width', 'editPreferences')
+          .firstElementChild.bindValue, editPreferences.tab_size);
+      assert.equal(valueOf('Columns', 'editPreferences')
+          .firstElementChild.bindValue, editPreferences.line_length);
+      assert.equal(valueOf('Indent unit', 'editPreferences')
+          .firstElementChild.bindValue, editPreferences.indent_unit);
+      assert.equal(valueOf('Cursor blink rate', 'editPreferences')
+          .firstElementChild.bindValue, editPreferences.cursor_blink_rate);
+      assert.equal(valueOf('Top menu', 'editPreferences')
+          .firstElementChild.checked, editPreferences.hide_top_menu);
+      assert.equal(valueOf('Syntax highlighting', 'editPreferences')
+          .firstElementChild.checked, editPreferences.syntax_highlighting);
+      assert.equal(valueOf('Show tabs', 'editPreferences')
+          .firstElementChild.checked, editPreferences.show_tabs);
+      assert.equal(valueOf('Whitespace errors', 'editPreferences')
+          .firstElementChild.checked, editPreferences.show_whitespace_errors);
+      assert.equal(valueOf('Line numbers', 'editPreferences')
+          .firstElementChild.checked, editPreferences.hide_line_numbers);
+      assert.equal(valueOf('Match brackets', 'editPreferences')
+          .firstElementChild.checked, editPreferences.match_brackets);
+      assert.equal(valueOf('Line wrapping', 'editPreferences')
+          .firstElementChild.checked, editPreferences.line_wrapping);
+      assert.equal(valueOf('Indent with tabs', 'editPreferences')
+          .firstElementChild.checked, editPreferences.indent_with_tabs);
+      assert.equal(valueOf('Auto close brackets', 'editPreferences')
+          .firstElementChild.checked, editPreferences.auto_close_brackets);
+
+      assert.isFalse(element.hasUnsavedChanges);
+    });
+
+    test('save changes', done => {
+      const showTabsCheckbox = valueOf('Show tabs', 'editPreferences')
+          .firstElementChild;
+      showTabsCheckbox.checked = false;
+      element._handleEditShowTabsChanged();
+
+      assert.isTrue(element.hasUnsavedChanges);
+
+      stub('gr-rest-api-interface', {
+        saveEditPreferences(prefs) {
+          assert.equal(prefs.show_tabs, false);
+          return Promise.resolve();
+        },
+      });
+
+      // Save the change.
+      element.save().then(() => {
+        assert.isFalse(element.hasUnsavedChanges);
+        done();
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
index 0cbd1f6..1b3d9d4 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
@@ -94,7 +94,7 @@
         <hr>
         <p>
           More configuration options for Gerrit may be found in the
-          <a on-tap="close" href$="[[_computeSettingsUrl(_account)]]">settings</a>.
+          <a on-tap="close" href$="[[settingsUrl]]">settings</a>.
         </p>
       </main>
       <footer>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
index 406d16c..dace2ca 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -30,6 +30,7 @@
      */
 
     properties: {
+      settingsUrl: String,
       /** @type {?} */
       _account: {
         type: Object,
@@ -89,9 +90,5 @@
     _computeSaveDisabled(name, username, email, saving) {
       return !name || !username || !email || saving;
     },
-
-    _computeSettingsUrl() {
-      return Gerrit.Nav.getUrlForSettings();
-    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index 8aae460..1195408 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -29,6 +29,7 @@
 <link rel="import" href="../../shared/gr-select/gr-select.html">
 <link rel="import" href="../gr-account-info/gr-account-info.html">
 <link rel="import" href="../gr-agreements-list/gr-agreements-list.html">
+<link rel="import" href="../gr-edit-preferences/gr-edit-preferences.html">
 <link rel="import" href="../gr-email-editor/gr-email-editor.html">
 <link rel="import" href="../gr-group-list/gr-group-list.html">
 <link rel="import" href="../gr-http-password/gr-http-password.html">
@@ -63,6 +64,7 @@
           <li><a href="#Profile">Profile</a></li>
           <li><a href="#Preferences">Preferences</a></li>
           <li><a href="#DiffPreferences">Diff Preferences</a></li>
+          <li><a href="#EditPreferences">Edit Preferences</a></li>
           <li><a href="#Menu">Menu</a></li>
           <li><a href="#ChangeTableColumns">Change Table Columns</a></li>
           <li><a href="#Notifications">Notifications</a></li>
@@ -237,10 +239,10 @@
             <span class="title">Fit to screen</span>
             <span class="value">
               <input
-                  id="lineWrapping"
+                  id="diffLineWrapping"
                   type="checkbox"
                   checked$="[[_diffPrefs.line_wrapping]]"
-                  on-change="_handleLineWrappingChanged">
+                  on-change="_handleDiffLineWrappingChanged">
             </span>
           </section>
           <section id="columnsPref" hidden$="[[_diffPrefs.line_wrapping]]">
@@ -280,10 +282,10 @@
             <span class="title">Show tabs</span>
             <span class="value">
               <input
-                  id="showTabs"
+                  id="diffShowTabs"
                   type="checkbox"
                   checked$="[[_diffPrefs.show_tabs]]"
-                  on-change="_handleShowTabsChanged">
+                  on-change="_handleDiffShowTabsChanged">
             </span>
           </section>
           <section>
@@ -300,10 +302,10 @@
             <span class="title">Syntax highlighting</span>
             <span class="value">
               <input
-                  id="syntaxHighlighting"
+                  id="diffSyntaxHighlighting"
                   type="checkbox"
                   checked$="[[_diffPrefs.syntax_highlighting]]"
-                  on-change="_handleSyntaxHighlightingChanged">
+                  on-change="_handleDiffSyntaxHighlightingChanged">
             </span>
           </section>
           <gr-button
@@ -311,6 +313,20 @@
               on-tap="_handleSaveDiffPreferences"
               disabled$="[[!_diffPrefsChanged]]">Save changes</gr-button>
         </fieldset>
+        <h2
+            id="EditPreferences"
+            class$="[[_computeHeaderClass(_editPrefsChanged)]]">
+          Edit Preferences
+        </h2>
+        <fieldset id="editPreferences">
+          <gr-edit-preferences
+              id="editPrefs"
+              has-unsaved-changes="{{_editPrefsChanged}}"></gr-edit-preferences>
+          <gr-button
+              id="saveEditPrefs"
+              on-tap="_handleSaveEditPreferences"
+              disabled$="[[!_editPrefsChanged]]">Save changes</gr-button>
+        </fieldset>
         <h2 id="Menu" class$="[[_computeHeaderClass(_menuChanged)]]">Menu</h2>
         <fieldset id="menu">
           <gr-menu-editor menu-items="{{_localMenu}}"></gr-menu-editor>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 08c3d4c1..5836b0d 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -90,6 +90,8 @@
         type: Boolean,
         value: false,
       },
+      /** @type {?} */
+      _editPrefsChanged: Boolean,
       _menuChanged: {
         type: Boolean,
         value: false,
@@ -146,6 +148,7 @@
         this.$.groupList.loadData(),
         this.$.httpPass.loadData(),
         this.$.identities.loadData(),
+        this.$.editPrefs.loadData(),
       ];
 
       promises.push(this.$.restAPI.getPreferences().then(prefs => {
@@ -287,12 +290,12 @@
       });
     },
 
-    _handleLineWrappingChanged() {
-      this.set('_diffPrefs.line_wrapping', this.$.lineWrapping.checked);
+    _handleDiffLineWrappingChanged() {
+      this.set('_diffPrefs.line_wrapping', this.$.diffLineWrapping.checked);
     },
 
-    _handleShowTabsChanged() {
-      this.set('_diffPrefs.show_tabs', this.$.showTabs.checked);
+    _handleDiffShowTabsChanged() {
+      this.set('_diffPrefs.show_tabs', this.$.diffShowTabs.checked);
     },
 
     _handleShowTrailingWhitespaceChanged() {
@@ -300,9 +303,9 @@
           this.$.showTrailingWhitespace.checked);
     },
 
-    _handleSyntaxHighlightingChanged() {
+    _handleDiffSyntaxHighlightingChanged() {
       this.set('_diffPrefs.syntax_highlighting',
-          this.$.syntaxHighlighting.checked);
+          this.$.diffSyntaxHighlighting.checked);
     },
 
     _handleSaveChangeTable() {
@@ -321,6 +324,10 @@
           });
     },
 
+    _handleSaveEditPreferences() {
+      this.$.editPrefs.save();
+    },
+
     _handleSaveMenu() {
       this.set('prefs.my', this._localMenu);
       this._cloneMenu();
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index 868a9e3..9a20b6c 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -254,7 +254,7 @@
       const showTabsCheckbox = valueOf('Show tabs', 'diffPreferences')
           .firstElementChild;
       showTabsCheckbox.checked = false;
-      element._handleShowTabsChanged();
+      element._handleDiffShowTabsChanged();
 
       assert.isTrue(element._diffPrefsChanged);
 
@@ -275,10 +275,10 @@
     test('columns input is hidden with fit to scsreen is selected', () => {
       assert.isFalse(element.$.columnsPref.hidden);
 
-      MockInteractions.tap(element.$.lineWrapping);
+      MockInteractions.tap(element.$.diffLineWrapping);
       assert.isTrue(element.$.columnsPref.hidden);
 
-      MockInteractions.tap(element.$.lineWrapping);
+      MockInteractions.tap(element.$.diffLineWrapping);
       assert.isFalse(element.$.columnsPref.hidden);
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index 2d61d18..a065f7a 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -40,10 +40,14 @@
         text-transform: none;
       }
       paper-button {
+        /* paper-button sets this to anti-aliased, which appears different than
+        roboto-medium elsewhere. */
+        -webkit-font-smoothing: initial;
         align-items: center;
         background-color: var(--background-color);
         color: var(--button-color);
         display: flex;
+        font-family: inherit;
         justify-content: center;
         margin: var(--margin, 0);
         min-width: var(--border, 0);
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
index ef78f3a..07aa537 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
@@ -90,6 +90,7 @@
             <paper-input
                 id="input"
                 label="[[labelText]]"
+                maxlength="[[maxLength]]"
                 value="{{_inputText}}"></paper-input>
             <div class="buttons">
               <gr-button link id="cancelBtn" on-tap="_cancel">cancel</gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
index af06291..94a6b64 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -51,6 +51,7 @@
         reflectToAttribute: true,
         value: false,
       },
+      maxLength: Number,
       _inputText: String,
       // This is used to push the iron-input element up on the page, so
       // the input is placed in approximately the same position as the
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
index eabe061..0e13563 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
@@ -39,10 +39,18 @@
         type: Boolean,
         value: false,
       },
+
+      /**
+       * The maximum number of characters to display in the tooltop.
+       */
+      tooltipLimit: {
+        type: Number,
+        value: 1024,
+      },
     },
 
     observers: [
-      '_updateTitle(text, limit)',
+      '_updateTitle(text, limit, tooltipLimit)',
     ],
 
     behaviors: [
@@ -53,10 +61,10 @@
      * The text or limit have changed. Recompute whether a tooltip needs to be
      * enabled.
      */
-    _updateTitle(text, limit) {
+    _updateTitle(text, limit, tooltipLimit) {
       this.hasTooltip = !!limit && !!text && text.length > limit;
       if (this.hasTooltip) {
-        this.setAttribute('title', text);
+        this.setAttribute('title', text.substr(0, tooltipLimit));
       } else {
         this.removeAttribute('title');
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
index 9e00331..d0d5a33 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
@@ -66,15 +66,20 @@
       assert.equal(element.getAttribute('title'), 'abc 123');
       assert.isTrue(element.hasTooltip);
 
+      element.tooltipLimit = 3;
+      flushAsynchronousOperations();
+      assert.equal(element.getAttribute('title'), 'abc');
+
+      element.tooltipLimit = 1024;
       element.limit = 100;
       flushAsynchronousOperations();
-      assert.equal(updateSpy.callCount, 4);
+      assert.equal(updateSpy.callCount, 6);
       assert.isNotOk(element.getAttribute('title'));
       assert.isFalse(element.hasTooltip);
 
       element.limit = null;
       flushAsynchronousOperations();
-      assert.equal(updateSpy.callCount, 5);
+      assert.equal(updateSpy.callCount, 7);
       assert.isNotOk(element.getAttribute('title'));
       assert.isFalse(element.hasTooltip);
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
index bd1b91b..cf0c243 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
@@ -68,7 +68,9 @@
     </style>
     <div class$="container [[_getBackgroundClass(transparentBackground)]]">
       <a href$="[[href]]">
-        <gr-limited-text limit="[[limit]]" text="[[text]]"></gr-limited-text>
+        <gr-limited-text
+            limit="[[limit]]"
+            text="[[text]]"></gr-limited-text>
       </a>
       <gr-button
           id="remove"
diff --git a/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.html b/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.html
deleted file mode 100644
index 15f44cf..0000000
--- a/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<!--
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-placeholder">
-  <template>
-    <style include="shared-styles">
-      main {
-        margin: 2em auto;
-        max-width: 46em;
-      }
-      h1 {
-        margin-bottom: .1em;
-      }
-      @media only screen and (max-width: 67em) {
-        main {
-          margin: 2em 0 2em 15em;
-        }
-      }
-      @media only screen and (max-width: 53em) {
-        .loading {
-          padding: 0 var(--default-horizontal-margin);
-        }
-        main {
-          margin: 2em 1em;
-        }
-      }
-    </style>
-    <main>
-      <h1>[[title]]</h1>
-      <section>
-        This page is not yet implemented in PolyGerrit. View it in the
-        <a id="gwtLink" href$="[[computeGwtUrl(path)]]" rel="external">
-        Old UI</a>
-      </section>
-    </main>
-  </template>
-  <script src="gr-placeholder.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.js b/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.js
deleted file mode 100644
index 9b60061..0000000
--- a/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-(function() {
-  'use strict';
-
-  Polymer({
-    is: 'gr-placeholder',
-
-    properties: {
-      path: String,
-      title: String,
-    },
-
-    behaviors: [
-      Gerrit.BaseUrlBehavior,
-    ],
-  });
-})();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index a3d3d90..b5bb35a 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -230,8 +230,12 @@
       return JSON.parse(source.substring(JSON_PREFIX.length));
     },
 
-    getConfig() {
-      return this._fetchSharedCacheURL('/config/server/info');
+    getConfig(noCache) {
+      if (!noCache) {
+        return this._fetchSharedCacheURL('/config/server/info');
+      }
+
+      return this.fetchJSON('/config/server/info');
     },
 
     getRepo(repo) {
@@ -675,13 +679,17 @@
     },
 
     getAccountGroups() {
-      return this._fetchSharedCacheURL('/accounts/self/groups');
+      return this.fetchJSON('/accounts/self/groups');
     },
 
     getAccountAgreements() {
       return this._fetchSharedCacheURL('/accounts/self/agreements');
     },
 
+    saveAccountAgreement(name) {
+      return this.send('PUT', '/accounts/self/agreements', name);
+    },
+
     /**
      * @param {string=} opt_params
      */
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
index e80cbe5..68db696 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
@@ -19,6 +19,11 @@
 
 <dom-module id="gr-tooltip-content">
   <template>
+    <style>
+      .arrow {
+        color: var(--arrow-color);
+      }
+    </style>
     <slot></slot><!--
  --><span class="arrow" hidden$="[[!showIcon]]">&#9432;</span>
   </template>
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html
index 31b1c6e..a3cf247 100644
--- a/polygerrit-ui/app/styles/shared-styles.html
+++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -87,6 +87,19 @@
       [hidden] {
         display: none !important;
       }
+      .separator {
+        background-color: rgba(0, 0, 0, .3);
+        height: 20px;
+        margin: 0 8px;
+        width: 1px;
+      }
+      .separator.transparent {
+        background-color: transparent;
+      }
+      paper-toggle-button {
+        --paper-toggle-button-checked-bar-color: var(--color-link);
+        --paper-toggle-button-checked-button-color: var(--color-link);
+      }
     </style>
   </template>
 </dom-module>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index ecbd86a..44a9296 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -119,6 +119,8 @@
     'plugins/gr-settings-api/gr-settings-api_test.html',
     'settings/gr-account-info/gr-account-info_test.html',
     'settings/gr-change-table-editor/gr-change-table-editor_test.html',
+    'settings/gr-cla-view/gr-cla-view_test.html',
+    'settings/gr-edit-preferences/gr-edit-preferences_test.html',
     'settings/gr-email-editor/gr-email-editor_test.html',
     'settings/gr-group-list/gr-group-list_test.html',
     'settings/gr-http-password/gr-http-password_test.html',
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index e63e739..448d940 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -51,10 +51,22 @@
                 action='store_true')
 opts.add_option('--name', help='name of the generated project',
                 action='store', default='gerrit', dest='project_name')
+opts.add_option('-b', '--batch', action='store_true',
+                dest='batch', help='Bazel batch option')
 args, _ = opts.parse_args()
 
+batch_option = '--batch' if args.batch else None
+
+def _build_bazel_cmd(*args):
+  cmd = ['bazel']
+  if batch_option:
+    cmd.append('--batch')
+  for arg in args:
+    cmd.append(arg)
+  return cmd
+
 def retrieve_ext_location():
-  return check_output(['bazel', 'info', 'output_base']).strip()
+  return check_output(_build_bazel_cmd('info', 'output_base')).strip()
 
 def gen_bazel_path():
   bazel = check_output(['which', 'bazel']).strip()
@@ -66,7 +78,7 @@
   deps = []
   t = cp_targets[target]
   try:
-    check_call(['bazel', 'build', t])
+    check_call(_build_bazel_cmd('build', t))
   except CalledProcessError:
     exit(1)
   name = 'bazel-bin/tools/eclipse/' + t.split(':')[1] + '.runtime_classpath'
@@ -277,7 +289,7 @@
     makedirs(path.join(ROOT, gwt_working_dir))
 
   try:
-    check_call(['bazel', 'build', MAIN, GWT, '//java/org/eclipse/jgit:libEdit-src.jar'])
+    check_call(_build_bazel_cmd('build', MAIN, GWT, '//java/org/eclipse/jgit:libEdit-src.jar'))
   except CalledProcessError:
     exit(1)
 except KeyboardInterrupt:
diff --git a/version.bzl b/version.bzl
index 340ba87..62d841f 100644
--- a/version.bzl
+++ b/version.bzl
@@ -3,10 +3,3 @@
 # when talking to the destination repository.
 #
 GERRIT_VERSION = "2.16-SNAPSHOT"
-
-def check_version(x):
-    if native.bazel_version == "":
-        # experimental / unreleased Bazel.
-        return
-    if native.bazel_version < x:
-        fail("\nERROR: Current Bazel version is {}, expected at least {}\n".format(native.bazel_version, x))