Merge "PolyGerrit: Update project, plugin and group admin elements to update the title correctly"
diff --git a/Documentation/cmd-flush-caches.txt b/Documentation/cmd-flush-caches.txt
index 4716f3b..9ba4808 100644
--- a/Documentation/cmd-flush-caches.txt
+++ b/Documentation/cmd-flush-caches.txt
@@ -60,7 +60,6 @@
 ----
 	$ ssh -p 29418 review.example.com gerrit flush-caches --list
 	accounts
-	accounts_byemail
 	diff
 	groups
 	ldap_groups
diff --git a/Documentation/cmd-show-caches.txt b/Documentation/cmd-show-caches.txt
index 59abc1c..5286d43 100644
--- a/Documentation/cmd-show-caches.txt
+++ b/Documentation/cmd-show-caches.txt
@@ -58,7 +58,6 @@
                                   |   Mem   Disk   Space|         |Mem  Disk|
   --------------------------------+---------------------+---------+---------+
     accounts                      |  4096               |   3.4ms | 99%     |
-    accounts_byemail              |  1024               |   7.6ms | 98%     |
     accounts_byname               |  4096               |  11.3ms | 99%     |
     adv_bases                     |                     |         |         |
     changes                       |                     |  27.1ms |  0%     |
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index ec2e046..71474f5 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -714,12 +714,6 @@
 If direct updates are made to any of these database tables, this
 cache should be flushed.
 
-cache `"accounts_byemail"`::
-+
-Caches account identities keyed by email address, which is scanned
-from the `account_external_ids` database table.  If updates are
-made to this table, this cache should be flushed.
-
 cache `"adv_bases"`::
 +
 Used only for push over smart HTTP when branch level access controls
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 68c1963..4471645 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -5,7 +5,7 @@
 . link:linux-quickstart.html[Quickstart for Installing Gerrit on Linux]
 
 == About Gerrit
-. link:intro-product-overview.html[Product Overview]
+. link:intro-quick.html[Product Overview]
 . link:intro-how-gerrit-works.html[How Gerrit Works]
 . link:intro-gerrit-walkthrough.html[Basic Gerrit Walkthrough]
 
diff --git a/Documentation/intro-product-overview.txt b/Documentation/intro-product-overview.txt
deleted file mode 100644
index afd0109..0000000
--- a/Documentation/intro-product-overview.txt
+++ /dev/null
@@ -1,40 +0,0 @@
-= Gerrit Product Overview
-
-Gerrit is a web-based code review tool built on top of the
-https://git-scm.com/[Git version control system]. This introduction provides
-an overview of Gerrit and describes how Gerrit integrates into a typical
-development workflow. It also provides a brief tutorial that shows how to manage
-a change using Gerrit.
-
-== What is Gerrit?
-
-Gerrit makes code review easy by providing a lightweight framework for reviewing
-commits before they are accepted by the codebase. Gerrit works equally well for
-projects where approving changes is restricted to selected users, as is typical
-for Open Source software development, as well as projects where all contributors
-are trusted.
-
-== Learn About Gerrit
-
-If you're new to Gerrit and want to know more about how it can improve your
-developer workflow, see the following topics:
-
-. link:intro-product-overview.html[Product Overview]
-. link:intro-gerrit-walkthrough.html[Basic Gerrit Walkthrough]
-
-== Getting Started
-
-This documentation contains several guides to help you learn about the Gerrit
-features most relevant to you:
-
-. link:intro-user.html[User Guide]
-. link:intro-project-owner.html[Project Owner Guide]
-. link:http://source.android.com/submit-patches/workflow[Default Android Workflow] (external)
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
-
-SEARCHBOX
----------
-
diff --git a/Documentation/intro-quick.txt b/Documentation/intro-quick.txt
index 39c2ec39..e6b1e43 100644
--- a/Documentation/intro-quick.txt
+++ b/Documentation/intro-quick.txt
@@ -1,7 +1,7 @@
-= Gerrit Code Review - A Quick Introduction
+= Gerrit Product Overview
 
 Gerrit is a web-based code review tool built on top of the
-https://git-scm.com/[git version control system]. This introduction provides
+https://git-scm.com/[Git version control system]. This introduction provides
 an overview of Gerrit and describes how Gerrit integrates into a typical
 development workflow. It also provides a brief tutorial that shows how to manage
 a change using Gerrit.
@@ -14,318 +14,22 @@
 for Open Source software development, as well as projects where all contributors
 are trusted.
 
-== Gerrit and the developer workflow
+== Learn About Gerrit
 
-To understand how Gerrit fits into and enhances the developer workflow, consider
-a typical project. This project has a central source repository, which serves as
-the authoritative copy of the project's contents.
+If you're new to Gerrit and want to know more about how it can improve your
+developer workflow, see the following topics:
 
-.Central Source Repository
-image::images/intro-quick-central-repo.png[Authoritative Source Repository]
+. link:intro-how-gerrit-works.html[How Gerrit Works]
+. link:intro-gerrit-walkthrough.html[Basic Gerrit Walkthrough]
 
-Gerrit takes the place of this central repository and adds an additional
-concept: a _store of pending changes_.
+== Getting Started
 
-.Gerrit in place of Central Repository
-image::images/intro-quick-central-gerrit.png[Gerrit in place of Central Repository]
+This documentation contains several guides to help you learn about the Gerrit
+features most relevant to you:
 
-With Gerrit, when a developer makes a change, it is sent to this store of
-pending changes, where other developers can review, discuss and approve the
-change. After enough reviewers grant their approval, the change becomes an
-official part of the codebase.
-
-In addition to this store of pending changes, Gerrit captures notes
-and comments about each change. These features allow developers to review
-changes at their convenience, or when conversations about a change can't
-happen face to face. They also help to create a record of the conversation
-around a given change, which can provide a history of when a change was made and
-why.
-
-Like any repository hosting solution, Gerrit has a powerful
-link:access-control.html[access control model.] This model allows you to
-fine-tune access to your repository.
-
-== Working with Gerrit: An example
-
-To understand how Gerrit works, let's follow a change through its entire
-life cycle. This example uses a Gerrit server configured as follows:
-
-* *Hostname*: gerrithost
-* *HTTP interface port*: 8080
-* *SSH interface port*: 29418
-
-In this walkthrough, we'll follow two developers, Max and Hannah, as they make
-and review a change to a +RecipeBook+ project. We'll follow the change through
-these stages:
-
-. Making the change.
-. Creating the review.
-. Reviewing the change.
-. Reworking the change.
-. Verifying the change.
-. Submitting the change.
-
-NOTE: The project and commands used in this section are for demonstration
-purposes only.
-
-=== Making the Change
-
-Our first developer, Max, has decided to make a change to the +RecipeBook+
-project he works on. His first step is to get the source code that he wants to
-modify. To get this code, he runs the following `git clone` command:
-
-----
-$ git clone ssh://gerrithost:29418/RecipeBook.git RecipeBook
-Cloning into RecipeBook...
-----
-
-After he clones the repository, he makes a change to the file and commits it
-locally.
-
-NOTE: At this point, the workflow is exactly the same as it would be if Max was
-not using Gerrit.
-
-Max is ready to create a commit message for his change. When he does, he
-includes a link:user-changeid.html[Change-Id]. This ID allows Gerrit to link
-together different versions of the same change being reviewed.
-
-=== Creating the Review
-
-Max's next step is to push his change to Gerrit so other contributors can review
-it. He does this using the `git push origin HEAD:refs/for/master` command, as
-follows:
-
-----
-$ <work>
-$ git commit
-[master 9651f22] Change to a proper, yeast based pizza dough.
- 1 files changed, 3 insertions(+), 2 deletions(-)
-$ git push origin HEAD:refs/for/master
-Counting objects: 5, done.
-Delta compression using up to 8 threads.
-Compressing objects: 100% (2/2), done.
-Writing objects: 100% (3/3), 542 bytes, done.
-Total 3 (delta 0), reused 0 (delta 0)
-remote:
-remote: New Changes:
-remote:   http://gerrithost:8080/68
-remote:
-To ssh://gerrithost:29418/RecipeBook.git
- * [new branch]      HEAD -> refs/for/master
-----
-
-The `refs/for/master` branch is a symbolic branch that Gerrit uses to create
-reviews for the master branch. If Max opted to push to a different branch, he
-would have modified his command to
-`git push origin HEAD:refs/for/<branch_name>`. Gerrit automatically creates a
-`refs/for/<branch_name>` for every branch that it tracks.
-
-The output of this command also contains a link to a web page Max can use to
-review this commit. Clicking on that link takes him to a screen similar to
-the following.
-
-.Gerrit Code Review Screen
-image::images/intro-quick-new-review.jpg[Gerrit Review Screen]
-
-This is the Gerrit code review screen, where other contributors can review
-his change. Max can also perform tasks such as:
-
-* Looking at the link:user-review-ui.html#diff-preferences[diff] of his change
-* Writing link:user-review-ui.html#inline-comments[inline] or
-  link:user-review-ui.html#reply[summary] comments to explain what he did and
-  why
-* link:intro-user.html#adding-reviewers[Adding a list of people] that should
-  review the change
-
-In this case, Max opts to manually add the senior developer on his team, Hannah,
-to review his change.
-
-=== Reviewing the Change
-
-Let's now switch to Hannah, the senior developer who will review Max's change.
-
-As mentioned previously, Max chose to manually add Hannah as a reviewer. Gerrit
-offers other ways for reviewers to find changes, including:
-
-* Using the link:user-search.html[search] feature that to find changes
-* Setting up link:user-notify.html[email notifications] to stay informed of
-  changes even if you are not added as a reviewer
-
-Because Max added Hannah as a reviewer, she receives an email telling her about
-his change. She opens up the Gerrit code review screen and selects Max's change.
-
-.Gerrit Code Review Screen
-image::images/intro-quick-new-review.jpg[Gerrit Review Screen]
-
-Notice the two "Need" lines:
-
-----
-* Need Verified
-* Need Code-Review
-----
-
-These two lines indicate what checks must be completed before the change is
-accepted. The default Gerrit workflow requires two checks:
-
-* *Code-Review*. This check requires that someone look at the code and ensures
-  that it meets project guidelines, styles, and other criteria.
-* *Verified*. This check means that the code actually compiles, passes any unit
-  tests, and performs as expected.
-
-In general, the *Code-Review* check requires an individual to look at the code,
-while the *Verified* check is done by an automated build server, through a
-mechanism such as the
-link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[Gerrit Trigger
-Jenkins Plugin].
-
-IMPORTANT: The Code-Review and Verified checks require different permissions
-in Gerrit. This requirement allows teams to separate these tasks. For example,
-an automated process can have the rights to verify a change, but not perform a
-code review.
-
-With the code review screen open, Hannah can begin to review Max's change. She
-can choose one of two ways to review the change: unified or side-by-side.
-Both views allow her to perform tasks such as add
-link:user-review-ui.html#inline-comments[inline] or
-link:user-review-ui.html#reply[summary] comments.
-
-Hannah opts to view the change using Gerrit's side-by-side view:
-
-.Side By Side Patch View
-image::images/intro-quick-review-line-comment.jpg[Adding a Comment]
-
-Hannah reviews the change and is ready to provide her feedback. She clicks the
-*Review* button on the change screen. This allows her to vote on the change.
-
-.Reviewing the Change
-image::images/intro-quick-reviewing-the-change.jpg[Reviewing the Change]
-
-A code review vote is essentially a numerical score between -2 and 2. The
-possible options are:
-
-* `+2 Looks good to me, approved`
-* `+1 Looks good to me, but someone else must approve`
-* `0 No score`
-* `-1 I would prefer that you didn't submit this`
-* `-2 Do not submit`
-
-By default, a change must have at least one `+2` vote and no `-2` votes before
-it can be submitted.
-
-IMPORTANT: Although votes use numerical values, they do not accumulate. Two
-`+1` votes do not equate to a `+2`.
-
-Hannah notices a possible issue with Max's change, so she selects a `-1` vote.
-She uses the *Cover Message* text box to provide Max with some additional
-feedback. When she is satisfied with her review, Hannah clicks the
-*Published Comments* button. At this point, her vote and cover message become
-visible to to all users.
-
-=== Reworking the Change
-
-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 set up the link:user-changeid.html[Change-Id commit-msg hook]
-before he uploaded the change, all he has to do to upload the re-worked change
-is push another commit with the same Change-Id in the message. To accomplish
-this, Max needs only perform the following tasks:
-
-* Check out the commit
-* Amend the commit
-* Push the commit to Gerrit
-
-----
-$ <checkout first commit>
-$ <rework>
-$ git commit --amend
-$ git push origin HEAD:refs/for/master
-Counting objects: 5, done.
-Delta compression using up to 8 threads.
-Compressing objects: 100% (2/2), done.
-Writing objects: 100% (3/3), 546 bytes, done.
-Total 3 (delta 0), reused 0 (delta 0)
-remote: Processing changes: updated: 1, done
-remote:
-remote: Updated Changes:
-remote:   http://gerrithost:8080/68
-remote:
-To ssh://gerrithost:29418/RecipeBook.git
- * [new branch]      HEAD -> refs/for/master
-----
-
-Notice that the output of this command is slightly different from Max's first
-commit. This time, the output verifies that the change was updated.
-
-Having uploaded the reworked commit, Max can go back to the Gerrit web
-interface and look at his change.
-
-.Reviewing the Rework
-image::images/intro-quick-review-2-patches.jpg[Reviewing the Rework]
-
-Notice that there are now two patch sets associated with this change: the
-initial submission and the rework.
-
-When Hannah next looks at Max's change, she sees that he incorporated her
-feedback. The change looks good to her, so she changes her vote to a `+2`.
-
-=== Verifying the Change
-
-Hannah's `+2` vote means that Max's change satisfies the *Needs Review*
-check. It has to pass one more check before it can be accepted: the *Needs
-Verified* check.
-
-The Verified check means that the change was confirmed to work. This type of
-check typically involves tasks such as checking that the code compiles, unit
-tests pass, and other actions. You can configure a Verified check to consist
-of as many or as few tasks as needed.
-
-NOTE: Remember that this walkthrough uses Gerrit's default workflow. Projects
-can add custom checks or even remove the Verified check entirely.
-
-Verification is typically an automated process using the
-link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[Gerrit Trigger Jenkins Plugin]
-or a similar mechanism. However, there are still times when a change requires
-manual verification, or a reviewer needs to check how or if a change works.
-To accommodate these and other similar circumstances, Gerrit exposes each change
-as a git branch. The Gerrit UI includes a
-link:user-review-us.html#download[*download*] link in the Gerrit Code Review
-Screen to make it easy for reviewers to fetch a branch for a specific change.
-To manually verify a change, a reviewer must have the
-link:config-labels.html#label_Verified[Verified] permission. Then, the reviewer
-can fetch and checkout that branch from Gerrit. Hannah has this permission, so
-she is authorized to manually verify Max's change.
-
-NOTE: The Verifier can be the same person as the code reviewer or a
-different person entirely.
-
-.Verifying the Change
-image::images/intro-quick-verifying.jpg[Verifying the Change]
-
-Unlike the code review check, the verify check is pass/fail. Hannah can provide
-a score of either `+1` or `-1`. A change must have at least one `+1` and no
-`-1`.
-
-Hannah selects a `+1` for her verified check. Max's change is now ready to be
-submitted.
-
-=== Submitting the Change
-
-Max is now ready to submit his change. He opens up the change in the Code Review
-screen and clicks the *Publish and Submit* button.
-
-At this point, Max's change is merged into the main part of the repository and
-becomes an accepted part of the project.
-
-== Next Steps
-
-This walkthrough provided a quick overview of how a change moves
-through the default Gerrit workflow. At this point, you can:
-
-* Read the link:intro-user.html[Users guide] to get a better sense of how to
-  make changes using Gerrit
-* Review the link:intro-project-owner.html[Project Owners guide] to learn more
-  about configuring projects in Gerrit, including setting user permissions and
-  configuring verification checks
+. link:intro-user.html[User Guide]
+. link:intro-project-owner.html[Project Owner Guide]
+. link:https://source.android.com/source/life-of-a-patch[Default Android Workflow] (external)
 
 GERRIT
 ------
@@ -333,3 +37,4 @@
 
 SEARCHBOX
 ---------
+
diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt
index 76a26e1..0b1a3e5 100644
--- a/Documentation/pgm-daemon.txt
+++ b/Documentation/pgm-daemon.txt
@@ -94,8 +94,6 @@
 ----
 [cache "accounts"]
   maxAge = 5 min
-[cache "accounts_byemail"]
-  maxAge = 5 min
 [cache "diff"]
   maxAge = 5 min
 [cache "groups"]
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index b8c6025..557fb3c 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1312,6 +1312,9 @@
   }
 ----
 
+Note that this endpoint will not update the change's parents, which is
+different from the link:#cherry-pick[cherry-pick] endpoint.
+
 If the change cannot be moved because the change state doesn't
 allow moving the change, the response is "`409 Conflict`" and
 the error message is contained in the response body.
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 6505f27..6c8848b 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -298,16 +298,6 @@
         "mem": 94
       }
     },
-    "accounts_byemail": {
-      "type": "MEM",
-      "entries": {
-        "mem": 4
-      },
-      "average_get": "771.8us",
-      "hit_ratio": {
-        "mem": 95
-      }
-    },
     "accounts_byname": {
       "type": "MEM",
       "entries": {
@@ -514,7 +504,6 @@
   )]}'
   [
     "accounts",
-    "accounts_byemail",
     "accounts_byname",
     "adv_bases",
     "change_kind",
diff --git a/gerrit-acceptance-framework/BUILD b/gerrit-acceptance-framework/BUILD
index b351c27..465dcc6 100644
--- a/gerrit-acceptance-framework/BUILD
+++ b/gerrit-acceptance-framework/BUILD
@@ -10,6 +10,7 @@
     "//gerrit-lucene:lucene",
     "//gerrit-pgm:init",
     "//gerrit-reviewdb:server",
+    "//gerrit-server:receive",
     "//gerrit-server:server",
     "//lib:gson",
     "//lib:jsch",
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index bfd012b..806469e 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
@@ -60,7 +59,6 @@
   private final Provider<GroupsUpdate> groupsUpdateProvider;
   private final SshKeyCache sshKeyCache;
   private final AccountCache accountCache;
-  private final AccountByEmailCache byEmailCache;
   private final ExternalIdsUpdate.Server externalIdsUpdate;
   private final boolean sshEnabled;
 
@@ -74,7 +72,6 @@
       @ServerInitiated Provider<GroupsUpdate> groupsUpdateProvider,
       SshKeyCache sshKeyCache,
       AccountCache accountCache,
-      AccountByEmailCache byEmailCache,
       ExternalIdsUpdate.Server externalIdsUpdate,
       @SshEnabled boolean sshEnabled) {
     accounts = new HashMap<>();
@@ -86,7 +83,6 @@
     this.groupsUpdateProvider = groupsUpdateProvider;
     this.sshKeyCache = sshKeyCache;
     this.accountCache = accountCache;
-    this.byEmailCache = byEmailCache;
     this.externalIdsUpdate = externalIdsUpdate;
     this.sshEnabled = sshEnabled;
   }
@@ -148,7 +144,6 @@
       if (username != null) {
         accountCache.evictByUsername(username);
       }
-      byEmailCache.evict(email);
 
       account = new TestAccount(id, username, email, fullName, sshKey, httpPass);
       if (username != null) {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 256be82..a95ac09 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -27,7 +27,7 @@
 import com.google.gerrit.pgm.Daemon;
 import com.google.gerrit.pgm.Init;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.AsyncReceiveCommits;
+import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
 import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
index 1219b0a..3174090 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.acceptance;
 
+import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.InProcessProtocol.Context;
 import com.google.gerrit.common.data.Capable;
@@ -28,11 +29,10 @@
 import com.google.gerrit.server.RequestCleanup;
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
-import com.google.gerrit.server.git.AsyncReceiveCommits;
-import com.google.gerrit.server.git.ReceiveCommits;
 import com.google.gerrit.server.git.ReceivePackInitializer;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
@@ -297,10 +297,10 @@
           throw new ServiceNotAuthorizedException();
         }
 
-        ReceiveCommits rc = factory.create(ctl, db).getReceiveCommits();
-        ReceivePack rp = rc.getReceivePack();
+        AsyncReceiveCommits arc = factory.create(ctl, db, null, ImmutableSetMultimap.of());
+        ReceivePack rp = arc.getReceivePack();
 
-        Capable r = rc.canUpload();
+        Capable r = arc.canUpload();
         if (r != Capable.OK) {
           throw new ServiceNotAuthorizedException();
         }
diff --git a/gerrit-acceptance-tests/BUILD b/gerrit-acceptance-tests/BUILD
index 0ad9002..91b90e3 100644
--- a/gerrit-acceptance-tests/BUILD
+++ b/gerrit-acceptance-tests/BUILD
@@ -20,6 +20,7 @@
         "//gerrit-pgm:util",
         "//gerrit-reviewdb:server",
         "//gerrit-server:prolog-common",
+        "//gerrit-server:receive",
         "//gerrit-server:server",
         "//gerrit-server:testutil",
         "//gerrit-sshd:sshd",
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 06fe021..6c1fb39 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -78,9 +78,9 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.account.AccountConfig;
 import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.account.WatchConfig;
 import com.google.gerrit.server.account.WatchConfig.NotifyType;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -144,8 +144,6 @@
 
   @Inject private AccountsUpdate.Server accountsUpdate;
 
-  @Inject private AccountByEmailCache byEmailCache;
-
   @Inject private ExternalIds externalIds;
 
   @Inject private ExternalIdsUpdate.User externalIdsUpdateFactory;
@@ -156,6 +154,8 @@
 
   @Inject private InternalAccountQuery accountQuery;
 
+  @Inject protected Emails emails;
+
   private AccountIndexedCounter accountIndexedCounter;
   private RegistrationHandle accountIndexEventCounterHandle;
   private ExternalIdsUpdate externalIdsUpdate;
@@ -677,52 +677,44 @@
   }
 
   @Test
-  public void lookUpFromCacheByEmail() throws Exception {
+  public void lookUpByEmail() throws Exception {
     // exact match with scheme "mailto:"
-    assertEmail(byEmailCache.get(admin.email), admin);
+    assertEmail(emails.getAccountFor(admin.email), admin);
 
     // exact match with other scheme
     String email = "foo.bar@example.com";
     externalIdsUpdateFactory
         .create()
         .insert(ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id, email));
-    assertEmail(byEmailCache.get(email), admin);
+    assertEmail(emails.getAccountFor(email), admin);
 
     // wrong case doesn't match
-    assertThat(byEmailCache.get(admin.email.toUpperCase(Locale.US))).isEmpty();
+    assertThat(emails.getAccountFor(admin.email.toUpperCase(Locale.US))).isEmpty();
 
     // prefix doesn't match
-    assertThat(byEmailCache.get(admin.email.substring(0, admin.email.indexOf('@')))).isEmpty();
+    assertThat(emails.getAccountFor(admin.email.substring(0, admin.email.indexOf('@')))).isEmpty();
 
     // non-existing doesn't match
-    assertThat(byEmailCache.get("non-existing@example.com")).isEmpty();
+    assertThat(emails.getAccountFor("non-existing@example.com")).isEmpty();
+
+    // lookup several accounts by email at once
+    ImmutableSetMultimap<String, Account.Id> byEmails =
+        emails.getAccountsFor(admin.email, user.email);
+    assertEmail(byEmails.get(admin.email), admin);
+    assertEmail(byEmails.get(user.email), user);
   }
 
   @Test
-  public void lookUpByEmail() throws Exception {
-    // exact match with scheme "mailto:"
-    assertEmail(accounts.byEmail(admin.email), admin);
+  public void lookUpByPreferredEmail() throws Exception {
+    // create an inconsistent account that has a preferred email without external ID
+    String prefEmail = "foo.preferred@example.com";
+    TestAccount foo = accountCreator.create(name("foo"));
+    accountsUpdate.create().update(db, foo.id, a -> a.setPreferredEmail(prefEmail));
 
-    // exact match with other scheme
-    String email = "foo.bar@example.com";
-    externalIdsUpdateFactory
-        .create()
-        .insert(ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id, email));
-    assertEmail(accounts.byEmail(email), admin);
-
-    // wrong case doesn't match
-    assertThat(accounts.byEmail(admin.email.toUpperCase(Locale.US))).isEmpty();
-
-    // prefix doesn't match
-    assertThat(accounts.byEmail(admin.email.substring(0, admin.email.indexOf('@')))).isEmpty();
-
-    // non-existing doesn't match
-    assertThat(accounts.byEmail("non-existing@example.com")).isEmpty();
-
-    // lookup several accounts by email at once
-    ImmutableSetMultimap<String, Account.Id> byEmails = accounts.byEmails(admin.email, user.email);
-    assertEmail(byEmails.get(admin.email), admin);
-    assertEmail(byEmails.get(user.email), user);
+    // verify that the account is still found when using the preferred email to lookup the account
+    ImmutableSet<Account.Id> accountsByPrefEmail = emails.getAccountFor(prefEmail);
+    assertThat(accountsByPrefEmail).hasSize(1);
+    assertThat(Iterables.getOnlyElement(accountsByPrefEmail)).isEqualTo(foo.id);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 854ab47..91cbc6d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -64,7 +64,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.ReceiveCommits;
+import com.google.gerrit.server.git.receive.ReceiveConstants;
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.project.Util;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -490,7 +490,7 @@
     GitUtil.fetch(testRepo, r.getPatchSet().getRefName() + ":ps");
     testRepo.reset("ps");
     r = amendChange(r.getChangeId(), "refs/for/master%ready", admin, testRepo);
-    r.assertErrorStatus(ReceiveCommits.ONLY_OWNER_CAN_MODIFY_WIP);
+    r.assertErrorStatus(ReceiveConstants.ONLY_OWNER_CAN_MODIFY_WIP);
 
     // Other user trying to move from WIP to WIP should succeed.
     r = amendChange(r.getChangeId(), "refs/for/master%wip", admin, testRepo);
@@ -506,7 +506,7 @@
     GitUtil.fetch(testRepo, r.getPatchSet().getRefName() + ":ps");
     testRepo.reset("ps");
     r = amendChange(r.getChangeId(), "refs/for/master%wip", admin, testRepo);
-    r.assertErrorStatus(ReceiveCommits.ONLY_OWNER_CAN_MODIFY_WIP);
+    r.assertErrorStatus(ReceiveConstants.ONLY_OWNER_CAN_MODIFY_WIP);
 
     // Other user trying to move from ready to ready should succeed.
     r = amendChange(r.getChangeId(), "refs/for/master%ready", admin, testRepo);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index d79095f..5affacf 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -39,8 +39,8 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.ReceiveCommitsAdvertiseRefsHook;
 import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.git.receive.ReceiveCommitsAdvertiseRefsHook;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.project.Util;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 6cbc532..e361091 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -479,7 +479,7 @@
     assertThat(commitsInRepo)
         .containsAllOf("Initial empty repository", "Change 1", "Change 2", "Change 3");
     if (getSubmitType() == SubmitType.MERGE_ALWAYS) {
-      assertThat(commitsInRepo).contains("Merge changes from topic '" + expectedTopic + "'");
+      assertThat(commitsInRepo).contains("Merge changes from topic \"" + expectedTopic + "\"");
     }
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
index 308c9a5..bb7da11 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.git.ChangeSet;
 import com.google.gerrit.server.git.MergeSuperSet;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gwtorm.server.OrmException;
@@ -304,7 +305,8 @@
   }
 
   private void assertChangeSetMergeable(ChangeData change, boolean expected)
-      throws MissingObjectException, IncorrectObjectTypeException, IOException, OrmException {
+      throws MissingObjectException, IncorrectObjectTypeException, IOException, OrmException,
+          PermissionBackendException {
     ChangeSet cs = mergeSuperSet.get().completeChangeSet(db, change.change(), user(admin));
     assertThat(submit.unmergeableChanges(cs).isEmpty()).isEqualTo(expected);
   }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
index 2755b91..21a5b6e 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
@@ -28,9 +28,8 @@
 /**
  * Pre-receive hook to check signed pushes.
  *
- * <p>If configured, prior to processing any push using {@link
- * com.google.gerrit.server.git.ReceiveCommits}, requires that any push certificate present must be
- * valid.
+ * <p>If configured, prior to processing any push using {@code ReceiveCommits}, requires that any
+ * push certificate present must be valid.
  */
 @Singleton
 public class SignedPushPreReceiveHook implements PreReceiveHook {
diff --git a/gerrit-httpd/BUILD b/gerrit-httpd/BUILD
index 9b1b610..57ecd26 100644
--- a/gerrit-httpd/BUILD
+++ b/gerrit-httpd/BUILD
@@ -26,6 +26,7 @@
         "//gerrit-patch-jgit:server",
         "//gerrit-prettify:server",
         "//gerrit-reviewdb:server",
+        "//gerrit-server:receive",
         "//gerrit-server:server",
         "//gerrit-util-cli:cli",
         "//gerrit-util-http:http",
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index a89e2d9..f073acf 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd;
 
 import com.google.common.cache.Cache;
+import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -24,11 +25,10 @@
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.git.AsyncReceiveCommits;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReceiveCommits;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -80,7 +80,7 @@
   private static final long serialVersionUID = 1L;
 
   private static final String ATT_CONTROL = ProjectControl.class.getName();
-  private static final String ATT_RC = ReceiveCommits.class.getName();
+  private static final String ATT_ARC = AsyncReceiveCommits.class.getName();
   private static final String ID_CACHE = "adv_bases";
 
   public static final String URL_REGEX;
@@ -295,11 +295,9 @@
         throw new ServiceNotAuthorizedException();
       }
 
-      ReceiveCommits rc = factory.create(pc, db).getReceiveCommits();
-      rc.init();
-
-      ReceivePack rp = rc.getReceivePack();
-      req.setAttribute(ATT_RC, rc);
+      AsyncReceiveCommits arc = factory.create(pc, db, null, ImmutableSetMultimap.of());
+      ReceivePack rp = arc.getReceivePack();
+      req.setAttribute(ATT_ARC, arc);
       return rp;
     }
   }
@@ -325,8 +323,8 @@
         throws IOException, ServletException {
       boolean isGet = "GET".equalsIgnoreCase(((HttpServletRequest) request).getMethod());
 
-      ReceiveCommits rc = (ReceiveCommits) request.getAttribute(ATT_RC);
-      ReceivePack rp = rc.getReceivePack();
+      AsyncReceiveCommits arc = (AsyncReceiveCommits) request.getAttribute(ATT_ARC);
+      ReceivePack rp = arc.getReceivePack();
       rp.getAdvertiseRefsHook().advertiseRefs(rp);
       ProjectControl pc = (ProjectControl) request.getAttribute(ATT_CONTROL);
       Project.NameKey projectName = pc.getProject().getNameKey();
@@ -340,7 +338,7 @@
         return;
       }
 
-      final Capable s = rc.canUpload();
+      Capable s = arc.canUpload();
       if (s != Capable.OK) {
         GitSmartHttpTools.sendError(
             (HttpServletRequest) request,
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index 9967af6..538d605 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -28,7 +28,7 @@
 import com.google.gerrit.server.config.GerritOptions;
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.config.GitwebCgiConfig;
-import com.google.gerrit.server.git.AsyncReceiveCommits;
+import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
 import com.google.gerrit.server.util.GuiceRequestScopePropagator;
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.inject.Inject;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index e721b7a..365789c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -44,6 +44,7 @@
 import java.io.OutputStream;
 import java.io.Writer;
 import java.util.List;
+import java.util.Optional;
 import java.util.UUID;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
@@ -215,8 +216,15 @@
 
   private AuthResult byPreferredEmail(String email) {
     try (ReviewDb db = schema.open()) {
-      List<Account> matches = db.accounts().byPreferredEmail(email).toList();
-      return matches.size() == 1 ? auth(matches.get(0)) : null;
+      Optional<Account> match =
+          accountQuery
+              .byPreferredEmail(email)
+              .stream()
+              // the index query also matches prefixes, filter those out
+              .filter(a -> email.equalsIgnoreCase(a.getAccount().getPreferredEmail()))
+              .map(AccountState::getAccount)
+              .findFirst();
+      return match.isPresent() ? auth(match.get()) : null;
     } catch (OrmException e) {
       getServletContext().log("cannot query database", e);
       return null;
diff --git a/gerrit-pgm/BUILD b/gerrit-pgm/BUILD
index 6056805..24a19d4 100644
--- a/gerrit-pgm/BUILD
+++ b/gerrit-pgm/BUILD
@@ -29,6 +29,8 @@
 
 DEPS = BASE_JETTY_DEPS + [
     "//gerrit-reviewdb:server",
+    "//gerrit-server:module",
+    "//gerrit-server:receive",
     "//lib:gwtorm",
     "//lib/log:jsonevent-layout",
 ]
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 3ef5f2b..72920d0 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -65,9 +65,9 @@
 import com.google.gerrit.server.config.RestCacheAdminModule;
 import com.google.gerrit.server.events.StreamEventsApiListener;
 import com.google.gerrit.server.git.GarbageCollectionModule;
-import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.index.DummyIndexModule;
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.index.IndexModule.IndexType;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 788f7ce..d56a5a6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -23,11 +23,11 @@
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.rules.PrologModule;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountByEmailCacheImpl;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountVisibility;
 import com.google.gerrit.server.account.AccountVisibilityProvider;
@@ -53,16 +53,17 @@
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitUploadPackGroups;
 import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.group.GroupModule;
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
 import com.google.gerrit.server.notedb.NoteDbModule;
 import com.google.gerrit.server.patch.DiffExecutorModule;
 import com.google.gerrit.server.patch.PatchListCacheImpl;
 import com.google.gerrit.server.project.CommentLinkProvider;
+import com.google.gerrit.server.project.CommitResource;
 import com.google.gerrit.server.project.DefaultPermissionBackendModule;
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.ProjectState;
@@ -115,6 +116,8 @@
         .in(SINGLETON);
     bind(new TypeLiteral<DynamicMap<ChangeQueryProcessor.ChangeAttributeFactory>>() {})
         .toInstance(DynamicMap.<ChangeQueryProcessor.ChangeAttributeFactory>emptyMap());
+    bind(new TypeLiteral<DynamicMap<RestView<CommitResource>>>() {})
+        .toInstance(DynamicMap.<RestView<CommitResource>>emptyMap());
     bind(String.class)
         .annotatedWith(CanonicalWebUrl.class)
         .toProvider(CanonicalWebUrlProvider.class);
@@ -153,7 +156,6 @@
     install(new GroupModule());
     install(new NoteDbModule(cfg));
     install(new PrologModule());
-    install(AccountByEmailCacheImpl.module());
     install(AccountCacheImpl.module());
     install(GroupCacheImpl.module());
     install(GroupIncludeCacheImpl.module());
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
index db74caa..9f371f2 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
@@ -28,9 +28,6 @@
   @PrimaryKey("accountId")
   Account get(Account.Id key) throws OrmException;
 
-  @Query("WHERE preferredEmail = ? LIMIT 2")
-  ResultSet<Account> byPreferredEmail(String email) throws OrmException;
-
   @Query("ORDER BY accountId")
   ResultSet<Account> all() throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index 5a83dc4..8f87503 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -5,13 +5,6 @@
 --
 
 -- *********************************************************************
--- AccountAccess
---    covers:             byPreferredEmail
-CREATE INDEX accounts_byPreferredEmail
-ON accounts (preferred_email);
-
-
--- *********************************************************************
 -- AccountGroupMemberAccess
 --    @PrimaryKey covers: byAccount
 CREATE INDEX account_group_members_byGroup
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
index 98f05ca..57b1a4a 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
@@ -6,14 +6,6 @@
 --
 
 -- *********************************************************************
--- AccountAccess
---    covers:             byPreferredEmail
-CREATE INDEX accounts_byPreferredEmail
-ON accounts (preferred_email)
-#
-
-
--- *********************************************************************
 -- AccountGroupMemberAccess
 --    @PrimaryKey covers: byAccount
 CREATE INDEX account_group_members_byGroup
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index dde86a4..e1d88ef 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -52,13 +52,6 @@
 --
 
 -- *********************************************************************
--- AccountAccess
---    covers:             byPreferredEmail
-CREATE INDEX accounts_byPreferredEmail
-ON accounts (preferred_email);
-
-
--- *********************************************************************
 -- AccountGroupMemberAccess
 --    @PrimaryKey covers: byAccount
 CREATE INDEX account_group_members_byGroup
diff --git a/gerrit-server/BUILD b/gerrit-server/BUILD
index aa7962e..567ec6c 100644
--- a/gerrit-server/BUILD
+++ b/gerrit-server/BUILD
@@ -5,9 +5,15 @@
     "src/main/java/com/google/gerrit/server/documentation/Constants.java",
 ]
 
+GERRIT_GLOBAL_MODULE_SRC = [
+    "src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java",
+]
+
+RECEIVE_SRCS = glob(["src/main/java/com/google/gerrit/server/git/receive/**/*.java"])
+
 SRCS = glob(
     ["src/main/java/**/*.java"],
-    exclude = CONSTANTS_SRC,
+    exclude = CONSTANTS_SRC + GERRIT_GLOBAL_MODULE_SRC + RECEIVE_SRCS,
 )
 
 RESOURCES = glob(["src/main/resources/**/*"])
@@ -25,6 +31,12 @@
     deps = [":server"],
 )
 
+# Giant kitchen-sink target.
+#
+# The only reason this hasn't been split up further is because we have too many
+# tangled dependencies (and Guice unfortunately makes it quite easy to get into
+# this state). Which means if you see an opportunity to split something off, you
+# should seize it.
 java_library(
     name = "server",
     srcs = SRCS,
@@ -94,7 +106,49 @@
     ],
 )
 
+# Large modules that import things from all across the server package
+# hierarchy, so they need lots of dependencies.
+java_library(
+    name = "module",
+    srcs = GERRIT_GLOBAL_MODULE_SRC,
+    visibility = ["//visibility:public"],
+    deps = [
+        ":receive",
+        ":server",
+        "//gerrit-extension-api:api",
+        "//lib:blame-cache",
+        "//lib:guava",
+        "//lib:soy",
+        "//lib:velocity",
+        "//lib/guice",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+    ],
+)
+
+java_library(
+    name = "receive",
+    srcs = RECEIVE_SRCS,
+    visibility = ["//visibility:public"],
+    deps = [
+        ":server",
+        "//gerrit-common:annotations",
+        "//gerrit-common:server",
+        "//gerrit-extension-api:api",
+        "//gerrit-reviewdb:server",
+        "//gerrit-util-cli:cli",
+        "//lib:args4j",
+        "//lib:guava",
+        "//lib:gwtorm",
+        "//lib/auto:auto-value",
+        "//lib/guice",
+        "//lib/guice:guice-assistedinject",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/log:api",
+    ],
+)
+
 TESTUTIL_DEPS = [
+    ":module",
     ":server",
     "//gerrit-common:annotations",
     "//gerrit-common:server",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java
index 3cc7335..8bfd880 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.events.ProjectEvent;
 import com.google.gerrit.server.events.RefEvent;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
@@ -86,7 +87,8 @@
   }
 
   @Override
-  public void postEvent(Change change, ChangeEvent event) throws OrmException {
+  public void postEvent(Change change, ChangeEvent event)
+      throws OrmException, PermissionBackendException {
     fireEvent(change, event);
   }
 
@@ -101,7 +103,7 @@
   }
 
   @Override
-  public void postEvent(Event event) throws OrmException {
+  public void postEvent(Event event) throws OrmException, PermissionBackendException {
     fireEvent(event);
   }
 
@@ -111,7 +113,8 @@
     }
   }
 
-  protected void fireEvent(Change change, ChangeEvent event) throws OrmException {
+  protected void fireEvent(Change change, ChangeEvent event)
+      throws OrmException, PermissionBackendException {
     for (UserScopedEventListener listener : listeners) {
       if (isVisibleTo(change, listener.getUser())) {
         listener.onEvent(event);
@@ -138,7 +141,7 @@
     fireEventForUnrestrictedListeners(event);
   }
 
-  protected void fireEvent(Event event) throws OrmException {
+  protected void fireEvent(Event event) throws OrmException, PermissionBackendException {
     for (UserScopedEventListener listener : listeners) {
       if (isVisibleTo(event, listener.getUser())) {
         listener.onEvent(event);
@@ -156,7 +159,8 @@
     }
   }
 
-  protected boolean isVisibleTo(Change change, CurrentUser user) throws OrmException {
+  protected boolean isVisibleTo(Change change, CurrentUser user)
+      throws OrmException, PermissionBackendException {
     if (change == null) {
       return false;
     }
@@ -164,9 +168,12 @@
     if (pe == null) {
       return false;
     }
-    ProjectControl pc = pe.controlFor(user);
     ReviewDb db = dbProvider.get();
-    return pc.controlFor(db, change).isVisible(db);
+    return permissionBackend
+        .user(user)
+        .change(notesFactory.createChecked(db, change))
+        .database(db)
+        .test(ChangePermission.READ);
   }
 
   protected boolean isVisibleTo(Branch.NameKey branchName, CurrentUser user) {
@@ -178,7 +185,8 @@
     return pc.controlForRef(branchName).isVisible();
   }
 
-  protected boolean isVisibleTo(Event event, CurrentUser user) throws OrmException {
+  protected boolean isVisibleTo(Event event, CurrentUser user)
+      throws OrmException, PermissionBackendException {
     if (event instanceof RefEvent) {
       RefEvent refEvent = (RefEvent) event;
       String ref = refEvent.getRefName();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java
index 20d55d6..947b656 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.ProjectEvent;
 import com.google.gerrit.server.events.RefEvent;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 
 /** Interface for posting (dispatching) Events */
@@ -31,8 +32,9 @@
    * @param change The change that the event is related to
    * @param event The event to post
    * @throws OrmException on failure to post the event due to DB error
+   * @throws PermissionBackendException on failure of permission checks
    */
-  void postEvent(Change change, ChangeEvent event) throws OrmException;
+  void postEvent(Change change, ChangeEvent event) throws OrmException, PermissionBackendException;
 
   /**
    * Post a stream event that is related to a branch
@@ -58,6 +60,7 @@
    *
    * @param event The event to post.
    * @throws OrmException on failure to post the event due to DB error
+   * @throws PermissionBackendException on failure of permission checks
    */
-  void postEvent(Event event) throws OrmException;
+  void postEvent(Event event) throws OrmException, PermissionBackendException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 2d3b41f..eb4665f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -50,6 +51,7 @@
 public final class StoredValues {
   public static final StoredValue<Accounts> ACCOUNTS = create(Accounts.class);
   public static final StoredValue<AccountCache> ACCOUNT_CACHE = create(AccountCache.class);
+  public static final StoredValue<Emails> EMAILS = create(Emails.class);
   public static final StoredValue<ReviewDb> REVIEW_DB = create(ReviewDb.class);
   public static final StoredValue<ChangeData> CHANGE_DATA = create(ChangeData.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index e4cca59..7750729 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -47,6 +47,9 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gwtorm.server.OrmException;
@@ -106,20 +109,20 @@
 
   private final NotesMigration migration;
   private final IdentifiedUser.GenericFactory userFactory;
-  private final ChangeControl.GenericFactory changeControlFactory;
   private final ApprovalCopier copier;
+  private final PermissionBackend permissionBackend;
 
   @VisibleForTesting
   @Inject
   public ApprovalsUtil(
       NotesMigration migration,
       IdentifiedUser.GenericFactory userFactory,
-      ChangeControl.GenericFactory changeControlFactory,
-      ApprovalCopier copier) {
+      ApprovalCopier copier,
+      PermissionBackend permissionBackend) {
     this.migration = migration;
     this.userFactory = userFactory;
-    this.changeControlFactory = changeControlFactory;
     this.copier = copier;
+    this.permissionBackend = permissionBackend;
   }
 
   /**
@@ -262,8 +265,8 @@
   private boolean canSee(ReviewDb db, ChangeNotes notes, Account.Id accountId) {
     try {
       IdentifiedUser user = userFactory.create(accountId);
-      return changeControlFactory.controlFor(notes, user).isVisible(db);
-    } catch (OrmException e) {
+      return permissionBackend.user(user).change(notes).database(db).test(ChangePermission.READ);
+    } catch (PermissionBackendException e) {
       log.warn(
           String.format(
               "Failed to check if account %d can see change %d",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
deleted file mode 100644
index 255078a..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ /dev/null
@@ -1,96 +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.common.collect.ImmutableSet.toImmutableSet;
-
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.account.externalids.ExternalIds;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.inject.Inject;
-import com.google.inject.Module;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
-import com.google.inject.name.Named;
-import java.util.Collections;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/** Translates an email address to a set of matching accounts. */
-@Singleton
-public class AccountByEmailCacheImpl implements AccountByEmailCache {
-  private static final Logger log = LoggerFactory.getLogger(AccountByEmailCacheImpl.class);
-  private static final String CACHE_NAME = "accounts_byemail";
-
-  public static Module module() {
-    return new CacheModule() {
-      @Override
-      protected void configure() {
-        cache(CACHE_NAME, String.class, new TypeLiteral<Set<Account.Id>>() {}).loader(Loader.class);
-        bind(AccountByEmailCacheImpl.class);
-        bind(AccountByEmailCache.class).to(AccountByEmailCacheImpl.class);
-      }
-    };
-  }
-
-  private final LoadingCache<String, Set<Account.Id>> cache;
-
-  @Inject
-  AccountByEmailCacheImpl(@Named(CACHE_NAME) LoadingCache<String, Set<Account.Id>> cache) {
-    this.cache = cache;
-  }
-
-  @Override
-  public Set<Account.Id> get(String email) {
-    try {
-      return cache.get(email);
-    } catch (ExecutionException e) {
-      log.warn("Cannot resolve accounts by email", e);
-      return Collections.emptySet();
-    }
-  }
-
-  @Override
-  public void evict(String email) {
-    if (email != null) {
-      cache.invalidate(email);
-    }
-  }
-
-  static class Loader extends CacheLoader<String, Set<Account.Id>> {
-    // This must be a provider to prevent a cyclic dependency within Google-internal glue code.
-    private final Provider<ExternalIds> externalIds;
-
-    @Inject
-    Loader(Provider<ExternalIds> externalIds) {
-      this.externalIds = externalIds;
-    }
-
-    @Override
-    public Set<Account.Id> load(String email) throws Exception {
-      return externalIds
-          .get()
-          .byEmail(email)
-          .stream()
-          .map(e -> e.accountId())
-          .collect(toImmutableSet());
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index d493cf5..f247d86 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -62,7 +62,6 @@
   private final Accounts accounts;
   private final AccountsUpdate.Server accountsUpdateFactory;
   private final AccountCache byIdCache;
-  private final AccountByEmailCache byEmailCache;
   private final Realm realm;
   private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeUserName.Factory changeUserNameFactory;
@@ -81,7 +80,6 @@
       Accounts accounts,
       AccountsUpdate.Server accountsUpdateFactory,
       AccountCache byIdCache,
-      AccountByEmailCache byEmailCache,
       Realm accountMapper,
       IdentifiedUser.GenericFactory userFactory,
       ChangeUserName.Factory changeUserNameFactory,
@@ -95,7 +93,6 @@
     this.accounts = accounts;
     this.accountsUpdateFactory = accountsUpdateFactory;
     this.byIdCache = byIdCache;
-    this.byEmailCache = byEmailCache;
     this.realm = accountMapper;
     this.userFactory = userFactory;
     this.changeUserNameFactory = changeUserNameFactory;
@@ -197,11 +194,6 @@
         throw new OrmException("Account " + user.getAccountId() + " has been deleted");
       }
     }
-
-    if (newEmail != null && !newEmail.equals(oldEmail)) {
-      byEmailCache.evict(oldEmail);
-      byEmailCache.evict(newEmail);
-    }
   }
 
   private static boolean eq(String a, String b) {
@@ -300,7 +292,6 @@
       }
     }
 
-    byEmailCache.evict(account.getPreferredEmail());
     realm.onCreateAccount(who, account);
     return new AuthResult(newId, extId.key(), true);
   }
@@ -383,7 +374,6 @@
                       a.setPreferredEmail(who.getEmailAddress());
                     }
                   });
-          byEmailCache.evict(who.getEmailAddress());
         }
       }
 
@@ -481,7 +471,6 @@
                     }
                   }
                 });
-        extIds.stream().forEach(e -> byEmailCache.evict(e.email()));
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 7f66b9c..894f7a1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -36,22 +36,22 @@
 public class AccountResolver {
   private final Realm realm;
   private final Accounts accounts;
-  private final AccountByEmailCache byEmail;
   private final AccountCache byId;
   private final Provider<InternalAccountQuery> accountQueryProvider;
+  private final Emails emails;
 
   @Inject
   AccountResolver(
       Realm realm,
       Accounts accounts,
-      AccountByEmailCache byEmail,
       AccountCache byId,
-      Provider<InternalAccountQuery> accountQueryProvider) {
+      Provider<InternalAccountQuery> accountQueryProvider,
+      Emails emails) {
     this.realm = realm;
     this.accounts = accounts;
-    this.byEmail = byEmail;
     this.byId = byId;
     this.accountQueryProvider = accountQueryProvider;
+    this.emails = emails;
   }
 
   /**
@@ -136,7 +136,8 @@
    * @return the single account that matches; null if no account matches or there are multiple
    *     candidates.
    */
-  public Account findByNameOrEmail(ReviewDb db, String nameOrEmail) throws OrmException {
+  public Account findByNameOrEmail(ReviewDb db, String nameOrEmail)
+      throws OrmException, IOException {
     Set<Account.Id> r = findAllByNameOrEmail(db, nameOrEmail);
     return r.size() == 1 ? byId.get(r.iterator().next()).getAccount() : null;
   }
@@ -149,11 +150,12 @@
    *     address ("email@example"), a full name ("Full Name").
    * @return the accounts that match, empty collection if none. Never null.
    */
-  public Set<Account.Id> findAllByNameOrEmail(ReviewDb db, String nameOrEmail) throws OrmException {
+  public Set<Account.Id> findAllByNameOrEmail(ReviewDb db, String nameOrEmail)
+      throws OrmException, IOException {
     int lt = nameOrEmail.indexOf('<');
     int gt = nameOrEmail.indexOf('>');
     if (lt >= 0 && gt > lt && nameOrEmail.contains("@")) {
-      Set<Account.Id> ids = byEmail.get(nameOrEmail.substring(lt + 1, gt));
+      Set<Account.Id> ids = emails.getAccountFor(nameOrEmail.substring(lt + 1, gt));
       if (ids.isEmpty() || ids.size() == 1) {
         return ids;
       }
@@ -171,7 +173,7 @@
     }
 
     if (nameOrEmail.contains("@")) {
-      return byEmail.get(nameOrEmail);
+      return emails.getAccountFor(nameOrEmail);
     }
 
     Account.Id id = realm.lookup(nameOrEmail);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java
index c3d0b81..28ed422 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java
@@ -14,18 +14,13 @@
 
 package com.google.gerrit.server.account;
 
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
 import static java.util.Comparator.comparing;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSetMultimap;
 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.account.externalids.ExternalIds;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -37,7 +32,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Stream;
@@ -56,20 +50,17 @@
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
   private final OutgoingEmailValidator emailValidator;
-  private final ExternalIds externalIds;
 
   @Inject
   Accounts(
       @GerritServerConfig Config cfg,
       GitRepositoryManager repoManager,
       AllUsersName allUsersName,
-      OutgoingEmailValidator emailValidator,
-      ExternalIds externalIds) {
+      OutgoingEmailValidator emailValidator) {
     this.readFromGit = cfg.getBoolean("user", null, "readAccountsFromGit", false);
     this.repoManager = repoManager;
     this.allUsersName = allUsersName;
     this.emailValidator = emailValidator;
-    this.externalIds = externalIds;
   }
 
   public Account get(ReviewDb db, Account.Id accountId)
@@ -99,44 +90,6 @@
   }
 
   /**
-   * Returns the accounts with the given email.
-   *
-   * <p>Each email should belong to a single account only. This means if more than one account is
-   * returned there is an inconsistency in the external IDs.
-   *
-   * <p>The accounts are retrieved via the external ID cache. Each access to the external ID cache
-   * requires reading the SHA1 of the refs/meta/external-ids branch. If accounts for multiple emails
-   * are needed it is more efficient to use {@link #byEmails(String...)} as this method reads the
-   * SHA1 of the refs/meta/external-ids branch only once (and not once per email).
-   *
-   * @see #byEmails(String...)
-   */
-  public ImmutableSet<Account.Id> byEmail(String email) throws IOException {
-    return externalIds.byEmail(email).stream().map(e -> e.accountId()).collect(toImmutableSet());
-  }
-
-  /**
-   * Returns the accounts for the given emails.
-   *
-   * <p>Each email should belong to a single account only. This means if more than one account for
-   * an email is returned there is an inconsistency in the external IDs.
-   *
-   * <p>The accounts are retrieved via the external ID cache. Each access to the external ID cache
-   * requires reading the SHA1 of the refs/meta/external-ids branch. If accounts for multiple emails
-   * are needed it is more efficient to use this method instead of {@link #byEmail(String)} as this
-   * method reads the SHA1 of the refs/meta/external-ids branch only once (and not once per email).
-   *
-   * @see #byEmail(String)
-   */
-  public ImmutableSetMultimap<String, Account.Id> byEmails(String... emails) throws IOException {
-    return externalIds
-        .byEmails(emails)
-        .entries()
-        .stream()
-        .collect(toImmutableSetMultimap(Map.Entry::getKey, e -> e.getValue().accountId()));
-  }
-
-  /**
    * Returns all accounts.
    *
    * @return all accounts
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
index 77d45f8..1812ef4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -69,7 +69,6 @@
   private final SshKeyCache sshKeyCache;
   private final AccountCache accountCache;
   private final AccountsUpdate.User accountsUpdate;
-  private final AccountByEmailCache byEmailCache;
   private final AccountLoader.Factory infoLoader;
   private final DynamicSet<AccountExternalIdCreator> externalIdCreators;
   private final ExternalIds externalIds;
@@ -87,7 +86,6 @@
       SshKeyCache sshKeyCache,
       AccountCache accountCache,
       AccountsUpdate.User accountsUpdate,
-      AccountByEmailCache byEmailCache,
       AccountLoader.Factory infoLoader,
       DynamicSet<AccountExternalIdCreator> externalIdCreators,
       ExternalIds externalIds,
@@ -102,7 +100,6 @@
     this.sshKeyCache = sshKeyCache;
     this.accountCache = accountCache;
     this.accountsUpdate = accountsUpdate;
-    this.byEmailCache = byEmailCache;
     this.infoLoader = infoLoader;
     this.externalIdCreators = externalIdCreators;
     this.externalIds = externalIds;
@@ -202,7 +199,6 @@
     }
 
     accountCache.evictByUsername(username);
-    byEmailCache.evict(input.email);
 
     AccountLoader loader = infoLoader.create(true);
     AccountInfo info = loader.get(id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index d8e46f4..9d9cf23 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -19,20 +19,23 @@
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.config.AuthConfig;
+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.Set;
 
 @Singleton
 public class DefaultRealm extends AbstractRealm {
   private final EmailExpander emailExpander;
-  private final AccountByEmailCache byEmail;
+  private final Provider<Emails> emails;
   private final AuthConfig authConfig;
 
   @Inject
-  DefaultRealm(EmailExpander emailExpander, AccountByEmailCache byEmail, AuthConfig authConfig) {
+  DefaultRealm(EmailExpander emailExpander, Provider<Emails> emails, AuthConfig authConfig) {
     this.emailExpander = emailExpander;
-    this.byEmail = byEmail;
+    this.emails = emails;
     this.authConfig = authConfig;
   }
 
@@ -75,11 +78,15 @@
   public void onCreateAccount(AuthRequest who, Account account) {}
 
   @Override
-  public Account.Id lookup(String accountName) {
+  public Account.Id lookup(String accountName) throws IOException {
     if (emailExpander.canExpand(accountName)) {
-      final Set<Account.Id> c = byEmail.get(emailExpander.expand(accountName));
-      if (1 == c.size()) {
-        return c.iterator().next();
+      try {
+        Set<Account.Id> c = emails.get().getAccountFor(emailExpander.expand(accountName));
+        if (1 == c.size()) {
+          return c.iterator().next();
+        }
+      } catch (OrmException e) {
+        throw new IOException("Failed to query accounts by email", e);
       }
     }
     return null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
index e31f481..15f4509 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// 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.
@@ -14,80 +14,85 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.common.base.Strings;
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.AcceptsCreate;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.ChildCollection;
-import com.google.gerrit.extensions.restapi.IdString;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.AccountResource.Email;
-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 static com.google.common.collect.ImmutableSet.toImmutableSet;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.Streams;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+
+/** Class to access accounts by email. */
 @Singleton
-public class Emails
-    implements ChildCollection<AccountResource, AccountResource.Email>,
-        AcceptsCreate<AccountResource> {
-  private final DynamicMap<RestView<AccountResource.Email>> views;
-  private final GetEmails list;
-  private final Provider<CurrentUser> self;
-  private final PermissionBackend permissionBackend;
-  private final CreateEmail.Factory createEmailFactory;
+public class Emails {
+  private final ExternalIds externalIds;
+  private final InternalAccountQuery accountQuery;
 
   @Inject
-  Emails(
-      DynamicMap<RestView<AccountResource.Email>> views,
-      GetEmails list,
-      Provider<CurrentUser> self,
-      PermissionBackend permissionBackend,
-      CreateEmail.Factory createEmailFactory) {
-    this.views = views;
-    this.list = list;
-    this.self = self;
-    this.permissionBackend = permissionBackend;
-    this.createEmailFactory = createEmailFactory;
+  public Emails(ExternalIds externalIds, InternalAccountQuery accountQuery) {
+    this.externalIds = externalIds;
+    this.accountQuery = accountQuery;
   }
 
-  @Override
-  public RestView<AccountResource> list() {
-    return list;
+  /**
+   * Returns the accounts with the given email.
+   *
+   * <p>Each email should belong to a single account only. This means if more than one account is
+   * returned there is an inconsistency in the external IDs.
+   *
+   * <p>The accounts are retrieved via the external ID cache. Each access to the external ID cache
+   * requires reading the SHA1 of the refs/meta/external-ids branch. If accounts for multiple emails
+   * are needed it is more efficient to use {@link #getAccountsFor(String...)} as this method reads
+   * the SHA1 of the refs/meta/external-ids branch only once (and not once per email).
+   *
+   * <p>In addition accounts are included that have the given email as preferred email even if they
+   * have no external ID for the preferred email. Having accounts with a preferred email that does
+   * not exist as external ID is an inconsistency, but existing functionality relies on still
+   * getting those accounts, which is why they are included. Accounts by preferred email are fetched
+   * from the account index.
+   *
+   * @see #getAccountsFor(String...)
+   */
+  public ImmutableSet<Account.Id> getAccountFor(String email) throws IOException, OrmException {
+    List<AccountState> byPreferredEmail = accountQuery.byPreferredEmail(email);
+    return Streams.concat(
+            externalIds.byEmail(email).stream().map(e -> e.accountId()),
+            byPreferredEmail
+                .stream()
+                // the index query also matches prefixes and emails with other case,
+                // filter those out
+                .filter(a -> email.equals(a.getAccount().getPreferredEmail()))
+                .map(a -> a.getAccount().getId()))
+        .collect(toImmutableSet());
   }
 
-  @Override
-  public AccountResource.Email parse(AccountResource rsrc, IdString id)
-      throws ResourceNotFoundException, PermissionBackendException, AuthException {
-    if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
-    }
-
-    if ("preferred".equals(id.get())) {
-      String email = rsrc.getUser().getAccount().getPreferredEmail();
-      if (Strings.isNullOrEmpty(email)) {
-        throw new ResourceNotFoundException(id);
-      }
-      return new AccountResource.Email(rsrc.getUser(), email);
-    } else if (rsrc.getUser().hasEmailAddress(id.get())) {
-      return new AccountResource.Email(rsrc.getUser(), id.get());
-    } else {
-      throw new ResourceNotFoundException(id);
-    }
-  }
-
-  @Override
-  public DynamicMap<RestView<Email>> views() {
-    return views;
-  }
-
-  @SuppressWarnings("unchecked")
-  @Override
-  public CreateEmail create(AccountResource parent, IdString email) {
-    return createEmailFactory.create(email.get());
+  /**
+   * Returns the accounts for the given emails.
+   *
+   * @see #getAccountFor(String)
+   */
+  public ImmutableSetMultimap<String, Account.Id> getAccountsFor(String... emails)
+      throws IOException, OrmException {
+    ImmutableSetMultimap.Builder<String, Account.Id> builder = ImmutableSetMultimap.builder();
+    externalIds
+        .byEmails(emails)
+        .entries()
+        .stream()
+        .forEach(e -> builder.put(e.getKey(), e.getValue().accountId()));
+    accountQuery
+        .byPreferredEmail(emails)
+        .entries()
+        .stream()
+        // the index query also matches prefixes and emails with other case,
+        // filter those out
+        .filter(e -> e.getKey().equals(e.getValue().getAccount().getPreferredEmail()))
+        .forEach(e -> builder.put(e.getKey(), e.getValue().getAccount().getId()));
+    return builder.build();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailsCollection.java
new file mode 100644
index 0000000..1c329bb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailsCollection.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountResource.Email;
+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;
+
+@Singleton
+public class EmailsCollection
+    implements ChildCollection<AccountResource, AccountResource.Email>,
+        AcceptsCreate<AccountResource> {
+  private final DynamicMap<RestView<AccountResource.Email>> views;
+  private final GetEmails list;
+  private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
+  private final CreateEmail.Factory createEmailFactory;
+
+  @Inject
+  EmailsCollection(
+      DynamicMap<RestView<AccountResource.Email>> views,
+      GetEmails list,
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
+      CreateEmail.Factory createEmailFactory) {
+    this.views = views;
+    this.list = list;
+    this.self = self;
+    this.permissionBackend = permissionBackend;
+    this.createEmailFactory = createEmailFactory;
+  }
+
+  @Override
+  public RestView<AccountResource> list() {
+    return list;
+  }
+
+  @Override
+  public AccountResource.Email parse(AccountResource rsrc, IdString id)
+      throws ResourceNotFoundException, PermissionBackendException, AuthException {
+    if (self.get() != rsrc.getUser()) {
+      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
+    }
+
+    if ("preferred".equals(id.get())) {
+      String email = rsrc.getUser().getAccount().getPreferredEmail();
+      if (Strings.isNullOrEmpty(email)) {
+        throw new ResourceNotFoundException(id);
+      }
+      return new AccountResource.Email(rsrc.getUser(), email);
+    } else if (rsrc.getUser().hasEmailAddress(id.get())) {
+      return new AccountResource.Email(rsrc.getUser(), id.get());
+    } else {
+      throw new ResourceNotFoundException(id);
+    }
+  }
+
+  @Override
+  public DynamicMap<RestView<Email>> views() {
+    return views;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public CreateEmail create(AccountResource parent, IdString email) {
+    return createEmailFactory.create(email.get());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetOAuthToken.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetOAuthToken.java
index 8df8c6b..1099d6b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetOAuthToken.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetOAuthToken.java
@@ -27,11 +27,14 @@
 import com.google.inject.Singleton;
 import java.net.URI;
 import java.net.URISyntaxException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Singleton
 class GetOAuthToken implements RestReadView<AccountResource> {
 
   private static final String BEARER_TYPE = "bearer";
+  private static final Logger log = LoggerFactory.getLogger(GetOAuthToken.class);
 
   private final Provider<CurrentUser> self;
   private final OAuthTokenCache tokenCache;
@@ -69,9 +72,15 @@
   }
 
   private static String getHostName(String canonicalWebUrl) {
+    if (canonicalWebUrl == null) {
+      log.error("No canonicalWebUrl defined in gerrit.config, OAuth may not work properly");
+      return null;
+    }
+
     try {
       return new URI(canonicalWebUrl).getHost();
     } catch (URISyntaxException e) {
+      log.error("Invalid canonicalWebUrl '" + canonicalWebUrl + "'", e);
       return null;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
index 775ce6d..44060be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
@@ -51,7 +51,7 @@
     get(ACCOUNT_KIND, "active").to(GetActive.class);
     put(ACCOUNT_KIND, "active").to(PutActive.class);
     delete(ACCOUNT_KIND, "active").to(DeleteActive.class);
-    child(ACCOUNT_KIND, "emails").to(Emails.class);
+    child(ACCOUNT_KIND, "emails").to(EmailsCollection.class);
     get(EMAIL_KIND).to(GetEmail.class);
     put(EMAIL_KIND).to(PutEmail.class);
     delete(EMAIL_KIND).to(DeleteEmail.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index 5d551bc..b5e4cba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
+import java.io.IOException;
 import java.util.Set;
 
 public interface Realm {
@@ -43,5 +44,5 @@
    * where there is an {@link EmailExpander} configured that knows how to convert the accountName
    * into an email address, and then locate the user by that email address.
    */
-  Account.Id lookup(String accountName);
+  Account.Id lookup(String accountName) throws IOException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
index 995aaa5..a6c844b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.QueryChanges;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
@@ -67,7 +68,7 @@
 
   @Override
   public AccountResource.StarredChange parse(AccountResource parent, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, OrmException, PermissionBackendException {
     IdentifiedUser user = parent.getUser();
     ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
     if (starredChangesUtil
@@ -104,7 +105,7 @@
       return createProvider.get().setChange(changes.parse(TopLevelResource.INSTANCE, id));
     } catch (ResourceNotFoundException e) {
       throw new UnprocessableEntityException(String.format("change %s not found", id.get()));
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("cannot resolve change", e);
       throw new UnprocessableEntityException("internal server error");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Stars.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Stars.java
index 52c6cdf..860f396 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Stars.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Stars.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.account.AccountResource.Star;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.QueryChanges;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -65,7 +66,7 @@
 
   @Override
   public Star parse(AccountResource parent, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, OrmException, PermissionBackendException {
     IdentifiedUser user = parent.getUser();
     ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
     Set<String> labels = starredChangesUtil.getLabels(user.getAccountId(), change.getId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 3e8a146..d400999 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -53,7 +53,9 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
@@ -475,8 +477,12 @@
             accountId -> {
               try {
                 IdentifiedUser user = userFactory.create(accountId);
-                return changeControlFactory.controlFor(notes, user).isVisible(db);
-              } catch (OrmException e) {
+                return permissionBackend
+                    .user(user)
+                    .change(notes)
+                    .database(db)
+                    .test(ChangePermission.READ);
+              } catch (PermissionBackendException e) {
                 log.warn(
                     String.format(
                         "Failed to check if account %d can see change %d",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
index eeb1ab3..d967ab8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -26,6 +26,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeFinder;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.change.QueryChanges;
 import com.google.gwtorm.server.OrmException;
@@ -44,6 +47,7 @@
   private final ChangeFinder changeFinder;
   private final CreateChange createChange;
   private final ChangeResource.Factory changeResourceFactory;
+  private final PermissionBackend permissionBackend;
 
   @Inject
   ChangesCollection(
@@ -53,7 +57,8 @@
       DynamicMap<RestView<ChangeResource>> views,
       ChangeFinder changeFinder,
       CreateChange createChange,
-      ChangeResource.Factory changeResourceFactory) {
+      ChangeResource.Factory changeResourceFactory,
+      PermissionBackend permissionBackend) {
     this.db = db;
     this.user = user;
     this.queryFactory = queryFactory;
@@ -61,6 +66,7 @@
     this.changeFinder = changeFinder;
     this.createChange = createChange;
     this.changeResourceFactory = changeResourceFactory;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
@@ -75,7 +81,7 @@
 
   @Override
   public ChangeResource parse(TopLevelResource root, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, OrmException, PermissionBackendException {
     List<ChangeControl> ctls = changeFinder.find(id.encoded(), user.get());
     if (ctls.isEmpty()) {
       throw new ResourceNotFoundException(id);
@@ -84,13 +90,14 @@
     }
 
     ChangeControl ctl = ctls.get(0);
-    if (!ctl.isVisible(db.get())) {
+    if (!canRead(ctl)) {
       throw new ResourceNotFoundException(id);
     }
     return changeResourceFactory.create(ctl);
   }
 
-  public ChangeResource parse(Change.Id id) throws ResourceNotFoundException, OrmException {
+  public ChangeResource parse(Change.Id id)
+      throws ResourceNotFoundException, OrmException, PermissionBackendException {
     List<ChangeControl> ctls = changeFinder.find(id, user.get());
     if (ctls.isEmpty()) {
       throw new ResourceNotFoundException(toIdString(id));
@@ -99,7 +106,7 @@
     }
 
     ChangeControl ctl = ctls.get(0);
-    if (!ctl.isVisible(db.get())) {
+    if (!canRead(ctl)) {
       throw new ResourceNotFoundException(toIdString(id));
     }
     return changeResourceFactory.create(ctl);
@@ -118,4 +125,12 @@
   public CreateChange post(TopLevelResource parent) throws RestApiException {
     return createChange;
   }
+
+  private boolean canRead(ChangeControl ctl) throws PermissionBackendException {
+    return permissionBackend
+        .user(user)
+        .change(ctl.getNotes())
+        .database(db)
+        .test(ChangePermission.READ);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
index 92ad46d..2a0a412 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -52,13 +52,16 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.CommitsCollection;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
@@ -100,6 +103,7 @@
   private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> user;
   private final ProjectsCollection projectsCollection;
+  private final CommitsCollection commits;
   private final ChangeInserter.Factory changeInserterFactory;
   private final ChangeJson.Factory jsonFactory;
   private final ChangeFinder changeFinder;
@@ -121,6 +125,7 @@
       PermissionBackend permissionBackend,
       Provider<CurrentUser> user,
       ProjectsCollection projectsCollection,
+      CommitsCollection commits,
       ChangeInserter.Factory changeInserterFactory,
       ChangeJson.Factory json,
       ChangeFinder changeFinder,
@@ -139,6 +144,7 @@
     this.permissionBackend = permissionBackend;
     this.user = user;
     this.projectsCollection = projectsCollection;
+    this.commits = commits;
     this.changeInserterFactory = changeInserterFactory;
     this.jsonFactory = json;
     this.changeFinder = changeFinder;
@@ -195,7 +201,11 @@
           throw new UnprocessableEntityException("Base change not found: " + input.baseChange);
         }
         ChangeControl ctl = Iterables.getOnlyElement(ctls);
-        if (!ctl.isVisible(db.get())) {
+        if (!permissionBackend
+            .user(user)
+            .change(ctl.getNotes())
+            .database(db)
+            .test(ChangePermission.READ)) {
           throw new UnprocessableEntityException("Base change not found: " + input.baseChange);
         }
         PatchSet ps = psUtil.current(db.get(), ctl.getNotes());
@@ -311,12 +321,13 @@
       throw new BadRequestException("merge.source must be non-empty");
     }
 
+    ProjectState state = projectControl.getProjectState();
     RevCommit sourceCommit = MergeUtil.resolveCommit(repo, rw, merge.source);
-    if (!projectControl.canReadCommit(db.get(), repo, sourceCommit)) {
+    if (!commits.canRead(state, repo, sourceCommit)) {
       throw new BadRequestException("do not have read permission for: " + merge.source);
     }
 
-    MergeUtil mergeUtil = mergeUtilFactory.create(projectControl.getProjectState());
+    MergeUtil mergeUtil = mergeUtilFactory.create(state);
     // default merge strategy from project settings
     String mergeStrategy =
         MoreObjects.firstNonNull(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
index b53bdd9..0b7d495 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
@@ -43,8 +43,10 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.CommitsCollection;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -69,9 +71,9 @@
 @Singleton
 public class CreateMergePatchSet
     extends RetryingRestModifyView<ChangeResource, MergePatchSetInput, Response<ChangeInfo>> {
-
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager gitManager;
+  private final CommitsCollection commits;
   private final TimeZone serverTimeZone;
   private final Provider<CurrentUser> user;
   private final ChangeJson.Factory jsonFactory;
@@ -83,6 +85,7 @@
   CreateMergePatchSet(
       Provider<ReviewDb> db,
       GitRepositoryManager gitManager,
+      CommitsCollection commits,
       @GerritPersonIdent PersonIdent myIdent,
       Provider<CurrentUser> user,
       ChangeJson.Factory json,
@@ -93,6 +96,7 @@
     super(retryHelper);
     this.db = db;
     this.gitManager = gitManager;
+    this.commits = commits;
     this.serverTimeZone = myIdent.getTimeZone();
     this.user = user;
     this.jsonFactory = json;
@@ -116,6 +120,7 @@
     ChangeControl ctl = rsrc.getControl();
     PatchSet ps = psUtil.current(db.get(), ctl.getNotes());
     ProjectControl projectControl = ctl.getProjectControl();
+    ProjectState state = projectControl.getProjectState();
     Change change = ctl.getChange();
     Project.NameKey project = change.getProject();
     Branch.NameKey dest = change.getDest();
@@ -125,7 +130,7 @@
         RevWalk rw = new RevWalk(reader)) {
 
       RevCommit sourceCommit = MergeUtil.resolveCommit(git, rw, merge.source);
-      if (!projectControl.canReadCommit(db.get(), git, sourceCommit)) {
+      if (!commits.canRead(state, git, sourceCommit)) {
         throw new ResourceNotFoundException(
             "cannot find source commit: " + merge.source + " to merge.");
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
index 8a6a1ab..cb77fd1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.ChangeSet;
 import com.google.gerrit.server.git.MergeSuperSet;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.OrmRuntimeException;
@@ -74,7 +75,7 @@
         changeResourceFactory.create(cd.changeControl()).prepareETag(h, user);
       }
       h.putBoolean(cs.furtherHiddenChanges());
-    } catch (IOException | OrmException e) {
+    } catch (IOException | OrmException | PermissionBackendException e) {
       throw new OrmRuntimeException(e);
     }
     return h.hash().toString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
index d2fb95a..cafd73b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
@@ -65,6 +66,7 @@
   private final GitRepositoryManager gitManager;
   private final AccountCache accountCache;
   private final Accounts accounts;
+  private final Emails emails;
   private final ProjectCache projectCache;
   private final MergeUtil.Factory mergeUtilFactory;
   private final ChangeData.Factory changeDataFactory;
@@ -77,6 +79,7 @@
       GitRepositoryManager gitManager,
       AccountCache accountCache,
       Accounts accounts,
+      Emails emails,
       ProjectCache projectCache,
       MergeUtil.Factory mergeUtilFactory,
       ChangeData.Factory changeDataFactory,
@@ -86,6 +89,7 @@
     this.gitManager = gitManager;
     this.accountCache = accountCache;
     this.accounts = accounts;
+    this.emails = emails;
     this.projectCache = projectCache;
     this.mergeUtilFactory = mergeUtilFactory;
     this.changeDataFactory = changeDataFactory;
@@ -148,7 +152,9 @@
 
   private SubmitType getSubmitType(ChangeData cd, PatchSet patchSet) throws OrmException {
     SubmitTypeRecord rec =
-        new SubmitRuleEvaluator(accountCache, accounts, cd).setPatchSet(patchSet).getSubmitType();
+        new SubmitRuleEvaluator(accountCache, accounts, emails, cd)
+            .setPatchSet(patchSet)
+            .getSubmitType();
     if (rec.status != SubmitTypeRecord.Status.OK) {
       throw new OrmException("Submit type rule failed: " + rec);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index a7711b6..f7e4469 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -56,6 +56,7 @@
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
@@ -338,8 +339,12 @@
       ReviewerState state,
       NotifyHandling notify,
       ListMultimap<RecipientType, Account.Id> accountsToNotify)
-      throws OrmException {
-    if (!rsrc.getControl().forUser(anonymousProvider.get()).isVisible(dbProvider.get())) {
+      throws PermissionBackendException {
+    if (!permissionBackend
+        .user(anonymousProvider)
+        .change(rsrc.getNotes())
+        .database(dbProvider)
+        .test(ChangePermission.READ)) {
       return fail(
           reviewer, MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, reviewer));
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
index 9ef445d..5724941 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeOpRepoManager;
 import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.update.UpdateException;
@@ -83,7 +84,8 @@
 
   @Override
   public BinaryResult apply(RevisionResource rsrc)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
+      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     if (Strings.isNullOrEmpty(format)) {
       throw new BadRequestException("format is not specified");
     }
@@ -111,7 +113,8 @@
   }
 
   private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormat f)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
+      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     ReviewDb db = dbProvider.get();
     ChangeControl control = rsrc.getControl();
     IdentifiedUser caller = control.getUser().asIdentifiedUser();
@@ -131,7 +134,8 @@
         | UpdateException
         | IOException
         | ConfigInvalidException
-        | RuntimeException e) {
+        | RuntimeException
+        | PermissionBackendException e) {
       op.close();
       throw e;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index fa7fdfd..5551320 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.permissions.LabelPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -52,6 +53,7 @@
   private final ChangeData.Factory changeDataFactory;
   private final AccountCache accountCache;
   private final Accounts accounts;
+  private final Emails emails;
   private final ApprovalsUtil approvalsUtil;
   private final AccountLoader.Factory accountLoaderFactory;
 
@@ -62,6 +64,7 @@
       ChangeData.Factory changeDataFactory,
       AccountCache accountCache,
       Accounts accounts,
+      Emails emails,
       ApprovalsUtil approvalsUtil,
       AccountLoader.Factory accountLoaderFactory) {
     this.db = db;
@@ -69,6 +72,7 @@
     this.changeDataFactory = changeDataFactory;
     this.accountCache = accountCache;
     this.accounts = accounts;
+    this.emails = emails;
     this.approvalsUtil = approvalsUtil;
     this.accountLoaderFactory = accountLoaderFactory;
   }
@@ -137,7 +141,7 @@
     PatchSet ps = cd.currentPatchSet();
     if (ps != null) {
       for (SubmitRecord rec :
-          new SubmitRuleEvaluator(accountCache, accounts, cd)
+          new SubmitRuleEvaluator(accountCache, accounts, emails, cd)
               .setFastEvalLabels(true)
               .setAllowDraft(true)
               .evaluate()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 3dd467a..81f830c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -217,7 +217,8 @@
   }
 
   public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
-      throws OrmException, RestApiException, IOException, UpdateException, ConfigInvalidException {
+      throws OrmException, RestApiException, IOException, UpdateException, ConfigInvalidException,
+          PermissionBackendException {
     Change change = rsrc.getChange();
     if (!change.getStatus().isOpen()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
@@ -340,7 +341,7 @@
     ChangeSet cs;
     try {
       cs = mergeSuperSet.get().completeChangeSet(db, cd.change(), resource.getControl().getUser());
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | IOException | PermissionBackendException e) {
       throw new OrmRuntimeException(
           "Could not determine complete set of changes to be submitted", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
index 568b50a..ea53dc3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.change.WalkSorter.PatchSetData;
 import com.google.gerrit.server.git.ChangeSet;
 import com.google.gerrit.server.git.MergeSuperSet;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
@@ -107,7 +108,7 @@
   @Override
   public Object apply(ChangeResource resource)
       throws AuthException, BadRequestException, ResourceConflictException, IOException,
-          OrmException {
+          OrmException, PermissionBackendException {
     SubmittedTogetherInfo info = applyInfo(resource);
     if (options.isEmpty()) {
       return info.changes;
@@ -116,7 +117,7 @@
   }
 
   public SubmittedTogetherInfo applyInfo(ChangeResource resource)
-      throws AuthException, IOException, OrmException {
+      throws AuthException, IOException, OrmException, PermissionBackendException {
     Change c = resource.getChange();
     try {
       List<ChangeData> cds;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
index 81027c0..9e93465 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
@@ -44,6 +45,7 @@
   private final AccountCache accountCache;
   private final AccountLoader.Factory accountInfoFactory;
   private final Accounts accounts;
+  private final Emails emails;
 
   @Option(name = "--filters", usage = "impact of filters in parent projects")
   private Filters filters = Filters.RUN;
@@ -54,14 +56,16 @@
       ChangeData.Factory changeDataFactory,
       RulesCache rules,
       AccountCache accountCache,
+      AccountLoader.Factory infoFactory,
       Accounts accounts,
-      AccountLoader.Factory infoFactory) {
+      Emails emails) {
     this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.rules = rules;
     this.accountCache = accountCache;
     this.accountInfoFactory = infoFactory;
     this.accounts = accounts;
+    this.emails = emails;
   }
 
   @Override
@@ -76,7 +80,7 @@
     input.filters = MoreObjects.firstNonNull(input.filters, filters);
     SubmitRuleEvaluator evaluator =
         new SubmitRuleEvaluator(
-            accountCache, accounts, changeDataFactory.create(db.get(), rsrc.getControl()));
+            accountCache, accounts, emails, changeDataFactory.create(db.get(), rsrc.getControl()));
 
     List<SubmitRecord> records =
         evaluator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
index ee93923..5fb37e6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
@@ -38,6 +39,7 @@
   private final Provider<ReviewDb> db;
   private final AccountCache accountCache;
   private final Accounts accounts;
+  private final Emails emails;
   private final ChangeData.Factory changeDataFactory;
   private final RulesCache rules;
 
@@ -49,11 +51,13 @@
       Provider<ReviewDb> db,
       AccountCache accountCache,
       Accounts accounts,
+      Emails emails,
       ChangeData.Factory changeDataFactory,
       RulesCache rules) {
     this.db = db;
     this.accountCache = accountCache;
     this.accounts = accounts;
+    this.emails = emails;
     this.changeDataFactory = changeDataFactory;
     this.rules = rules;
   }
@@ -70,7 +74,7 @@
     input.filters = MoreObjects.firstNonNull(input.filters, filters);
     SubmitRuleEvaluator evaluator =
         new SubmitRuleEvaluator(
-            accountCache, accounts, changeDataFactory.create(db.get(), rsrc.getControl()));
+            accountCache, accounts, emails, changeDataFactory.create(db.get(), rsrc.getControl()));
 
     SubmitTypeRecord rec =
         evaluator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 31989e3..43afcc8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -79,7 +79,6 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.account.AccountByEmailCacheImpl;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountManager;
@@ -120,10 +119,10 @@
 import com.google.gerrit.server.git.MergedByPushOp;
 import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.git.ReceivePackInitializer;
-import com.google.gerrit.server.git.ReplaceOp;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.git.receive.ReceiveCommitsModule;
 import com.google.gerrit.server.git.strategy.SubmitStrategy;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.MergeValidationListener;
@@ -215,7 +214,6 @@
     bind(BlameCache.class).to(BlameCacheImpl.class);
     bind(Sequences.class);
     install(authModule);
-    install(AccountByEmailCacheImpl.module());
     install(AccountCacheImpl.module());
     install(BatchUpdate.module());
     install(ChangeKindCacheImpl.module());
@@ -239,6 +237,7 @@
     install(new GroupModule());
     install(new NoteDbModule(cfg));
     install(new PrologModule());
+    install(new ReceiveCommitsModule());
     install(new SshAddressesModule());
     install(ThreadLocalRequestContext.module());
 
@@ -400,7 +399,6 @@
     factory(MergeValidators.Factory.class);
     factory(ProjectConfigValidator.Factory.class);
     factory(NotesBranchUtil.Factory.class);
-    factory(ReplaceOp.Factory.class);
     factory(MergedByPushOp.Factory.class);
     factory(GitModules.Factory.class);
     factory(VersionedAuthorizedKeys.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index ce04f26..d31c26d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -35,8 +35,8 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.data.AccountAttribute;
@@ -83,9 +83,9 @@
   private static final Logger log = LoggerFactory.getLogger(EventFactory.class);
 
   private final AccountCache accountCache;
+  private final Emails emails;
   private final Provider<String> urlProvider;
   private final PatchListCache patchListCache;
-  private final AccountByEmailCache byEmailCache;
   private final PersonIdent myIdent;
   private final ChangeData.Factory changeDataFactory;
   private final ApprovalsUtil approvalsUtil;
@@ -96,8 +96,8 @@
   @Inject
   EventFactory(
       AccountCache accountCache,
+      Emails emails,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      AccountByEmailCache byEmailCache,
       PatchListCache patchListCache,
       @GerritPersonIdent PersonIdent myIdent,
       ChangeData.Factory changeDataFactory,
@@ -106,9 +106,9 @@
       Provider<InternalChangeQuery> queryProvider,
       SchemaFactory<ReviewDb> schema) {
     this.accountCache = accountCache;
+    this.emails = emails;
     this.urlProvider = urlProvider;
     this.patchListCache = patchListCache;
-    this.byEmailCache = byEmailCache;
     this.myIdent = myIdent;
     this.changeDataFactory = changeDataFactory;
     this.approvalsUtil = approvalsUtil;
@@ -497,7 +497,7 @@
         }
       }
       p.kind = changeKindCache.getChangeKind(db, change, patchSet);
-    } catch (IOException e) {
+    } catch (IOException | OrmException e) {
       log.error("Cannot load patch set data for " + patchSet.getId(), e);
     } catch (PatchListNotAvailableException e) {
       log.error(String.format("Cannot get size information for %s.", pId), e);
@@ -507,7 +507,7 @@
 
   // TODO: The same method exists in PatchSetInfoFactory, find a common place
   // for it
-  private UserIdentity toUserIdentity(PersonIdent who) {
+  private UserIdentity toUserIdentity(PersonIdent who) throws IOException, OrmException {
     UserIdentity u = new UserIdentity();
     u.setName(who.getName());
     u.setEmail(who.getEmailAddress());
@@ -517,7 +517,7 @@
     // If only one account has access to this email address, select it
     // as the identity of the user.
     //
-    Set<Account.Id> a = byEmailCache.get(u.getEmail());
+    Set<Account.Id> a = emails.getAccountFor(u.getEmail());
     if (a.size() == 1) {
       u.setAccount(a.iterator().next());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index b79f137..318c251 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -53,6 +53,7 @@
 import com.google.gerrit.server.data.RefUpdateAttribute;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.OrmException;
@@ -261,7 +262,7 @@
       event.oldAssignee = accountAttributeSupplier(ev.getOldAssignee());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -277,7 +278,7 @@
       event.oldTopic = ev.getOldTopic();
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -295,7 +296,7 @@
       event.uploader = accountAttributeSupplier(ev.getWho());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -315,7 +316,7 @@
           approvalsAttributeSupplier(change, ev.getNewApprovals(), ev.getOldApprovals());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -333,7 +334,7 @@
         event.reviewer = accountAttributeSupplier(reviewer);
         dispatcher.get().postEvent(change, event);
       }
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -360,7 +361,7 @@
       event.removed = hashtagArray(ev.getRemovedHashtags());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -399,7 +400,7 @@
       event.uploader = accountAttributeSupplier(ev.getWho());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -419,7 +420,7 @@
       event.approvals = approvalsAttributeSupplier(change, ev.getApprovals(), ev.getOldApprovals());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -437,7 +438,7 @@
       event.reason = ev.getReason();
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -455,7 +456,7 @@
       event.newRev = ev.getNewRevisionId();
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -473,7 +474,7 @@
       event.reason = ev.getReason();
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -493,7 +494,7 @@
       event.approvals = approvalsAttributeSupplier(change, ev.getApprovals(), ev.getOldApprovals());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
deleted file mode 100644
index f615321..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
+++ /dev/null
@@ -1,179 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.inject.Inject;
-import com.google.inject.PrivateModule;
-import com.google.inject.Provides;
-import com.google.inject.Singleton;
-import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.FactoryModuleBuilder;
-import com.google.inject.name.Named;
-import java.io.OutputStream;
-import java.util.Collection;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.PreReceiveHook;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.transport.ReceiveCommand.Result;
-import org.eclipse.jgit.transport.ReceivePack;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/** Hook that delegates to {@link ReceiveCommits} in a worker thread. */
-public class AsyncReceiveCommits implements PreReceiveHook {
-  private static final Logger log = LoggerFactory.getLogger(AsyncReceiveCommits.class);
-
-  private static final String TIMEOUT_NAME = "ReceiveCommitsOverallTimeout";
-
-  public interface Factory {
-    AsyncReceiveCommits create(ProjectControl projectControl, Repository repository);
-  }
-
-  public static class Module extends PrivateModule {
-    @Override
-    public void configure() {
-      install(new FactoryModuleBuilder().build(AsyncReceiveCommits.Factory.class));
-      expose(AsyncReceiveCommits.Factory.class);
-      // Don't expose the binding for ReceiveCommits.Factory. All callers should
-      // be using AsyncReceiveCommits.Factory instead.
-      install(new FactoryModuleBuilder().build(ReceiveCommits.Factory.class));
-    }
-
-    @Provides
-    @Singleton
-    @Named(TIMEOUT_NAME)
-    long getTimeoutMillis(@GerritServerConfig Config cfg) {
-      return ConfigUtil.getTimeUnit(
-          cfg, "receive", null, "timeout", TimeUnit.MINUTES.toMillis(4), TimeUnit.MILLISECONDS);
-    }
-  }
-
-  private class Worker implements ProjectRunnable {
-    private final Collection<ReceiveCommand> commands;
-
-    private Worker(Collection<ReceiveCommand> commands) {
-      this.commands = commands;
-    }
-
-    @Override
-    public void run() {
-      rc.processCommands(commands, progress);
-    }
-
-    @Override
-    public Project.NameKey getProjectNameKey() {
-      return rc.getProject().getNameKey();
-    }
-
-    @Override
-    public String getRemoteName() {
-      return null;
-    }
-
-    @Override
-    public boolean hasCustomizedPrint() {
-      return true;
-    }
-
-    @Override
-    public String toString() {
-      return "receive-commits";
-    }
-  }
-
-  private class MessageSenderOutputStream extends OutputStream {
-    @Override
-    public void write(int b) {
-      rc.getMessageSender().sendBytes(new byte[] {(byte) b});
-    }
-
-    @Override
-    public void write(byte[] what, int off, int len) {
-      rc.getMessageSender().sendBytes(what, off, len);
-    }
-
-    @Override
-    public void write(byte[] what) {
-      rc.getMessageSender().sendBytes(what);
-    }
-
-    @Override
-    public void flush() {
-      rc.getMessageSender().flush();
-    }
-  }
-
-  private final ReceiveCommits rc;
-  private final ExecutorService executor;
-  private final RequestScopePropagator scopePropagator;
-  private final MultiProgressMonitor progress;
-  private final long timeoutMillis;
-
-  @Inject
-  AsyncReceiveCommits(
-      ReceiveCommits.Factory factory,
-      @ReceiveCommitsExecutor ExecutorService executor,
-      RequestScopePropagator scopePropagator,
-      @Named(TIMEOUT_NAME) long timeoutMillis,
-      @Assisted ProjectControl projectControl,
-      @Assisted Repository repo) {
-    this.executor = executor;
-    this.scopePropagator = scopePropagator;
-    rc = factory.create(projectControl, repo);
-    rc.getReceivePack().setPreReceiveHook(this);
-
-    progress = new MultiProgressMonitor(new MessageSenderOutputStream(), "Processing changes");
-    this.timeoutMillis = timeoutMillis;
-  }
-
-  @Override
-  public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
-    try {
-      progress.waitFor(
-          executor.submit(scopePropagator.wrap(new Worker(commands))),
-          timeoutMillis,
-          TimeUnit.MILLISECONDS);
-    } catch (ExecutionException e) {
-      log.warn(
-          String.format(
-              "Error in ReceiveCommits while processing changes for project %s",
-              rc.getProject().getName()),
-          e);
-      rc.addError("internal error while processing changes");
-      // ReceiveCommits has tried its best to catch errors, so anything at this
-      // point is very bad.
-      for (ReceiveCommand c : commands) {
-        if (c.getResult() == Result.NOT_ATTEMPTED) {
-          c.setResult(Result.REJECTED_OTHER_REASON, "internal error");
-        }
-      }
-    } finally {
-      rc.sendMessages();
-    }
-  }
-
-  public ReceiveCommits getReceiveCommits() {
-    return rc;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
index 36805d6..15a9d74 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
@@ -25,7 +25,6 @@
     factory(RenameGroupOp.Factory.class);
     factory(MetaDataUpdate.InternalFactory.class);
     bind(MetaDataUpdate.Server.class);
-    bind(ReceiveConfig.class);
     DynamicSet.bind(binder(), PostUploadHook.class).to(UploadPackMetricsHook.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java
index 960c72a..4a7c7e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java
@@ -48,7 +48,7 @@
 import org.slf4j.LoggerFactory;
 
 /**
- * Helper for assigning groups to commits during {@link ReceiveCommits}.
+ * Helper for assigning groups to commits during {@code ReceiveCommits}.
  *
  * <p>For each commit encountered along a walk between the branch tip and the tip of the push, the
  * group of a commit is defined as follows:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index ea28fa9..5572db2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -63,6 +63,7 @@
 import com.google.gerrit.server.git.strategy.SubmitStrategyListener;
 import com.google.gerrit.server.git.validators.MergeValidationException;
 import com.google.gerrit.server.git.validators.MergeValidators;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.SubmitRuleOptions;
@@ -423,6 +424,8 @@
    * @param submitInput parameters regarding the merge
    * @throws OrmException an error occurred reading or writing the database.
    * @throws RestApiException if an error occurred.
+   * @throws PermissionBackendException if permissions can't be checked
+   * @throws IOException an error occurred reading from NoteDb.
    */
   public void merge(
       ReviewDb db,
@@ -431,7 +434,8 @@
       boolean checkSubmitRules,
       SubmitInput submitInput,
       boolean dryrun)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
+      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     this.submitInput = submitInput;
     this.accountsToNotify = notifyUtil.resolveAccounts(submitInput.notifyDetails);
     this.dryrun = dryrun;
@@ -487,7 +491,12 @@
             }
             return null;
           },
-          retryTracker);
+          RetryHelper.options()
+              .listener(retryTracker)
+              // Up to the entire submit operation is retried, including possibly many projects.
+              // Multiply the timeout by the number of projects we're actually attempting to submit.
+              .timeout(retryHelper.getDefaultTimeout().multipliedBy(cs.projects().size()))
+              .build());
 
       if (projects > 1) {
         topicMetrics.topicSubmissionsCompleted.increment();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
index ad205f8..6d20864 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -173,7 +173,7 @@
     openRepos = new HashMap<>();
   }
 
-  void setContext(ReviewDb db, Timestamp ts, IdentifiedUser caller, RequestId submissionId) {
+  public void setContext(ReviewDb db, Timestamp ts, IdentifiedUser caller, RequestId submissionId) {
     this.db = db;
     this.ts = ts;
     this.caller = caller;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
index e880543..978ef3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
@@ -33,10 +33,14 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
@@ -98,9 +102,11 @@
 
   private final AccountCache accountCache;
   private final Accounts accounts;
+  private final Emails emails;
   private final ChangeData.Factory changeDataFactory;
   private final Provider<InternalChangeQuery> queryProvider;
   private final Provider<MergeOpRepoManager> repoManagerProvider;
+  private final PermissionBackend permissionBackend;
   private final Config cfg;
   private final Map<QueryKey, List<ChangeData>> queryCache;
   private final Map<Branch.NameKey, Optional<RevCommit>> heads;
@@ -113,15 +119,19 @@
       @GerritServerConfig Config cfg,
       AccountCache accountCache,
       Accounts accounts,
+      Emails emails,
       ChangeData.Factory changeDataFactory,
       Provider<InternalChangeQuery> queryProvider,
-      Provider<MergeOpRepoManager> repoManagerProvider) {
+      Provider<MergeOpRepoManager> repoManagerProvider,
+      PermissionBackend permissionBackend) {
     this.cfg = cfg;
     this.accountCache = accountCache;
     this.accounts = accounts;
+    this.emails = emails;
     this.changeDataFactory = changeDataFactory;
     this.queryProvider = queryProvider;
     this.repoManagerProvider = repoManagerProvider;
+    this.permissionBackend = permissionBackend;
     queryCache = new HashMap<>();
     heads = new HashMap<>();
   }
@@ -134,11 +144,13 @@
   }
 
   public ChangeSet completeChangeSet(ReviewDb db, Change change, CurrentUser user)
-      throws IOException, OrmException {
+      throws IOException, OrmException, PermissionBackendException {
     try {
       ChangeData cd = changeDataFactory.create(db, change.getProject(), change.getId());
       cd.changeControl(user);
-      ChangeSet cs = new ChangeSet(cd, cd.changeControl().isVisible(db, cd));
+      ChangeSet cs =
+          new ChangeSet(
+              cd, permissionBackend.user(user).change(cd).database(db).test(ChangePermission.READ));
       if (Submit.wholeTopicEnabled(cfg)) {
         return completeChangeSetIncludingTopics(db, cs, user);
       }
@@ -169,7 +181,9 @@
     SubmitTypeRecord str =
         ps == cd.currentPatchSet()
             ? cd.submitTypeRecord()
-            : new SubmitRuleEvaluator(accountCache, accounts, cd).setPatchSet(ps).getSubmitType();
+            : new SubmitRuleEvaluator(accountCache, accounts, emails, cd)
+                .setPatchSet(ps)
+                .getSubmitType();
     if (!str.isOk()) {
       logErrorAndThrow("Failed to get submit type for " + cd.getId() + ": " + str.errorMessage);
     }
@@ -212,7 +226,7 @@
   }
 
   private ChangeSet completeChangeSetWithoutTopic(ReviewDb db, ChangeSet changes, CurrentUser user)
-      throws IOException, OrmException {
+      throws IOException, OrmException, PermissionBackendException {
     Collection<ChangeData> visibleChanges = new ArrayList<>();
     Collection<ChangeData> nonVisibleChanges = new ArrayList<>();
 
@@ -231,7 +245,7 @@
                 + " at ChangeData creation time");
 
         boolean visible = changes.ids().contains(cd.getId());
-        if (visible && !cd.changeControl().isVisible(db, cd)) {
+        if (visible && !canRead(db, user, cd)) {
           // We thought the change was visible, but it isn't.
           // This can happen if the ACL changes during the
           // completeChangeSet computation, for example.
@@ -357,7 +371,7 @@
       CurrentUser user,
       Set<String> topicsSeen,
       Set<String> visibleTopicsSeen)
-      throws OrmException {
+      throws OrmException, PermissionBackendException {
     List<ChangeData> visibleChanges = new ArrayList<>();
     List<ChangeData> nonVisibleChanges = new ArrayList<>();
 
@@ -370,7 +384,7 @@
       for (ChangeData topicCd : query().byTopicOpen(topic)) {
         try {
           topicCd.changeControl(user);
-          if (topicCd.changeControl().isVisible(db, topicCd)) {
+          if (canRead(db, user, topicCd)) {
             visibleChanges.add(topicCd);
           } else {
             nonVisibleChanges.add(topicCd);
@@ -402,7 +416,8 @@
   }
 
   private ChangeSet completeChangeSetIncludingTopics(
-      ReviewDb db, ChangeSet changes, CurrentUser user) throws IOException, OrmException {
+      ReviewDb db, ChangeSet changes, CurrentUser user)
+      throws IOException, OrmException, PermissionBackendException {
     Set<String> topicsSeen = new HashSet<>();
     Set<String> visibleTopicsSeen = new HashSet<>();
     int oldSeen;
@@ -443,4 +458,9 @@
     logError(msg);
     throw new OrmException(msg);
   }
+
+  private boolean canRead(ReviewDb db, CurrentUser user, ChangeData cd)
+      throws PermissionBackendException {
+    return permissionBackend.user(user).change(cd).database(db).test(ChangePermission.READ);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 8026cae..1cabc53 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -689,9 +689,9 @@
     }
 
     if (topics.size() == 1) {
-      return String.format("Merge changes from topic '%s'", Iterables.getFirst(topics, null));
+      return String.format("Merge changes from topic \"%s\"", Iterables.getFirst(topics, null));
     } else if (topics.size() > 1) {
-      return String.format("Merge changes from topics '%s'", Joiner.on("', '").join(topics));
+      return String.format("Merge changes from topics \"%s\"", Joiner.on("\", \"").join(topics));
     } else {
       return String.format(
           "Merge changes %s%s",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java
index 59017e7..a44d21c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -198,7 +198,7 @@
         change, patchSet, ctx.getAccount(), patchSet.getRevision().get(), ctx.getWhen());
   }
 
-  private PatchSetInfo getPatchSetInfo(ChangeContext ctx) throws IOException {
+  private PatchSetInfo getPatchSetInfo(ChangeContext ctx) throws IOException, OrmException {
     RevWalk rw = ctx.getRevWalk();
     RevCommit commit =
         rw.parseCommit(ObjectId.fromString(checkNotNull(patchSet).getRevision().get()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index 93aa361..2afea25 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -30,8 +30,10 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -262,12 +264,16 @@
     try {
       Map<Change.Id, Branch.NameKey> visibleChanges = new HashMap<>();
       for (ChangeData cd : changeCache.getChangeData(db.get(), project.getNameKey())) {
-        if (projectCtl.controlForIndexedChange(cd.change()).isVisible(db.get(), cd)) {
+        if (permissionBackend
+            .user(user)
+            .indexedChange(cd, changeNotesFactory.createFromIndexedChange(cd.change()))
+            .database(db)
+            .test(ChangePermission.READ)) {
           visibleChanges.put(cd.getId(), cd.change().getDest());
         }
       }
       return visibleChanges;
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error(
           "Cannot load changes for project "
               + project.getName()
@@ -282,12 +288,12 @@
     try {
       Map<Change.Id, Branch.NameKey> visibleChanges = new HashMap<>();
       for (ChangeNotes cn : changeNotesFactory.scan(git, db.get(), project)) {
-        if (projectCtl.controlFor(cn).isVisible(db.get())) {
+        if (permissionBackend.user(user).change(cn).database(db).test(ChangePermission.READ)) {
           visibleChanges.put(cn.getChangeId(), cn.getChange().getDest());
         }
       }
       return visibleChanges;
-    } catch (IOException | OrmException e) {
+    } catch (IOException | OrmException | PermissionBackendException e) {
       log.error(
           "Cannot load changes for project " + project + ", assuming no changes are visible", e);
       return Collections.emptyMap();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AllRefsWatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AllRefsWatcher.java
new file mode 100644
index 0000000..4afaacd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AllRefsWatcher.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.receive;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import java.util.Map;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.transport.AdvertiseRefsHook;
+import org.eclipse.jgit.transport.BaseReceivePack;
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
+import org.eclipse.jgit.transport.UploadPack;
+
+/**
+ * Hook that scans all refs and holds onto the results reference.
+ *
+ * <p>This allows a caller who has an {@code AllRefsWatcher} instance to get the full map of refs in
+ * the repo, even if refs are filtered by a later hook or filter.
+ */
+class AllRefsWatcher implements AdvertiseRefsHook {
+  private Map<String, Ref> allRefs;
+
+  @Override
+  public void advertiseRefs(BaseReceivePack rp) throws ServiceMayNotContinueException {
+    allRefs = HookUtil.ensureAllRefsAdvertised(rp);
+  }
+
+  @Override
+  public void advertiseRefs(UploadPack uploadPack) {
+    throw new UnsupportedOperationException();
+  }
+
+  Map<String, Ref> getAllRefs() {
+    checkState(allRefs != null, "getAllRefs() only valid after refs were advertised");
+    return allRefs;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
new file mode 100644
index 0000000..71d8f63
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -0,0 +1,277 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.receive;
+
+import com.google.common.collect.SetMultimap;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.extensions.restapi.AuthException;
+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.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.HackPushNegotiateHook;
+import com.google.gerrit.server.git.MultiProgressMonitor;
+import com.google.gerrit.server.git.ProjectRunnable;
+import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.inject.Inject;
+import com.google.inject.PrivateModule;
+import com.google.inject.Provider;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import com.google.inject.name.Named;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.AdvertiseRefsHook;
+import org.eclipse.jgit.transport.AdvertiseRefsHookChain;
+import org.eclipse.jgit.transport.PreReceiveHook;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceiveCommand.Result;
+import org.eclipse.jgit.transport.ReceivePack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Hook that delegates to {@link ReceiveCommits} in a worker thread. */
+public class AsyncReceiveCommits implements PreReceiveHook {
+  private static final Logger log = LoggerFactory.getLogger(AsyncReceiveCommits.class);
+
+  private static final String TIMEOUT_NAME = "ReceiveCommitsOverallTimeout";
+
+  public interface Factory {
+    AsyncReceiveCommits create(
+        ProjectControl projectControl,
+        Repository repository,
+        @Nullable MessageSender messageSender,
+        SetMultimap<ReviewerStateInternal, Account.Id> extraReviewers);
+  }
+
+  public static class Module extends PrivateModule {
+    @Override
+    public void configure() {
+      install(new FactoryModuleBuilder().build(AsyncReceiveCommits.Factory.class));
+      expose(AsyncReceiveCommits.Factory.class);
+      // Don't expose the binding for ReceiveCommits.Factory. All callers should
+      // be using AsyncReceiveCommits.Factory instead.
+      install(new FactoryModuleBuilder().build(ReceiveCommits.Factory.class));
+    }
+
+    @Provides
+    @Singleton
+    @Named(TIMEOUT_NAME)
+    long getTimeoutMillis(@GerritServerConfig Config cfg) {
+      return ConfigUtil.getTimeUnit(
+          cfg, "receive", null, "timeout", TimeUnit.MINUTES.toMillis(4), TimeUnit.MILLISECONDS);
+    }
+  }
+
+  private class Worker implements ProjectRunnable {
+    final MultiProgressMonitor progress;
+
+    private final Collection<ReceiveCommand> commands;
+    private final ReceiveCommits rc;
+
+    private Worker(Collection<ReceiveCommand> commands) {
+      this.commands = commands;
+      rc = factory.create(projectControl, rp, allRefsWatcher, extraReviewers);
+      rc.init();
+      rc.setMessageSender(messageSender);
+      progress = new MultiProgressMonitor(new MessageSenderOutputStream(), "Processing changes");
+    }
+
+    @Override
+    public void run() {
+      rc.processCommands(commands, progress);
+    }
+
+    @Override
+    public Project.NameKey getProjectNameKey() {
+      return rc.getProject().getNameKey();
+    }
+
+    @Override
+    public String getRemoteName() {
+      return null;
+    }
+
+    @Override
+    public boolean hasCustomizedPrint() {
+      return true;
+    }
+
+    @Override
+    public String toString() {
+      return "receive-commits";
+    }
+
+    void sendMessages() {
+      rc.sendMessages();
+    }
+
+    private class MessageSenderOutputStream extends OutputStream {
+      @Override
+      public void write(int b) {
+        rc.getMessageSender().sendBytes(new byte[] {(byte) b});
+      }
+
+      @Override
+      public void write(byte[] what, int off, int len) {
+        rc.getMessageSender().sendBytes(what, off, len);
+      }
+
+      @Override
+      public void write(byte[] what) {
+        rc.getMessageSender().sendBytes(what);
+      }
+
+      @Override
+      public void flush() {
+        rc.getMessageSender().flush();
+      }
+    }
+  }
+
+  private final ReceiveCommits.Factory factory;
+  private final ReceivePack rp;
+  private final ExecutorService executor;
+  private final RequestScopePropagator scopePropagator;
+  private final ReceiveConfig receiveConfig;
+  private final long timeoutMillis;
+  private final ProjectControl projectControl;
+  private final Repository repo;
+  private final MessageSender messageSender;
+  private final SetMultimap<ReviewerStateInternal, Account.Id> extraReviewers;
+  private final AllRefsWatcher allRefsWatcher;
+
+  @Inject
+  AsyncReceiveCommits(
+      ReceiveCommits.Factory factory,
+      PermissionBackend permissionBackend,
+      VisibleRefFilter.Factory refFilterFactory,
+      Provider<InternalChangeQuery> queryProvider,
+      @ReceiveCommitsExecutor ExecutorService executor,
+      RequestScopePropagator scopePropagator,
+      ReceiveConfig receiveConfig,
+      TransferConfig transferConfig,
+      Provider<LazyPostReceiveHookChain> lazyPostReceive,
+      @Named(TIMEOUT_NAME) long timeoutMillis,
+      @Assisted ProjectControl projectControl,
+      @Assisted Repository repo,
+      @Assisted @Nullable MessageSender messageSender,
+      @Assisted SetMultimap<ReviewerStateInternal, Account.Id> extraReviewers)
+      throws PermissionBackendException {
+    this.factory = factory;
+    this.executor = executor;
+    this.scopePropagator = scopePropagator;
+    this.receiveConfig = receiveConfig;
+    this.timeoutMillis = timeoutMillis;
+    this.projectControl = projectControl;
+    this.repo = repo;
+    this.messageSender = messageSender;
+    this.extraReviewers = extraReviewers;
+
+    IdentifiedUser user = projectControl.getUser().asIdentifiedUser();
+    ProjectState state = projectControl.getProjectState();
+    Project.NameKey projectName = projectControl.getProject().getNameKey();
+    rp = new ReceivePack(repo);
+    rp.setAllowCreates(true);
+    rp.setAllowDeletes(true);
+    rp.setAllowNonFastForwards(true);
+    rp.setRefLogIdent(user.newRefLogIdent());
+    rp.setTimeout(transferConfig.getTimeout());
+    rp.setMaxObjectSizeLimit(transferConfig.getEffectiveMaxObjectSizeLimit(state));
+    rp.setCheckReceivedObjects(state.getConfig().getCheckReceivedObjects());
+    rp.setRefFilter(new ReceiveRefFilter());
+    rp.setAllowPushOptions(true);
+    rp.setPreReceiveHook(this);
+    rp.setPostReceiveHook(lazyPostReceive.get());
+
+    // If the user lacks READ permission, some references may be filtered and hidden from view.
+    // Check objects mentioned inside the incoming pack file are reachable from visible refs.
+    try {
+      permissionBackend.user(user).project(projectName).check(ProjectPermission.READ);
+    } catch (AuthException e) {
+      rp.setCheckReferencedObjectsAreReachable(receiveConfig.checkReferencedObjectsAreReachable);
+    }
+
+    List<AdvertiseRefsHook> advHooks = new ArrayList<>(4);
+    allRefsWatcher = new AllRefsWatcher();
+    advHooks.add(allRefsWatcher);
+    advHooks.add(refFilterFactory.create(state, repo).setShowMetadata(false));
+    advHooks.add(new ReceiveCommitsAdvertiseRefsHook(queryProvider, projectName));
+    advHooks.add(new HackPushNegotiateHook());
+    rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
+  }
+
+  /** Determine if the user can upload commits. */
+  public Capable canUpload() {
+    Capable result = projectControl.canPushToAtLeastOneRef();
+    if (result != Capable.OK) {
+      return result;
+    }
+    if (receiveConfig.checkMagicRefs) {
+      result = MagicBranch.checkMagicBranchRefs(repo, projectControl.getProject());
+    }
+    return result;
+  }
+
+  @Override
+  public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
+    Worker w = new Worker(commands);
+    try {
+      w.progress.waitFor(
+          executor.submit(scopePropagator.wrap(w)), timeoutMillis, TimeUnit.MILLISECONDS);
+    } catch (ExecutionException e) {
+      log.warn(
+          String.format(
+              "Error in ReceiveCommits while processing changes for project %s",
+              projectControl.getProject().getName()),
+          e);
+      rp.sendError("internal error while processing changes");
+      // ReceiveCommits has tried its best to catch errors, so anything at this
+      // point is very bad.
+      for (ReceiveCommand c : commands) {
+        if (c.getResult() == Result.NOT_ATTEMPTED) {
+          c.setResult(Result.REJECTED_OTHER_REASON, "internal error");
+        }
+      }
+    } finally {
+      w.sendMessages();
+    }
+  }
+
+  public ReceivePack getReceivePack() {
+    return rp;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeProgressOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ChangeProgressOp.java
similarity index 95%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeProgressOp.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ChangeProgressOp.java
index 1a39a76..6774465 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeProgressOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ChangeProgressOp.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.git.receive;
 
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/HookUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/HookUtil.java
new file mode 100644
index 0000000..90b220a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/HookUtil.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.receive;
+
+import java.io.IOException;
+import java.util.Map;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.transport.BaseReceivePack;
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
+
+/** Static utilities for writing {@link ReceiveCommits}-related hooks. */
+class HookUtil {
+  /**
+   * Scan and advertise all refs in the repo if refs have not already been advertised; otherwise,
+   * just return the advertised map.
+   *
+   * @param rp receive-pack handler.
+   * @return map of refs that were advertised.
+   * @throws ServiceMayNotContinueException if a problem occurred.
+   */
+  static Map<String, Ref> ensureAllRefsAdvertised(BaseReceivePack rp)
+      throws ServiceMayNotContinueException {
+    Map<String, Ref> refs = rp.getAdvertisedRefs();
+    if (refs != null) {
+      return refs;
+    }
+    try {
+      refs = rp.getRepository().getRefDatabase().getRefs(RefDatabase.ALL);
+    } catch (ServiceMayNotContinueException e) {
+      throw e;
+    } catch (IOException e) {
+      ServiceMayNotContinueException ex = new ServiceMayNotContinueException();
+      ex.initCause(e);
+      throw ex;
+    }
+    rp.setAdvertisedRefs(refs, rp.getAdvertisedObjects());
+    return refs;
+  }
+
+  private HookUtil() {}
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LazyPostReceiveHookChain.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
similarity index 96%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/LazyPostReceiveHookChain.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
index bc12e02..7adb21b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LazyPostReceiveHookChain.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.git.receive;
 
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.inject.Inject;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/MessageSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/MessageSender.java
new file mode 100644
index 0000000..a338021
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/MessageSender.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.receive;
+
+/**
+ * Interface used by {@link ReceiveCommits} for send messages over the wire during {@code
+ * receive-pack}.
+ */
+public interface MessageSender {
+  void sendMessage(String what);
+
+  void sendError(String what);
+
+  void sendBytes(byte[] what);
+
+  void sendBytes(byte[] what, int off, int len);
+
+  void flush();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
similarity index 92%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 42fb1b3..7f909d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.git.receive;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkNotNull;
@@ -21,12 +21,15 @@
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
 import static com.google.gerrit.server.change.HashtagsUtil.cleanupHashtag;
 import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
+import static com.google.gerrit.server.git.receive.ReceiveConstants.COMMAND_REJECTION_MESSAGE_FOOTER;
+import static com.google.gerrit.server.git.receive.ReceiveConstants.ONLY_OWNER_CAN_MODIFY_WIP;
+import static com.google.gerrit.server.git.receive.ReceiveConstants.SAME_CHANGE_ID_IN_MULTIPLE_CHANGES;
+import static com.google.gerrit.server.git.validators.CommitValidators.NEW_PATCHSET_PATTERN;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
 import static java.util.Comparator.comparingInt;
 import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
-import static org.eclipse.jgit.lib.RefDatabase.ALL;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
@@ -40,17 +43,18 @@
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.collect.SortedSetMultimap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
@@ -94,7 +98,19 @@
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
 import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.BanCommit;
+import com.google.gerrit.server.git.GroupCollector;
+import com.google.gerrit.server.git.MergeOp;
+import com.google.gerrit.server.git.MergeOpRepoManager;
+import com.google.gerrit.server.git.MergedByPushOp;
+import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.ReceivePackInitializer;
+import com.google.gerrit.server.git.SubmoduleException;
+import com.google.gerrit.server.git.SubmoduleOp;
+import com.google.gerrit.server.git.TagCache;
+import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.CommitValidators;
@@ -105,12 +121,12 @@
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -153,7 +169,6 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -171,40 +186,19 @@
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.revwalk.filter.RevFilter;
-import org.eclipse.jgit.transport.AdvertiseRefsHook;
-import org.eclipse.jgit.transport.AdvertiseRefsHookChain;
-import org.eclipse.jgit.transport.BaseReceivePack;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.ReceiveCommand.Result;
 import org.eclipse.jgit.transport.ReceivePack;
-import org.eclipse.jgit.transport.RefFilter;
-import org.eclipse.jgit.transport.ServiceMayNotContinueException;
-import org.eclipse.jgit.transport.UploadPack;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /** Receives change upload using the Git receive-pack protocol. */
-public class ReceiveCommits {
+class ReceiveCommits {
   private static final Logger log = LoggerFactory.getLogger(ReceiveCommits.class);
   private static final String BYPASS_REVIEW = "bypass-review";
 
-  public static final Pattern NEW_PATCHSET =
-      Pattern.compile("^" + REFS_CHANGES + "(?:[0-9][0-9]/)?([1-9][0-9]*)(?:/new)?$");
-
-  private static final String COMMAND_REJECTION_MESSAGE_FOOTER =
-      "Please read the documentation and contact an administrator\n"
-          + "if you feel the configuration is incorrect";
-
-  private static final String SAME_CHANGE_ID_IN_MULTIPLE_CHANGES =
-      "same Change-Id in multiple changes.\n"
-          + "Squash the commits with the same Change-Id or "
-          + "ensure Change-Ids are unique for each commit";
-
-  public static final String ONLY_OWNER_CAN_MODIFY_WIP =
-      "only change owner can modify Work-in-Progress";
-
   private enum Error {
     CONFIG_UPDATE(
         "You are not allowed to perform this operation.\n"
@@ -226,25 +220,17 @@
       this.value = value;
     }
 
-    public String get() {
+    String get() {
       return value;
     }
   }
 
   interface Factory {
-    ReceiveCommits create(ProjectControl projectControl, Repository repository);
-  }
-
-  public interface MessageSender {
-    void sendMessage(String what);
-
-    void sendError(String what);
-
-    void sendBytes(byte[] what);
-
-    void sendBytes(byte[] what, int off, int len);
-
-    void flush();
+    ReceiveCommits create(
+        ProjectControl projectControl,
+        ReceivePack receivePack,
+        AllRefsWatcher allRefsWatcher,
+        SetMultimap<ReviewerStateInternal, Account.Id> extraReviewers);
   }
 
   private class ReceivePackMessageSender implements MessageSender {
@@ -296,64 +282,66 @@
         }
       };
 
-  private Set<Account.Id> reviewersFromCommandLine = Sets.newLinkedHashSet();
-  private Set<Account.Id> ccFromCommandLine = Sets.newLinkedHashSet();
+  // ReceiveCommits has a lot of fields, sorry. Here and in the constructor they are split up
+  // somewhat, and kept sorted lexicographically within sections, except where later assignments
+  // depend on previous ones.
 
-  private final IdentifiedUser user;
-  private final ReviewDb db;
-  private final Sequences seq;
-  private final Provider<InternalChangeQuery> queryProvider;
-  private final ChangeNotes.Factory notesFactory;
-  private final AccountsUpdate.Server accountsUpdate;
+  // Injected fields.
   private final AccountResolver accountResolver;
-  private final PermissionBackend permissionBackend;
-  private final PermissionBackend.ForProject permissions;
-  private final CmdLineParser.Factory optionParserFactory;
-  private final PatchSetInfoFactory patchSetInfoFactory;
-  private final PatchSetUtil psUtil;
-  private final ProjectCache projectCache;
-  private final String canonicalWebUrl;
-  private final CommitValidators.Factory commitValidatorsFactory;
-  private final RefOperationValidators.Factory refValidatorsFactory;
-  private final TagCache tagCache;
-  private final ChangeInserter.Factory changeInserterFactory;
-  private final RequestScopePropagator requestScopePropagator;
-  private final SshInfo sshInfo;
+  private final AccountsUpdate.Server accountsUpdate;
   private final AllProjectsName allProjectsName;
-  private final ReceiveConfig receiveConfig;
-  private final DynamicSet<ReceivePackInitializer> initializers;
   private final BatchUpdate.Factory batchUpdateFactory;
-  private final SetHashtagsOp.Factory hashtagsFactory;
-  private final ReplaceOp.Factory replaceOpFactory;
-  private final MergedByPushOp.Factory mergedByPushOpFactory;
-
-  private final ProjectControl projectControl;
-  private final Project project;
-  private final LabelTypes labelTypes;
-  private final Repository repo;
-  private final ReceivePack rp;
-  private final NoteMap rejectCommits;
-  private final RequestId receiveId;
-  private MagicBranchInput magicBranch;
-  private boolean newChangeForAllNotInTarget;
-  private final ListMultimap<String, String> pushOptions = LinkedListMultimap.create();
-
-  private List<CreateRequest> newChanges = Collections.emptyList();
-  private final Map<Change.Id, ReplaceRequest> replaceByChange = new LinkedHashMap<>();
-  private final List<UpdateGroupsRequest> updateGroups = new ArrayList<>();
-  private final Set<ObjectId> validCommits = new HashSet<>();
-
-  private ListMultimap<Change.Id, Ref> refsByChange;
-  private ListMultimap<ObjectId, Ref> refsById;
-  private Map<String, Ref> allRefs;
-
-  private final SubmoduleOp.Factory subOpFactory;
-  private final Provider<MergeOp> mergeOpProvider;
-  private final Provider<MergeOpRepoManager> ormProvider;
-  private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
-  private final NotesMigration notesMigration;
   private final ChangeEditUtil editUtil;
   private final ChangeIndexer indexer;
+  private final ChangeInserter.Factory changeInserterFactory;
+  private final ChangeNotes.Factory notesFactory;
+  private final CmdLineParser.Factory optionParserFactory;
+  private final CommitValidators.Factory commitValidatorsFactory;
+  private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
+  private final DynamicSet<ReceivePackInitializer> initializers;
+  private final IdentifiedUser user;
+  private final MergedByPushOp.Factory mergedByPushOpFactory;
+  private final NotesMigration notesMigration;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final PatchSetUtil psUtil;
+  private final PermissionBackend permissionBackend;
+  private final ProjectCache projectCache;
+  private final Provider<InternalChangeQuery> queryProvider;
+  private final Provider<MergeOp> mergeOpProvider;
+  private final Provider<MergeOpRepoManager> ormProvider;
+  private final ReceiveConfig receiveConfig;
+  private final RefOperationValidators.Factory refValidatorsFactory;
+  private final ReplaceOp.Factory replaceOpFactory;
+  private final RequestScopePropagator requestScopePropagator;
+  private final ReviewDb db;
+  private final Sequences seq;
+  private final SetHashtagsOp.Factory hashtagsFactory;
+  private final SshInfo sshInfo;
+  private final String canonicalWebUrl;
+  private final SubmoduleOp.Factory subOpFactory;
+  private final TagCache tagCache;
+
+  // Assisted injected fields.
+  private final AllRefsWatcher allRefsWatcher;
+  private final ImmutableSetMultimap<ReviewerStateInternal, Account.Id> extraReviewers;
+  private final ProjectControl projectControl;
+  private final ReceivePack rp;
+
+  // Immutable fields derived from constructor arguments.
+  private final LabelTypes labelTypes;
+  private final NoteMap rejectCommits;
+  private final PermissionBackend.ForProject permissions;
+  private final Project project;
+  private final Repository repo;
+  private final RequestId receiveId;
+
+  // Collections populated during processing.
+  private final List<UpdateGroupsRequest> updateGroups;
+  private final List<ValidationMessage> messages;
+  private final ListMultimap<Error, String> errors;
+  private final ListMultimap<String, String> pushOptions;
+  private final Map<Change.Id, ReplaceRequest> replaceByChange;
+  private final Set<ObjectId> validCommits;
 
   /**
    * Actual commands to be executed, as opposed to the mix of actual and magic commands that were
@@ -362,10 +350,18 @@
    * <p>Excludes commands executed implicitly as part of other {@link BatchUpdateOp}s, such as
    * creating patch set refs.
    */
-  private final List<ReceiveCommand> actualCommands = new ArrayList<>();
+  private final List<ReceiveCommand> actualCommands;
 
-  private final List<ValidationMessage> messages = new ArrayList<>();
-  private ListMultimap<Error, String> errors = LinkedListMultimap.create();
+  // Collections lazily populated during processing.
+  private List<CreateRequest> newChanges;
+  private ListMultimap<Change.Id, Ref> refsByChange;
+  private ListMultimap<ObjectId, Ref> refsById;
+
+  // Other settings populated during processing.
+  private MagicBranchInput magicBranch;
+  private boolean newChangeForAllNotInTarget;
+
+  // Handles for outputting back over the wire to the end user.
   private Task newProgress;
   private Task replaceProgress;
   private Task closeProgress;
@@ -374,179 +370,120 @@
 
   @Inject
   ReceiveCommits(
-      ReviewDb db,
-      Sequences seq,
-      Provider<InternalChangeQuery> queryProvider,
-      ChangeNotes.Factory notesFactory,
-      AccountsUpdate.Server accountsUpdate,
-      AccountResolver accountResolver,
-      PermissionBackend permissionBackend,
-      CmdLineParser.Factory optionParserFactory,
-      PatchSetInfoFactory patchSetInfoFactory,
-      PatchSetUtil psUtil,
-      ProjectCache projectCache,
-      TagCache tagCache,
-      VisibleRefFilter.Factory refFilterFactory,
-      ChangeInserter.Factory changeInserterFactory,
-      CommitValidators.Factory commitValidatorsFactory,
-      RefOperationValidators.Factory refValidatorsFactory,
       @CanonicalWebUrl String canonicalWebUrl,
-      RequestScopePropagator requestScopePropagator,
-      SshInfo sshInfo,
+      AccountResolver accountResolver,
+      AccountsUpdate.Server accountsUpdate,
       AllProjectsName allProjectsName,
-      ReceiveConfig receiveConfig,
-      TransferConfig transferConfig,
-      DynamicSet<ReceivePackInitializer> initializers,
-      Provider<LazyPostReceiveHookChain> lazyPostReceive,
-      @Assisted ProjectControl projectControl,
-      @Assisted Repository repo,
-      SubmoduleOp.Factory subOpFactory,
-      Provider<MergeOp> mergeOpProvider,
-      Provider<MergeOpRepoManager> ormProvider,
-      DynamicMap<ProjectConfigEntry> pluginConfigEntries,
-      NotesMigration notesMigration,
+      BatchUpdate.Factory batchUpdateFactory,
       ChangeEditUtil editUtil,
       ChangeIndexer indexer,
-      BatchUpdate.Factory batchUpdateFactory,
-      SetHashtagsOp.Factory hashtagsFactory,
+      ChangeInserter.Factory changeInserterFactory,
+      ChangeNotes.Factory notesFactory,
+      CmdLineParser.Factory optionParserFactory,
+      CommitValidators.Factory commitValidatorsFactory,
+      DynamicMap<ProjectConfigEntry> pluginConfigEntries,
+      DynamicSet<ReceivePackInitializer> initializers,
+      MergedByPushOp.Factory mergedByPushOpFactory,
+      NotesMigration notesMigration,
+      PatchSetInfoFactory patchSetInfoFactory,
+      PatchSetUtil psUtil,
+      PermissionBackend permissionBackend,
+      ProjectCache projectCache,
+      Provider<InternalChangeQuery> queryProvider,
+      Provider<MergeOp> mergeOpProvider,
+      Provider<MergeOpRepoManager> ormProvider,
+      ReceiveConfig receiveConfig,
+      RefOperationValidators.Factory refValidatorsFactory,
       ReplaceOp.Factory replaceOpFactory,
-      MergedByPushOp.Factory mergedByPushOpFactory)
-      throws IOException, PermissionBackendException {
-    this.user = projectControl.getUser().asIdentifiedUser();
-    this.db = db;
-    this.seq = seq;
-    this.queryProvider = queryProvider;
-    this.notesFactory = notesFactory;
-    this.accountsUpdate = accountsUpdate;
+      RequestScopePropagator requestScopePropagator,
+      ReviewDb db,
+      Sequences seq,
+      SetHashtagsOp.Factory hashtagsFactory,
+      SshInfo sshInfo,
+      SubmoduleOp.Factory subOpFactory,
+      TagCache tagCache,
+      @Assisted ProjectControl projectControl,
+      @Assisted ReceivePack rp,
+      @Assisted AllRefsWatcher allRefsWatcher,
+      @Assisted SetMultimap<ReviewerStateInternal, Account.Id> extraReviewers)
+      throws IOException {
+    // Injected fields.
     this.accountResolver = accountResolver;
-    this.permissionBackend = permissionBackend;
-    this.optionParserFactory = optionParserFactory;
-    this.patchSetInfoFactory = patchSetInfoFactory;
-    this.psUtil = psUtil;
-    this.projectCache = projectCache;
+    this.accountsUpdate = accountsUpdate;
+    this.allProjectsName = allProjectsName;
+    this.batchUpdateFactory = batchUpdateFactory;
     this.canonicalWebUrl = canonicalWebUrl;
-    this.tagCache = tagCache;
     this.changeInserterFactory = changeInserterFactory;
     this.commitValidatorsFactory = commitValidatorsFactory;
-    this.refValidatorsFactory = refValidatorsFactory;
-    this.requestScopePropagator = requestScopePropagator;
-    this.sshInfo = sshInfo;
-    this.allProjectsName = allProjectsName;
-    this.receiveConfig = receiveConfig;
-    this.initializers = initializers;
-    this.batchUpdateFactory = batchUpdateFactory;
-    this.hashtagsFactory = hashtagsFactory;
-    this.replaceOpFactory = replaceOpFactory;
-    this.mergedByPushOpFactory = mergedByPushOpFactory;
-
-    this.projectControl = projectControl;
-    this.labelTypes = projectControl.getLabelTypes();
-    this.project = projectControl.getProject();
-    this.repo = repo;
-    this.rp = new ReceivePack(repo);
-    this.rejectCommits = BanCommit.loadRejectCommitsMap(repo, rp.getRevWalk());
-    this.receiveId = RequestId.forProject(project.getNameKey());
-
-    this.subOpFactory = subOpFactory;
-    this.mergeOpProvider = mergeOpProvider;
-    this.ormProvider = ormProvider;
-    this.pluginConfigEntries = pluginConfigEntries;
-    this.notesMigration = notesMigration;
-
+    this.db = db;
     this.editUtil = editUtil;
+    this.hashtagsFactory = hashtagsFactory;
     this.indexer = indexer;
+    this.initializers = initializers;
+    this.mergeOpProvider = mergeOpProvider;
+    this.mergedByPushOpFactory = mergedByPushOpFactory;
+    this.notesFactory = notesFactory;
+    this.notesMigration = notesMigration;
+    this.optionParserFactory = optionParserFactory;
+    this.ormProvider = ormProvider;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.permissionBackend = permissionBackend;
+    this.pluginConfigEntries = pluginConfigEntries;
+    this.projectCache = projectCache;
+    this.psUtil = psUtil;
+    this.queryProvider = queryProvider;
+    this.receiveConfig = receiveConfig;
+    this.refValidatorsFactory = refValidatorsFactory;
+    this.replaceOpFactory = replaceOpFactory;
+    this.requestScopePropagator = requestScopePropagator;
+    this.seq = seq;
+    this.sshInfo = sshInfo;
+    this.subOpFactory = subOpFactory;
+    this.tagCache = tagCache;
 
-    this.messageSender = new ReceivePackMessageSender();
+    // Assisted injected fields.
+    this.allRefsWatcher = allRefsWatcher;
+    this.extraReviewers = ImmutableSetMultimap.copyOf(extraReviewers);
+    this.projectControl = projectControl;
+    this.rp = rp;
 
-    ProjectState ps = projectControl.getProjectState();
-
-    this.newChangeForAllNotInTarget = ps.isCreateNewChangeForAllNotInTarget();
-    rp.setAllowCreates(true);
-    rp.setAllowDeletes(true);
-    rp.setAllowNonFastForwards(true);
-    rp.setRefLogIdent(user.newRefLogIdent());
-    rp.setTimeout(transferConfig.getTimeout());
-    rp.setMaxObjectSizeLimit(
-        transferConfig.getEffectiveMaxObjectSizeLimit(projectControl.getProjectState()));
-    rp.setCheckReceivedObjects(ps.getConfig().getCheckReceivedObjects());
-    rp.setRefFilter(
-        new RefFilter() {
-          @Override
-          public Map<String, Ref> filter(Map<String, Ref> refs) {
-            Map<String, Ref> filteredRefs = Maps.newHashMapWithExpectedSize(refs.size());
-            for (Map.Entry<String, Ref> e : refs.entrySet()) {
-              String name = e.getKey();
-              if (!name.startsWith(REFS_CHANGES)
-                  && !name.startsWith(RefNames.REFS_CACHE_AUTOMERGE)) {
-                filteredRefs.put(name, e.getValue());
-              }
-            }
-            return filteredRefs;
-          }
-        });
-
+    // Immutable fields derived from constructor arguments.
+    repo = rp.getRepository();
+    user = projectControl.getUser().asIdentifiedUser();
+    project = projectControl.getProject();
+    labelTypes = projectControl.getLabelTypes();
     permissions = permissionBackend.user(user).project(project.getNameKey());
-    // If the user lacks READ permission, some references may be filtered and hidden from view.
-    // Check objects mentioned inside the incoming pack file are reachable from visible refs.
-    try {
-      permissionBackend.user(user).project(project.getNameKey()).check(ProjectPermission.READ);
-    } catch (AuthException e) {
-      rp.setCheckReferencedObjectsAreReachable(receiveConfig.checkReferencedObjectsAreReachable);
-    }
+    receiveId = RequestId.forProject(project.getNameKey());
+    rejectCommits = BanCommit.loadRejectCommitsMap(rp.getRepository(), rp.getRevWalk());
 
-    rp.setAdvertiseRefsHook(
-        refFilterFactory.create(projectControl.getProjectState(), repo).setShowMetadata(false));
-    List<AdvertiseRefsHook> advHooks = new ArrayList<>(3);
-    advHooks.add(
-        new AdvertiseRefsHook() {
-          @Override
-          public void advertiseRefs(BaseReceivePack rp) throws ServiceMayNotContinueException {
-            allRefs = rp.getAdvertisedRefs();
-            if (allRefs == null) {
-              try {
-                allRefs = rp.getRepository().getRefDatabase().getRefs(ALL);
-              } catch (ServiceMayNotContinueException e) {
-                throw e;
-              } catch (IOException e) {
-                ServiceMayNotContinueException ex = new ServiceMayNotContinueException();
-                ex.initCause(e);
-                throw ex;
-              }
-            }
-            rp.setAdvertisedRefs(allRefs, rp.getAdvertisedObjects());
-          }
+    // Collections populated during processing.
+    actualCommands = new ArrayList<>();
+    errors = LinkedListMultimap.create();
+    messages = new ArrayList<>();
+    pushOptions = LinkedListMultimap.create();
+    replaceByChange = new LinkedHashMap<>();
+    updateGroups = new ArrayList<>();
+    validCommits = new HashSet<>();
 
-          @Override
-          public void advertiseRefs(UploadPack uploadPack) {}
-        });
-    advHooks.add(rp.getAdvertiseRefsHook());
-    advHooks.add(
-        new ReceiveCommitsAdvertiseRefsHook(
-            queryProvider, projectControl.getProject().getNameKey()));
-    advHooks.add(new HackPushNegotiateHook());
-    rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
-    rp.setPostReceiveHook(lazyPostReceive.get());
-    rp.setAllowPushOptions(true);
+    // Collections lazily populated during processing.
+    newChanges = Collections.emptyList();
+
+    // Other settings populated during processing.
+    newChangeForAllNotInTarget =
+        projectControl.getProjectState().isCreateNewChangeForAllNotInTarget();
+
+    // Handles for outputting back over the wire to the end user.
+    messageSender = new ReceivePackMessageSender();
   }
 
-  public void init() {
+  void init() {
     for (ReceivePackInitializer i : initializers) {
       i.init(projectControl.getProject().getNameKey(), rp);
     }
   }
 
-  /** Add reviewers for new (or updated) changes. */
-  public void addReviewers(Collection<Account.Id> who) {
-    reviewersFromCommandLine.addAll(who);
-  }
-
-  /** Add reviewers for new (or updated) changes. */
-  public void addExtraCC(Collection<Account.Id> who) {
-    ccFromCommandLine.addAll(who);
-  }
-
   /** Set a message sender for this operation. */
-  public void setMessageSender(MessageSender ms) {
+  void setMessageSender(MessageSender ms) {
     messageSender = ms != null ? ms : new ReceivePackMessageSender();
   }
 
@@ -561,23 +498,6 @@
     return project;
   }
 
-  /** @return the ReceivePack instance to speak the native Git protocol. */
-  public ReceivePack getReceivePack() {
-    return rp;
-  }
-
-  /** Determine if the user can upload commits. */
-  public Capable canUpload() {
-    Capable result = projectControl.canPushToAtLeastOneRef();
-    if (result != Capable.OK) {
-      return result;
-    }
-    if (receiveConfig.checkMagicRefs) {
-      result = MagicBranch.checkMagicBranchRefs(repo, project);
-    }
-    return result;
-  }
-
   private void addMessage(String message) {
     messages.add(new CommitValidationMessage(message, false));
   }
@@ -819,7 +739,8 @@
           | OrmException
           | UpdateException
           | IOException
-          | ConfigInvalidException e) {
+          | ConfigInvalidException
+          | PermissionBackendException e) {
         logError("Error submitting changes to " + project.getName(), e);
         reject(magicBranchCmd, "error during submit");
       }
@@ -897,7 +818,7 @@
             };
       }
 
-      Matcher m = NEW_PATCHSET.matcher(cmd.getRefName());
+      Matcher m = NEW_PATCHSET_PATTERN.matcher(cmd.getRefName());
       if (m.matches()) {
         // The referenced change must exist and must still be open.
         //
@@ -1437,7 +1358,7 @@
       return ref.substring(0, split);
     }
 
-    public NotifyHandling getNotify() {
+    NotifyHandling getNotify() {
       if (notify != null) {
         return notify;
       }
@@ -1447,7 +1368,7 @@
       return NotifyHandling.ALL;
     }
 
-    public NotifyHandling getNotify(ChangeNotes notes) {
+    NotifyHandling getNotify(ChangeNotes notes) {
       if (notify != null) {
         return notify;
       }
@@ -1467,7 +1388,7 @@
    * @return an unmodifiable view of pushOptions.
    */
   @Nullable
-  public ListMultimap<String, String> getPushOptions() {
+  ListMultimap<String, String> getPushOptions() {
     return ImmutableListMultimap.copyOf(pushOptions);
   }
 
@@ -1480,8 +1401,8 @@
 
     logDebug("Found magic branch {}", cmd.getRefName());
     magicBranch = new MagicBranchInput(user, cmd, labelTypes, notesMigration);
-    magicBranch.reviewer.addAll(reviewersFromCommandLine);
-    magicBranch.cc.addAll(ccFromCommandLine);
+    magicBranch.reviewer.addAll(extraReviewers.get(ReviewerStateInternal.REVIEWER));
+    magicBranch.cc.addAll(extraReviewers.get(ReviewerStateInternal.CC));
 
     String ref;
     CmdLineParser clp = optionParserFactory.create(magicBranch);
@@ -1700,7 +1621,7 @@
   }
 
   private RevCommit readBranchTip(ReceiveCommand cmd, Branch.NameKey branch) throws IOException {
-    Ref r = allRefs.get(branch.get());
+    Ref r = allRefs().get(branch.get());
     if (r == null) {
       reject(cmd, branch.get() + " not found");
       return null;
@@ -2073,7 +1994,7 @@
     for (RevCommit c : magicBranch.baseCommit) {
       rp.getRevWalk().markUninteresting(c);
     }
-    Ref targetRef = allRefs.get(magicBranch.ctl.getRefName());
+    Ref targetRef = allRefs().get(magicBranch.ctl.getRefName());
     if (targetRef != null) {
       logDebug(
           "Marking target ref {} ({}) uninteresting",
@@ -2085,7 +2006,7 @@
 
   private void rejectImplicitMerges(Set<RevCommit> mergedParents) throws IOException {
     if (!mergedParents.isEmpty()) {
-      Ref targetRef = allRefs.get(magicBranch.ctl.getRefName());
+      Ref targetRef = allRefs().get(magicBranch.ctl.getRefName());
       if (targetRef != null) {
         RevWalk rw = rp.getRevWalk();
         RevCommit tip = rw.parseCommit(targetRef.getObjectId());
@@ -2119,7 +2040,7 @@
 
   private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) {
     int i = 0;
-    for (Ref ref : allRefs.values()) {
+    for (Ref ref : allRefs().values()) {
       if ((ref.getName().startsWith(R_HEADS) || ref.getName().equals(forRef))
           && ref.getObjectId() != null) {
         try {
@@ -2264,7 +2185,8 @@
   }
 
   private void submit(Collection<CreateRequest> create, Collection<ReplaceRequest> replace)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
+      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     Map<ObjectId, Change> bySha = Maps.newHashMapWithExpectedSize(create.size() + replace.size());
     for (CreateRequest r : create) {
       checkNotNull(r.change, "cannot submit new change %s; op may not have run", r.changeId);
@@ -2544,10 +2466,10 @@
               RefNames.refsEdit(user.getAccountId(), notes.getChangeId(), psId));
     }
 
-    private void newPatchSet() throws IOException {
+    private void newPatchSet() throws IOException, OrmException {
       RevCommit newCommit = rp.getRevWalk().parseCommit(newCommitId);
       psId =
-          ChangeUtil.nextPatchSetIdFromAllRefsMap(allRefs, notes.getChange().currentPatchSetId());
+          ChangeUtil.nextPatchSetIdFromAllRefsMap(allRefs(), notes.getChange().currentPatchSetId());
       info = patchSetInfoFactory.get(rp.getRevWalk(), newCommit, psId);
       cmd = new ReceiveCommand(ObjectId.zeroId(), newCommitId, psId.toRefName());
     }
@@ -2679,10 +2601,10 @@
       int estRefsPerChange = 4;
       refsById = MultimapBuilder.hashKeys().arrayListValues().build();
       refsByChange =
-          MultimapBuilder.hashKeys(allRefs.size() / estRefsPerChange)
+          MultimapBuilder.hashKeys(allRefs().size() / estRefsPerChange)
               .arrayListValues(estRefsPerChange)
               .build();
-      for (Ref ref : allRefs.values()) {
+      for (Ref ref : allRefs().values()) {
         ObjectId obj = ref.getObjectId();
         if (obj != null) {
           PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
@@ -2760,7 +2682,7 @@
     PermissionBackend.ForRef perm = permissions.ref(ctl.getRefName());
     if (!RefNames.REFS_CONFIG.equals(cmd.getRefName())
         && !(MagicBranch.isMagicBranch(cmd.getRefName())
-            || NEW_PATCHSET.matcher(cmd.getRefName()).matches())
+            || NEW_PATCHSET_PATTERN.matcher(cmd.getRefName()).matches())
         && pushOptions.containsKey(BYPASS_REVIEW)) {
       try {
         perm.check(RefPermission.BYPASS_REVIEW);
@@ -2965,6 +2887,10 @@
     return r;
   }
 
+  private Map<String, Ref> allRefs() {
+    return allRefsWatcher.getAllRefs();
+  }
+
   private void reject(@Nullable ReceiveCommand cmd, String why) {
     if (cmd != null) {
       cmd.setResult(REJECTED_OTHER_REASON, why);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
similarity index 89%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
index 2316782..3645392 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
@@ -12,9 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
-
-import static org.eclipse.jgit.lib.RefDatabase.ALL;
+package com.google.gerrit.server.git.receive;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
@@ -30,7 +28,6 @@
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
-import java.io.IOException;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
@@ -72,19 +69,7 @@
 
   @Override
   public void advertiseRefs(BaseReceivePack rp) throws ServiceMayNotContinueException {
-    Map<String, Ref> oldRefs = rp.getAdvertisedRefs();
-    if (oldRefs == null) {
-      try {
-        oldRefs = rp.getRepository().getRefDatabase().getRefs(ALL);
-      } catch (ServiceMayNotContinueException e) {
-        throw e;
-      } catch (IOException e) {
-        ServiceMayNotContinueException ex = new ServiceMayNotContinueException();
-        ex.initCause(e);
-        throw ex;
-      }
-    }
-    Result r = advertiseRefs(oldRefs);
+    Result r = advertiseRefs(HookUtil.ensureAllRefsAdvertised(rp));
     rp.setAdvertisedRefs(r.allRefs(), r.additionalHaves());
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutor.java
similarity index 95%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutor.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutor.java
index bc69a08..ee83a2c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutor.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.git.receive;
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutorModule.java
similarity index 87%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutorModule.java
index 90bdd52..4eb760d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutorModule.java
@@ -12,12 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.git.receive;
 
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.SendEmailExecutor;
+import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.update.ChangeUpdateExecutor;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
@@ -28,7 +30,12 @@
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
 
-/** Module providing the {@link ReceiveCommitsExecutor}. */
+/**
+ * Module providing the {@link ReceiveCommitsExecutor}.
+ *
+ * <p>Unlike {@link ReceiveCommitsModule}, this module is intended to be installed only in top-level
+ * injectors like in {@code Daemon}, not in the {@code sysInjector}.
+ */
 public class ReceiveCommitsExecutorModule extends AbstractModule {
   @Override
   protected void configure() {}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommitsModule.java
similarity index 61%
rename from gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommitsModule.java
index e73d82b..a973460 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommitsModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// 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.
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.account;
+package com.google.gerrit.server.git.receive;
 
-import com.google.gerrit.reviewdb.client.Account;
-import java.util.Set;
+import com.google.gerrit.extensions.config.FactoryModule;
 
-/** Translates an email address to a set of matching accounts. */
-public interface AccountByEmailCache {
-  Set<Account.Id> get(String email);
-
-  void evict(String email);
+public class ReceiveCommitsModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    bind(ReceiveConfig.class);
+    factory(ReplaceOp.Factory.class);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
similarity index 97%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
index a3f2a31..39b6d8b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.git.receive;
 
 import static com.google.gerrit.common.data.GlobalCapability.BATCH_CHANGES_LIMIT;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConstants.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
new file mode 100644
index 0000000..99742f3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.receive;
+
+import com.google.common.annotations.VisibleForTesting;
+
+public final class ReceiveConstants {
+  @VisibleForTesting
+  public static final String ONLY_OWNER_CAN_MODIFY_WIP =
+      "only change owner can modify Work-in-Progress";
+
+  static final String COMMAND_REJECTION_MESSAGE_FOOTER =
+      "Please read the documentation and contact an administrator\n"
+          + "if you feel the configuration is incorrect";
+
+  static final String SAME_CHANGE_ID_IN_MULTIPLE_CHANGES =
+      "same Change-Id in multiple changes.\n"
+          + "Squash the commits with the same Change-Id or "
+          + "ensure Change-Ids are unique for each commit";
+
+  private ReceiveConstants() {}
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveRefFilter.java
new file mode 100644
index 0000000..16cba53
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveRefFilter.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.receive;
+
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_CACHE_AUTOMERGE;
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
+
+import com.google.common.collect.Maps;
+import java.util.Map;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.transport.RefFilter;
+
+class ReceiveRefFilter implements RefFilter {
+  @Override
+  public Map<String, Ref> filter(Map<String, Ref> refs) {
+    Map<String, Ref> filteredRefs = Maps.newHashMapWithExpectedSize(refs.size());
+    for (Map.Entry<String, Ref> e : refs.entrySet()) {
+      String name = e.getKey();
+      if (!name.startsWith(REFS_CHANGES) && !name.startsWith(REFS_CACHE_AUTOMERGE)) {
+        filteredRefs.put(name, e.getValue());
+      }
+    }
+    return filteredRefs;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
similarity index 98%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 8f2d121..a558c6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.git.receive;
 
 import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
@@ -45,7 +45,9 @@
 import com.google.gerrit.server.change.EmailReviewComments;
 import com.google.gerrit.server.extensions.events.CommentAdded;
 import com.google.gerrit.server.extensions.events.RevisionCreated;
-import com.google.gerrit.server.git.ReceiveCommits.MagicBranchInput;
+import com.google.gerrit.server.git.MergedByPushOp;
+import com.google.gerrit.server.git.SendEmailExecutor;
+import com.google.gerrit.server.git.receive.ReceiveCommits.MagicBranchInput;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
 import com.google.gerrit.server.notedb.ChangeNotes;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 49399ef..1b01c26 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -89,7 +89,8 @@
     }
 
     @Override
-    protected void updateRepoImpl(RepoContext ctx) throws IntegrationException, IOException {
+    protected void updateRepoImpl(RepoContext ctx)
+        throws IntegrationException, IOException, OrmException {
       // If there is only one parent, a cherry-pick can be done by taking the
       // delta relative to that one parent and redoing that on the current merge
       // tip.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
index 0d012e5..c76a59a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Streams;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -31,6 +31,7 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -38,6 +39,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -60,10 +62,13 @@
     }
   }
 
-  public static Iterable<ObjectId> getAlreadyAccepted(Repository repo) throws IOException {
-    return FluentIterable.from(repo.getRefDatabase().getRefs(Constants.R_HEADS).values())
-        .append(repo.getRefDatabase().getRefs(Constants.R_TAGS).values())
-        .transform(Ref::getObjectId);
+  public static Set<ObjectId> getAlreadyAccepted(Repository repo) throws IOException {
+    return Streams.concat(
+            repo.getRefDatabase().getRefs(Constants.R_HEADS).values().stream(),
+            repo.getRefDatabase().getRefs(Constants.R_TAGS).values().stream())
+        .map(Ref::getObjectId)
+        .filter(o -> o != null)
+        .collect(Collectors.toSet());
   }
 
   public static Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw) throws IOException {
@@ -76,6 +81,9 @@
       throws IOException {
     for (ObjectId id : ids) {
       RevObject obj = rw.parseAny(id);
+      if (obj instanceof RevTag) {
+        obj = rw.peel(obj);
+      }
       if (obj instanceof RevCommit) {
         out.add((RevCommit) obj);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 1d20264..4b96578 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.git.validators;
 
 import static com.google.gerrit.reviewdb.client.Change.CHANGE_ID_PATTERN;
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
-import static com.google.gerrit.server.git.ReceiveCommits.NEW_PATCHSET;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
@@ -78,6 +78,9 @@
 public class CommitValidators {
   private static final Logger log = LoggerFactory.getLogger(CommitValidators.class);
 
+  public static final Pattern NEW_PATCHSET_PATTERN =
+      Pattern.compile("^" + REFS_CHANGES + "(?:[0-9][0-9]/)?([1-9][0-9]*)(?:/new)?$");
+
   @Singleton
   public static class Factory {
     private final PersonIdent gerritIdent;
@@ -266,7 +269,7 @@
 
     private static boolean shouldValidateChangeId(CommitReceivedEvent event) {
       return MagicBranch.isMagicBranch(event.command.getRefName())
-          || NEW_PATCHSET.matcher(event.command.getRefName()).matches();
+          || NEW_PATCHSET_PATTERN.matcher(event.command.getRefName()).matches();
     }
 
     private CommitValidationMessage getMissingChangeIdErrorMsg(String errMsg, RevCommit c) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
index e1513b3..11c9d83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -200,6 +200,7 @@
     BitSet isIndexed = new BitSet(n);
     BitSet notIndexed = new BitSet(n);
     BitSet rewritten = new BitSet(n);
+    BitSet changeSource = new BitSet(n);
     List<Predicate<ChangeData>> newChildren = Lists.newArrayListWithCapacity(n);
     for (int i = 0; i < n; i++) {
       Predicate<ChangeData> c = in.getChild(i);
@@ -211,6 +212,9 @@
         notIndexed.set(i);
         newChildren.add(c);
       } else {
+        if (nc instanceof ChangeDataSource) {
+          changeSource.set(i);
+        }
         rewritten.set(i);
         newChildren.add(nc);
       }
@@ -221,7 +225,11 @@
     } else if (notIndexed.cardinality() == n) {
       return null; // Can't rewrite any children, so cannot rewrite in.
     } else if (rewritten.cardinality() == n) {
-      return in.copy(newChildren); // All children were rewritten.
+      // All children were rewritten.
+      if (changeSource.cardinality() == n) {
+        return copy(in, newChildren);
+      }
+      return in.copy(newChildren);
     }
     return partitionChildren(in, newChildren, isIndexed, index, opts);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
index 5dae659..a752324 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
 import java.time.format.DateTimeFormatter;
 import java.util.Collections;
 import java.util.HashSet;
@@ -42,7 +43,7 @@
       AccountResolver accountResolver,
       boolean draftPatchSet,
       List<FooterLine> footerLines)
-      throws OrmException {
+      throws OrmException, IOException {
     MailRecipients recipients = new MailRecipients();
     if (!draftPatchSet) {
       for (FooterLine footerLine : footerLines) {
@@ -70,7 +71,7 @@
 
   private static Account.Id toAccountId(
       ReviewDb db, AccountResolver accountResolver, String nameOrEmail)
-      throws OrmException, NoSuchAccountException {
+      throws OrmException, NoSuchAccountException, IOException {
     Account a = accountResolver.findByNameOrEmail(db, nameOrEmail);
     if (a == null) {
       throw new NoSuchAccountException("\"" + nameOrEmail + "\" is not registered");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 24bae37..68bcda4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -36,8 +36,8 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.change.EmailReviewComments;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.extensions.events.CommentAdded;
@@ -58,6 +58,7 @@
 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.HashMap;
@@ -73,7 +74,7 @@
 public class MailProcessor {
   private static final Logger log = LoggerFactory.getLogger(MailProcessor.class);
 
-  private final AccountByEmailCache accountByEmailCache;
+  private final Emails emails;
   private final RetryHelper retryHelper;
   private final ChangeMessagesUtil changeMessagesUtil;
   private final CommentsUtil commentsUtil;
@@ -90,7 +91,7 @@
 
   @Inject
   public MailProcessor(
-      AccountByEmailCache accountByEmailCache,
+      Emails emails,
       RetryHelper retryHelper,
       ChangeMessagesUtil changeMessagesUtil,
       CommentsUtil commentsUtil,
@@ -104,7 +105,7 @@
       CommentAdded commentAdded,
       AccountCache accountCache,
       @CanonicalWebUrl Provider<String> canonicalUrl) {
-    this.accountByEmailCache = accountByEmailCache;
+    this.emails = emails;
     this.retryHelper = retryHelper;
     this.changeMessagesUtil = changeMessagesUtil;
     this.commentsUtil = commentsUtil;
@@ -134,7 +135,7 @@
   }
 
   private void processImpl(BatchUpdate.Factory buf, MailMessage message)
-      throws OrmException, UpdateException, RestApiException {
+      throws OrmException, UpdateException, RestApiException, IOException {
     for (DynamicMap.Entry<MailFilter> filter : mailFilters) {
       if (!filter.getProvider().get().shouldProcessMessage(message)) {
         log.warn(
@@ -154,15 +155,15 @@
       return;
     }
 
-    Set<Account.Id> accounts = accountByEmailCache.get(metadata.author);
-    if (accounts.size() != 1) {
+    Set<Account.Id> accountIds = emails.getAccountFor(metadata.author);
+    if (accountIds.size() != 1) {
       log.error(
           String.format(
               "Address %s could not be matched to a unique account. It was matched to %s. Will delete message.",
-              metadata.author, accounts));
+              metadata.author, accountIds));
       return;
     }
-    Account.Id account = accounts.iterator().next();
+    Account.Id account = accountIds.iterator().next();
     if (!accountCache.get(account).getAccount().isActive()) {
       log.warn(String.format("Mail: Account %s is inactive. Will delete message.", account));
       return;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 15a4b13..7bef0a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectState;
@@ -405,12 +406,12 @@
   }
 
   @Override
-  protected boolean isVisibleTo(Account.Id to) throws OrmException {
-    return projectState == null
-        || projectState
-            .controlFor(args.identifiedUserFactory.create(to))
-            .controlFor(args.db.get(), change)
-            .isVisible(args.db.get());
+  protected boolean isVisibleTo(Account.Id to) throws OrmException, PermissionBackendException {
+    return args.permissionBackend
+        .user(args.identifiedUserFactory.create(to))
+        .change(changeData)
+        .database(args.db.get())
+        .test(ChangePermission.READ);
   }
 
   /** Find all users who are authors of any part of this change. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
index ce68cca..7d83c5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -435,21 +435,43 @@
   }
 
   /**
-   * @return a shortened version of the given comment's message. Will be shortened to 75 characters
-   *     or the first line, whichever is shorter.
+   * @return a shortened version of the given comment's message. Will be shortened to 100 characters
+   *     or the first line, or following the last period within the first 100 characters, whichever
+   *     is shorter. If the message is shortened, an ellipsis is appended.
    */
-  private String getShortenedCommentMessage(Comment comment) {
-    String msg = comment.message.trim();
-    if (msg.length() > 75) {
-      msg = msg.substring(0, 75);
+  protected static String getShortenedCommentMessage(String message) {
+    int threshold = 100;
+    String fullMessage = message.trim();
+    String msg = fullMessage;
+
+    if (msg.length() > threshold) {
+      msg = msg.substring(0, threshold);
     }
+
     int lf = msg.indexOf('\n');
+    int period = msg.lastIndexOf('.');
+
     if (lf > 0) {
+      // Truncate if a line feed appears within the threshold.
       msg = msg.substring(0, lf);
+
+    } else if (period > 0) {
+      // Otherwise truncate if there is a period within the threshold.
+      msg = msg.substring(0, period + 1);
     }
+
+    // Append an ellipsis if the message has been truncated.
+    if (!msg.equals(fullMessage)) {
+      msg += " […]";
+    }
+
     return msg;
   }
 
+  protected static String getShortenedCommentMessage(Comment comment) {
+    return getShortenedCommentMessage(comment.message);
+  }
+
   /**
    * @return grouped inline comment data mapped to data structures that are suitable for passing
    *     into Soy.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 4e204ce..e569adf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.send.EmailHeader.AddressList;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gwtorm.server.OrmException;
@@ -479,7 +480,7 @@
         rcptTo.add(to);
         add(rt, toAddress(to), override);
       }
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Error reading database for account: " + to, e);
     }
   }
@@ -487,9 +488,10 @@
   /**
    * @param to account.
    * @throws OrmException
+   * @throws PermissionBackendException
    * @return whether this email is visible to the given account.
    */
-  protected boolean isVisibleTo(Account.Id to) throws OrmException {
+  protected boolean isVisibleTo(Account.Id to) throws OrmException, PermissionBackendException {
     return true;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index cd9c4c3..41bade6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.account.AccountByEmailCache;
+import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gwtorm.server.OrmException;
@@ -45,17 +45,17 @@
 public class PatchSetInfoFactory {
   private final GitRepositoryManager repoManager;
   private final PatchSetUtil psUtil;
-  private final AccountByEmailCache byEmailCache;
+  private final Emails emails;
 
   @Inject
-  public PatchSetInfoFactory(
-      GitRepositoryManager repoManager, PatchSetUtil psUtil, AccountByEmailCache byEmailCache) {
+  public PatchSetInfoFactory(GitRepositoryManager repoManager, PatchSetUtil psUtil, Emails emails) {
     this.repoManager = repoManager;
     this.psUtil = psUtil;
-    this.byEmailCache = byEmailCache;
+    this.emails = emails;
   }
 
-  public PatchSetInfo get(RevWalk rw, RevCommit src, PatchSet.Id psi) throws IOException {
+  public PatchSetInfo get(RevWalk rw, RevCommit src, PatchSet.Id psi)
+      throws IOException, OrmException {
     rw.parseBody(src);
     PatchSetInfo info = new PatchSetInfo(psi);
     info.setSubject(src.getShortMessage());
@@ -84,13 +84,13 @@
       PatchSetInfo info = get(rw, src, patchSet.getId());
       info.setParents(toParentInfos(src.getParents(), rw));
       return info;
-    } catch (IOException e) {
+    } catch (IOException | OrmException e) {
       throw new PatchSetInfoNotAvailableException(e);
     }
   }
 
   // TODO: The same method exists in EventFactory, find a common place for it
-  private UserIdentity toUserIdentity(PersonIdent who) {
+  private UserIdentity toUserIdentity(PersonIdent who) throws IOException, OrmException {
     final UserIdentity u = new UserIdentity();
     u.setName(who.getName());
     u.setEmail(who.getEmailAddress());
@@ -100,7 +100,7 @@
     // If only one account has access to this email address, select it
     // as the identity of the user.
     //
-    final Set<Account.Id> a = byEmailCache.get(u.getEmail());
+    Set<Account.Id> a = emails.getAccountFor(u.getEmail());
     if (a.size() == 1) {
       u.setAccount(a.iterator().next());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
index 24f5164..4c6e6753 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -119,7 +119,12 @@
     }
 
     @Override
-    public ForChange change(ChangeNotes cd) {
+    public ForChange change(ChangeNotes notes) {
+      return new FailedChange(message, cause);
+    }
+
+    @Override
+    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
       return new FailedChange(message, cause);
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 93db963..522eccb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -139,6 +139,15 @@
       return ref(notes.getChange().getDest()).change(notes);
     }
 
+    /**
+     * @return instance scoped for the change loaded from index, and its destination ref and
+     *     project. This method should only be used when database access is harmful and potentially
+     *     stale data from the index is acceptable.
+     */
+    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
+      return ref(notes.getChange().getDest()).indexedChange(cd, notes);
+    }
+
     /** Verify scoped user can {@code perm}, throwing if denied. */
     public abstract void check(GlobalOrPluginPermission perm)
         throws AuthException, PermissionBackendException;
@@ -269,6 +278,12 @@
     /** @return instance scoped to change. */
     public abstract ForChange change(ChangeNotes notes);
 
+    /**
+     * @return instance scoped to change loaded from index. This method should only be used when
+     *     database access is harmful and potentially stale data from the index is acceptable.
+     */
+    public abstract ForChange indexedChange(ChangeData cd, ChangeNotes notes);
+
     /** Verify scoped user can {@code perm}, throwing if denied. */
     public abstract void check(RefPermission perm) throws AuthException, PermissionBackendException;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 94f5ebf..d0b7ad4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -136,18 +136,6 @@
       return create(refControl, notesFactory.create(db, project, changeId));
     }
 
-    /**
-     * Create a change control for a change that was loaded from index. This method should only be
-     * used when database access is harmful and potentially stale data from the index is acceptable.
-     *
-     * @param refControl ref control
-     * @param change change loaded from secondary index
-     * @return change control
-     */
-    ChangeControl createForIndexedChange(RefControl refControl, Change change) {
-      return create(refControl, notesFactory.createFromIndexedChange(change));
-    }
-
     ChangeControl create(RefControl refControl, ChangeNotes notes) {
       return new ChangeControl(changeDataFactory, approvalsUtil, refControl, notes, patchSetUtil);
     }
@@ -209,12 +197,12 @@
   }
 
   /** Can this user see this change? */
-  public boolean isVisible(ReviewDb db) throws OrmException {
+  boolean isVisible(ReviewDb db) throws OrmException {
     return isVisible(db, null);
   }
 
   /** Can this user see this change? */
-  public boolean isVisible(ReviewDb db, @Nullable ChangeData cd) throws OrmException {
+  private boolean isVisible(ReviewDb db, @Nullable ChangeData cd) throws OrmException {
     if (getChange().isPrivate() && !isPrivateVisible(db, cd)) {
       return false;
     }
@@ -226,6 +214,7 @@
 
   /** Can this user see the given patchset? */
   public boolean isPatchVisible(PatchSet ps, ReviewDb db) throws OrmException {
+    // TODO(hiesel) These don't need to be migrated, just remove after support for drafts is removed
     if (ps != null && ps.isDraft() && !isDraftVisible(db, null)) {
       return false;
     }
@@ -234,6 +223,7 @@
 
   /** Can this user see the given patchset? */
   public boolean isPatchVisible(PatchSet ps, ChangeData cd) throws OrmException {
+    // TODO(hiesel) These don't need to be migrated, just remove after support for drafts is removed
     checkArgument(
         cd.getId().equals(ps.getId().getParentKey()), "%s not for change %s", ps, cd.getId());
     if (ps.isDraft() && !isDraftVisible(cd.db(), cd)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java
index 1a81726..1ab2dbd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java
@@ -19,13 +19,11 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.InMemoryInserter;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.io.IOException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -39,11 +37,9 @@
 
 /** Check the mergeability at current branch for a git object references expression. */
 public class CheckMergeability implements RestReadView<BranchResource> {
-
   private String source;
   private String strategy;
   private SubmitType submitType;
-  private final Provider<ReviewDb> db;
 
   @Option(
     name = "--source",
@@ -68,14 +64,15 @@
   }
 
   private final GitRepositoryManager gitManager;
+  private final CommitsCollection commits;
 
   @Inject
   CheckMergeability(
-      GitRepositoryManager gitManager, @GerritServerConfig Config cfg, Provider<ReviewDb> db) {
+      GitRepositoryManager gitManager, CommitsCollection commits, @GerritServerConfig Config cfg) {
     this.gitManager = gitManager;
+    this.commits = commits;
     this.strategy = MergeUtil.getMergeStrategy(cfg).getName();
     this.submitType = cfg.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
-    this.db = db;
   }
 
   @Override
@@ -102,7 +99,7 @@
       RevCommit targetCommit = rw.parseCommit(destRef.getObjectId());
       RevCommit sourceCommit = MergeUtil.resolveCommit(git, rw, source);
 
-      if (!resource.getControl().canReadCommit(db.get(), git, sourceCommit)) {
+      if (!commits.canRead(resource.getProjectState(), git, sourceCommit)) {
         throw new BadRequestException("do not have read permission for: " + source);
       }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
index d481c014..e38f442 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
@@ -19,33 +19,48 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.change.IncludedInResolver;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+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 org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
 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 CommitsCollection implements ChildCollection<ProjectResource, CommitResource> {
+  private static final Logger log = LoggerFactory.getLogger(CommitsCollection.class);
+
   private final DynamicMap<RestView<CommitResource>> views;
   private final GitRepositoryManager repoManager;
-  private final Provider<ReviewDb> db;
+  private final VisibleRefFilter.Factory refFilter;
+  private final Provider<InternalChangeQuery> queryProvider;
 
   @Inject
   public CommitsCollection(
       DynamicMap<RestView<CommitResource>> views,
       GitRepositoryManager repoManager,
-      Provider<ReviewDb> db) {
+      VisibleRefFilter.Factory refFilter,
+      Provider<InternalChangeQuery> queryProvider) {
     this.views = views;
     this.repoManager = repoManager;
-    this.db = db;
+    this.refFilter = refFilter;
+    this.queryProvider = queryProvider;
   }
 
   @Override
@@ -67,7 +82,7 @@
         RevWalk rw = new RevWalk(repo)) {
       RevCommit commit = rw.parseCommit(objectId);
       rw.parseBody(commit);
-      if (!parent.getControl().canReadCommit(db.get(), repo, commit)) {
+      if (!canRead(parent.getProjectState(), repo, commit)) {
         throw new ResourceNotFoundException(id);
       }
       for (int i = 0; i < commit.getParentCount(); i++) {
@@ -83,4 +98,37 @@
   public DynamicMap<RestView<CommitResource>> views() {
     return views;
   }
+
+  /** @return true if {@code commit} is visible to the caller. */
+  public boolean canRead(ProjectState state, Repository repo, RevCommit commit) {
+    Project.NameKey project = state.getProject().getNameKey();
+
+    // Look for changes associated with the commit.
+    try {
+      List<ChangeData> changes =
+          queryProvider.get().enforceVisibility(true).byProjectCommit(project, commit);
+      if (!changes.isEmpty()) {
+        return true;
+      }
+    } catch (OrmException e) {
+      log.error("Cannot look up change for commit " + commit.name() + " in " + project, e);
+    }
+
+    return isReachableFrom(state, repo, commit, repo.getAllRefs());
+  }
+
+  public boolean isReachableFrom(
+      ProjectState state, Repository repo, RevCommit commit, Map<String, Ref> refs) {
+    try (RevWalk rw = new RevWalk(repo)) {
+      refs = refFilter.create(state, repo).filter(refs, true);
+      return IncludedInResolver.includedInAny(repo, rw, commit, refs.values());
+    } catch (IOException e) {
+      log.error(
+          String.format(
+              "Cannot verify permissions to commit object %s in repository %s",
+              commit.name(), state.getProject().getNameKey()),
+          e);
+      return false;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
index 03db4f6..58a8987 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
@@ -17,10 +17,8 @@
 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.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -34,13 +32,13 @@
 
 @Singleton
 public class GetHead implements RestReadView<ProjectResource> {
-  private GitRepositoryManager repoManager;
-  private Provider<ReviewDb> db;
+  private final GitRepositoryManager repoManager;
+  private final CommitsCollection commits;
 
   @Inject
-  GetHead(GitRepositoryManager repoManager, Provider<ReviewDb> db) {
+  GetHead(GitRepositoryManager repoManager, CommitsCollection commits) {
     this.repoManager = repoManager;
-    this.db = db;
+    this.commits = commits;
   }
 
   @Override
@@ -59,7 +57,7 @@
       } else if (head.getObjectId() != null) {
         try (RevWalk rw = new RevWalk(repo)) {
           RevCommit commit = rw.parseCommit(head.getObjectId());
-          if (rsrc.getControl().canReadCommit(db.get(), repo, commit)) {
+          if (commits.canRead(rsrc.getProjectState(), repo, commit)) {
             return head.getObjectId().name();
           }
           throw new AuthException("not allowed to see HEAD");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index a1ab177..ff0070d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
@@ -39,11 +40,9 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.change.IncludedInResolver;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitUploadPackGroups;
-import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.FailedPermissionBackend;
@@ -55,7 +54,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -71,10 +69,11 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
 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;
 
@@ -132,16 +131,14 @@
 
   private final Set<AccountGroup.UUID> uploadGroups;
   private final Set<AccountGroup.UUID> receiveGroups;
-
   private final String canonicalWebUrl;
   private final PermissionBackend.WithUser perm;
   private final CurrentUser user;
   private final ProjectState state;
+  private final CommitsCollection commits;
   private final ChangeControl.Factory changeControlFactory;
   private final PermissionCollection.Factory permissionFilter;
-  private final VisibleRefFilter.Factory refFilter;
   private final Collection<ContributorAgreement> contributorAgreements;
-  private final Provider<InternalChangeQuery> queryProvider;
   private final Metrics metrics;
 
   private List<SectionMatcher> allSections;
@@ -156,22 +153,20 @@
       @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
       ProjectCache pc,
       PermissionCollection.Factory permissionFilter,
+      CommitsCollection commits,
       ChangeControl.Factory changeControlFactory,
-      VisibleRefFilter.Factory refFilter,
-      Provider<InternalChangeQuery> queryProvider,
       @CanonicalWebUrl @Nullable String canonicalWebUrl,
       PermissionBackend permissionBackend,
       @Assisted CurrentUser who,
       @Assisted ProjectState ps,
       Metrics metrics) {
     this.changeControlFactory = changeControlFactory;
-    this.refFilter = refFilter;
     this.uploadGroups = uploadGroups;
     this.receiveGroups = receiveGroups;
     this.permissionFilter = permissionFilter;
+    this.commits = commits;
     this.contributorAgreements = pc.getAllProjects().getConfig().getContributorAgreements();
     this.canonicalWebUrl = canonicalWebUrl;
-    this.queryProvider = queryProvider;
     this.metrics = metrics;
     this.perm = permissionBackend.user(who);
     user = who;
@@ -190,17 +185,6 @@
         controlForRef(change.getDest()), db, change.getProject(), change.getId());
   }
 
-  /**
-   * Create a change control for a change that was loaded from index. This method should only be
-   * used when database access is harmful and potentially stale data from the index is acceptable.
-   *
-   * @param change change loaded from secondary index
-   * @return change control
-   */
-  public ChangeControl controlForIndexedChange(Change change) {
-    return changeControlFactory.createForIndexedChange(controlForRef(change.getDest()), change);
-  }
-
   public ChangeControl controlFor(ChangeNotes notes) {
     return changeControlFactory.create(controlForRef(notes.getChange().getDest()), notes);
   }
@@ -483,50 +467,26 @@
     return false;
   }
 
-  /** @return whether a commit is visible to user. */
-  public boolean canReadCommit(ReviewDb db, Repository repo, RevCommit commit) {
-    // Look for changes associated with the commit.
+  boolean isReachableFromHeadsOrTags(Repository repo, RevCommit commit) {
     try {
-      List<ChangeData> changes =
-          queryProvider.get().byProjectCommit(getProject().getNameKey(), commit);
-      for (ChangeData change : changes) {
-        if (controlFor(db, change.change()).isVisible(db)) {
-          return true;
-        }
+      RefDatabase refdb = repo.getRefDatabase();
+      Collection<Ref> heads = refdb.getRefs(Constants.R_HEADS).values();
+      Collection<Ref> tags = refdb.getRefs(Constants.R_TAGS).values();
+      Map<String, Ref> refs = Maps.newHashMapWithExpectedSize(heads.size() + tags.size());
+      for (Ref r : Iterables.concat(heads, tags)) {
+        refs.put(r.getName(), r);
       }
-    } catch (OrmException e) {
-      log.error(
-          "Cannot look up change for commit " + commit.name() + " in " + getProject().getName(), e);
-    }
-    // Scan all visible refs.
-    return canReadCommitFromVisibleRef(repo, commit);
-  }
-
-  private boolean canReadCommitFromVisibleRef(Repository repo, RevCommit commit) {
-    try (RevWalk rw = new RevWalk(repo)) {
-      return isMergedIntoVisibleRef(repo, rw, commit, repo.getAllRefs().values());
+      return commits.isReachableFrom(state, repo, commit, refs);
     } catch (IOException e) {
-      String msg =
+      log.error(
           String.format(
               "Cannot verify permissions to commit object %s in repository %s",
-              commit.name(), getProject().getNameKey());
-      log.error(msg, e);
+              commit.name(), getProject().getNameKey()),
+          e);
       return false;
     }
   }
 
-  boolean isMergedIntoVisibleRef(
-      Repository repo, RevWalk rw, RevCommit commit, Collection<Ref> unfilteredRefs)
-      throws IOException {
-    VisibleRefFilter filter = refFilter.create(state, repo);
-    Map<String, Ref> m = Maps.newHashMapWithExpectedSize(unfilteredRefs.size());
-    for (Ref r : unfilteredRefs) {
-      m.put(r.getName(), r);
-    }
-    Map<String, Ref> refs = filter.filter(m, true);
-    return IncludedInResolver.includedInAny(repo, rw, commit, refs.values());
-  }
-
   ForProject asForProject() {
     return new ForProjectImpl();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 4bb823e..1ab1dbb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -45,9 +45,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import org.eclipse.jgit.lib.Constants;
 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.RevObject;
@@ -347,7 +345,7 @@
       // If the user has push permissions, they can create the ref regardless
       // of whether they are pushing any new objects along with the create.
       return null;
-    } else if (isMergedIntoBranchOrTag(repo, commit)) {
+    } else if (projectControl.isReachableFromHeadsOrTags(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
@@ -357,21 +355,6 @@
     return userId + " lacks permission " + Permission.PUSH + " for creating new commit object";
   }
 
-  private boolean isMergedIntoBranchOrTag(Repository repo, RevCommit commit) {
-    try (RevWalk rw = new RevWalk(repo)) {
-      List<Ref> refs = new ArrayList<>(repo.getRefDatabase().getRefs(Constants.R_HEADS).values());
-      refs.addAll(repo.getRefDatabase().getRefs(Constants.R_TAGS).values());
-      return projectControl.isMergedIntoVisibleRef(repo, rw, commit, refs);
-    } catch (IOException e) {
-      String msg =
-          String.format(
-              "Cannot verify permissions to commit object %s in repository %s",
-              commit.name(), projectControl.getProject().getNameKey());
-      log.error(msg, e);
-    }
-    return false;
-  }
-
   /**
    * Determines whether the user can delete the Git ref controlled by this object.
    *
@@ -713,6 +696,11 @@
     }
 
     @Override
+    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
+      return getProjectControl().controlFor(notes).asForChange(cd, db);
+    }
+
+    @Override
     public void check(RefPermission perm) throws AuthException, PermissionBackendException {
       if (!can(perm)) {
         throw new AuthException(perm.describeForException() + " not permitted");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 9f63b16..7274100 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.googlecode.prolog_cafe.exceptions.CompileException;
@@ -85,6 +86,7 @@
 
   private final AccountCache accountCache;
   private final Accounts accounts;
+  private final Emails emails;
   private final ChangeData cd;
   private final ChangeControl control;
 
@@ -96,10 +98,12 @@
 
   private Term submitRule;
 
-  public SubmitRuleEvaluator(AccountCache accountCache, Accounts accounts, ChangeData cd)
+  public SubmitRuleEvaluator(
+      AccountCache accountCache, Accounts accounts, Emails emails, ChangeData cd)
       throws OrmException {
     this.accountCache = accountCache;
     this.accounts = accounts;
+    this.emails = emails;
     this.cd = cd;
     this.control = cd.changeControl();
   }
@@ -573,6 +577,7 @@
     }
     env.set(StoredValues.ACCOUNTS, accounts);
     env.set(StoredValues.ACCOUNT_CACHE, accountCache);
+    env.set(StoredValues.EMAILS, emails);
     env.set(StoredValues.REVIEW_DB, cd.db());
     env.set(StoredValues.CHANGE_DATA, cd);
     env.set(StoredValues.CHANGE_CONTROL, control);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index 70d8484..8fa29753 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -14,8 +14,12 @@
 
 package com.google.gerrit.server.query.account;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.common.base.Joiner;
+import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -24,6 +28,7 @@
 import com.google.gerrit.server.query.InternalQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
 import org.slf4j.Logger;
@@ -106,6 +111,17 @@
     return query(AccountPredicates.preferredEmail(email));
   }
 
+  public Multimap<String, AccountState> byPreferredEmail(String... emails) throws OrmException {
+    List<String> emailList = Arrays.asList(emails);
+    List<List<AccountState>> r =
+        query(emailList.stream().map(e -> AccountPredicates.preferredEmail(e)).collect(toList()));
+    Multimap<String, AccountState> accountsByEmail = ArrayListMultimap.create();
+    for (int i = 0; i < emailList.size(); i++) {
+      accountsByEmail.putAll(emailList.get(i), r.get(i));
+    }
+    return accountsByEmail;
+  }
+
   public List<AccountState> byWatchedProject(Project.NameKey project) throws OrmException {
     return query(AccountPredicates.watchedProject(project));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 96f6b5d..af23b8d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -60,6 +60,7 @@
 import com.google.gerrit.server.StarredChangesUtil.StarRef;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.change.MergeabilityCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
@@ -304,7 +305,7 @@
     ChangeData cd =
         new ChangeData(
             null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-            null, null, null, project, id);
+            null, null, null, null, project, id);
     cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));
     return cd;
   }
@@ -316,6 +317,7 @@
   private final IdentifiedUser.GenericFactory userFactory;
   private final AccountCache accountCache;
   private final Accounts accounts;
+  private final Emails emails;
   private final ProjectCache projectCache;
   private final MergeUtil.Factory mergeUtilFactory;
   private final ChangeNotes.Factory notesFactory;
@@ -376,6 +378,7 @@
       IdentifiedUser.GenericFactory userFactory,
       AccountCache accountCache,
       Accounts accounts,
+      Emails emails,
       ProjectCache projectCache,
       MergeUtil.Factory mergeUtilFactory,
       ChangeNotes.Factory notesFactory,
@@ -396,6 +399,7 @@
     this.userFactory = userFactory;
     this.accountCache = accountCache;
     this.accounts = accounts;
+    this.emails = emails;
     this.projectCache = projectCache;
     this.mergeUtilFactory = mergeUtilFactory;
     this.notesFactory = notesFactory;
@@ -418,6 +422,7 @@
       IdentifiedUser.GenericFactory userFactory,
       AccountCache accountCache,
       Accounts accounts,
+      Emails emails,
       ProjectCache projectCache,
       MergeUtil.Factory mergeUtilFactory,
       ChangeNotes.Factory notesFactory,
@@ -437,6 +442,7 @@
     this.userFactory = userFactory;
     this.accountCache = accountCache;
     this.accounts = accounts;
+    this.emails = emails;
     this.projectCache = projectCache;
     this.mergeUtilFactory = mergeUtilFactory;
     this.notesFactory = notesFactory;
@@ -460,6 +466,7 @@
       IdentifiedUser.GenericFactory userFactory,
       AccountCache accountCache,
       Accounts accounts,
+      Emails emails,
       ProjectCache projectCache,
       MergeUtil.Factory mergeUtilFactory,
       ChangeNotes.Factory notesFactory,
@@ -479,6 +486,7 @@
     this.userFactory = userFactory;
     this.accountCache = accountCache;
     this.accounts = accounts;
+    this.emails = emails;
     this.projectCache = projectCache;
     this.mergeUtilFactory = mergeUtilFactory;
     this.notesFactory = notesFactory;
@@ -503,6 +511,7 @@
       IdentifiedUser.GenericFactory userFactory,
       AccountCache accountCache,
       Accounts accounts,
+      Emails emails,
       ProjectCache projectCache,
       MergeUtil.Factory mergeUtilFactory,
       ChangeNotes.Factory notesFactory,
@@ -522,6 +531,7 @@
     this.userFactory = userFactory;
     this.accountCache = accountCache;
     this.accounts = accounts;
+    this.emails = emails;
     this.projectCache = projectCache;
     this.mergeUtilFactory = mergeUtilFactory;
     this.notesFactory = notesFactory;
@@ -547,6 +557,7 @@
       IdentifiedUser.GenericFactory userFactory,
       AccountCache accountCache,
       Accounts accounts,
+      Emails emails,
       ProjectCache projectCache,
       MergeUtil.Factory mergeUtilFactory,
       ChangeNotes.Factory notesFactory,
@@ -569,6 +580,7 @@
     this.userFactory = userFactory;
     this.accountCache = accountCache;
     this.accounts = accounts;
+    this.emails = emails;
     this.projectCache = projectCache;
     this.mergeUtilFactory = mergeUtilFactory;
     this.notesFactory = notesFactory;
@@ -1117,7 +1129,9 @@
         return Collections.emptyList();
       }
       records =
-          new SubmitRuleEvaluator(accountCache, accounts, this).setOptions(options).evaluate();
+          new SubmitRuleEvaluator(accountCache, accounts, emails, this)
+              .setOptions(options)
+              .evaluate();
       submitRecords.put(options, records);
     }
     return records;
@@ -1134,7 +1148,8 @@
 
   public SubmitTypeRecord submitTypeRecord() throws OrmException {
     if (submitTypeRecord == null) {
-      submitTypeRecord = new SubmitRuleEvaluator(accountCache, accounts, this).getSubmitType();
+      submitTypeRecord =
+          new SubmitRuleEvaluator(accountCache, accounts, emails, this).getSubmitType();
     }
     return submitTypeRecord;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index fa08f53..15fd190 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -18,6 +18,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.IsVisibleToPredicate;
@@ -29,17 +32,20 @@
   protected final ChangeNotes.Factory notesFactory;
   protected final ChangeControl.GenericFactory changeControl;
   protected final CurrentUser user;
+  protected final PermissionBackend permissionBackend;
 
   public ChangeIsVisibleToPredicate(
       Provider<ReviewDb> db,
       ChangeNotes.Factory notesFactory,
       ChangeControl.GenericFactory changeControlFactory,
-      CurrentUser user) {
+      CurrentUser user,
+      PermissionBackend permissionBackend) {
     super(ChangeQueryBuilder.FIELD_VISIBLETO, describe(user));
     this.db = db;
     this.notesFactory = notesFactory;
     this.changeControl = changeControlFactory;
     this.user = user;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
@@ -47,21 +53,35 @@
     if (cd.fastIsVisibleTo(user)) {
       return true;
     }
+    Change change;
     try {
-      Change c = cd.change();
-      if (c == null) {
+      change = cd.change();
+      if (change == null) {
         return false;
       }
-
-      ChangeNotes notes = notesFactory.createFromIndexedChange(c);
-      ChangeControl cc = changeControl.controlFor(notes, user);
-      if (cc.isVisible(db.get(), cd)) {
-        cd.cacheVisibleTo(cc);
-        return true;
-      }
     } catch (NoSuchChangeException e) {
       // Ignored
+      return false;
     }
+
+    ChangeNotes notes = notesFactory.createFromIndexedChange(change);
+    ChangeControl cc = changeControl.controlFor(notes, user);
+    boolean visible;
+    try {
+      visible =
+          permissionBackend
+              .user(user)
+              .indexedChange(cd, notes)
+              .database(db)
+              .test(ChangePermission.READ);
+    } catch (PermissionBackendException e) {
+      throw new OrmException("unable to check permissions", e);
+    }
+    if (visible) {
+      cd.cacheVisibleTo(cc);
+      return true;
+    }
+
     return false;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index aecfc42..52802de 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -80,6 +80,7 @@
 import com.google.inject.ProvisionException;
 import com.google.inject.util.Providers;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
@@ -648,7 +649,12 @@
 
   @Operator
   public Predicate<ChangeData> conflicts(String value) throws OrmException, QueryParseException {
-    return new ConflictsPredicate(args, value, parseChange(value));
+    List<Change> changes = parseChange(value);
+    List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
+    for (Change c : changes) {
+      or.add(ConflictsPredicate.create(args, value, c));
+    }
+    return Predicate.or(or);
   }
 
   @Operator
@@ -940,7 +946,7 @@
 
   public Predicate<ChangeData> visibleto(CurrentUser user) {
     return new ChangeIsVisibleToPredicate(
-        args.db, args.notesFactory, args.changeControlGenericFactory, user);
+        args.db, args.notesFactory, args.changeControlGenericFactory, user, args.permissionBackend);
   }
 
   public Predicate<ChangeData> is_visible() throws QueryParseException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index f4064f5..eeb6d01 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryProcessor;
@@ -55,6 +56,7 @@
   private final ChangeControl.GenericFactory changeControlFactory;
   private final ChangeNotes.Factory notesFactory;
   private final DynamicMap<ChangeAttributeFactory> attributeFactories;
+  private final PermissionBackend permissionBackend;
 
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
@@ -74,7 +76,8 @@
       Provider<ReviewDb> db,
       ChangeControl.GenericFactory changeControlFactory,
       ChangeNotes.Factory notesFactory,
-      DynamicMap<ChangeAttributeFactory> attributeFactories) {
+      DynamicMap<ChangeAttributeFactory> attributeFactories,
+      PermissionBackend permissionBackend) {
     super(
         userProvider,
         limitsFactory,
@@ -88,6 +91,7 @@
     this.changeControlFactory = changeControlFactory;
     this.notesFactory = notesFactory;
     this.attributeFactories = attributeFactories;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
@@ -130,7 +134,8 @@
   protected Predicate<ChangeData> enforceVisibility(Predicate<ChangeData> pred) {
     return new AndChangeSource(
         pred,
-        new ChangeIsVisibleToPredicate(db, notesFactory, changeControlFactory, userProvider.get()),
+        new ChangeIsVisibleToPredicate(
+            db, notesFactory, changeControlFactory, userProvider.get(), permissionBackend),
         start);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 9e8f77b..8212d64 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -25,7 +24,6 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.query.OrPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
@@ -45,130 +43,48 @@
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
 
-public class ConflictsPredicate extends OrPredicate<ChangeData> {
+public class ConflictsPredicate {
   // UI code may depend on this string, so use caution when changing.
   protected static final String TOO_MANY_FILES = "too many files to find conflicts";
 
-  protected final String value;
+  private ConflictsPredicate() {}
 
-  public ConflictsPredicate(Arguments args, String value, List<Change> changes)
+  public static Predicate<ChangeData> create(Arguments args, String value, Change c)
       throws QueryParseException, OrmException {
-    super(predicates(args, value, changes));
-    this.value = value;
-  }
-
-  public static List<Predicate<ChangeData>> predicates(
-      final Arguments args, String value, List<Change> changes)
-      throws QueryParseException, OrmException {
-    int indexTerms = 0;
-
-    List<Predicate<ChangeData>> changePredicates = Lists.newArrayListWithCapacity(changes.size());
-    final Provider<ReviewDb> db = args.db;
-    for (Change c : changes) {
-      final ChangeDataCache changeDataCache =
-          new ChangeDataCache(c, db, args.changeDataFactory, args.projectCache);
-      List<String> files = listFiles(c, args, changeDataCache);
-      indexTerms += 3 + files.size();
-      if (indexTerms > args.indexConfig.maxTerms()) {
-        // Short-circuit with a nice error message if we exceed the index
-        // backend's term limit. This assumes that "conflicts:foo" is the entire
-        // query; if there are more terms in the input, we might not
-        // short-circuit here, which will result in a more generic error message
-        // later on in the query parsing.
-        throw new QueryParseException(TOO_MANY_FILES);
-      }
-
-      List<Predicate<ChangeData>> filePredicates = Lists.newArrayListWithCapacity(files.size());
-      for (String file : files) {
-        filePredicates.add(new EqualsPathPredicate(ChangeQueryBuilder.FIELD_PATH, file));
-      }
-
-      List<Predicate<ChangeData>> predicatesForOneChange = Lists.newArrayListWithCapacity(5);
-      predicatesForOneChange.add(not(new LegacyChangeIdPredicate(c.getId())));
-      predicatesForOneChange.add(new ProjectPredicate(c.getProject().get()));
-      predicatesForOneChange.add(new RefPredicate(c.getDest().get()));
-
-      predicatesForOneChange.add(or(or(filePredicates), new IsMergePredicate(args, value)));
-
-      predicatesForOneChange.add(
-          new ChangeOperatorPredicate(ChangeQueryBuilder.FIELD_CONFLICTS, value) {
-
-            @Override
-            public boolean match(ChangeData object) throws OrmException {
-              Change otherChange = object.change();
-              if (otherChange == null) {
-                return false;
-              }
-              if (!otherChange.getDest().equals(c.getDest())) {
-                return false;
-              }
-              SubmitTypeRecord str = object.submitTypeRecord();
-              if (!str.isOk()) {
-                return false;
-              }
-              ObjectId other = ObjectId.fromString(object.currentPatchSet().getRevision().get());
-              ConflictKey conflictsKey =
-                  new ConflictKey(
-                      changeDataCache.getTestAgainst(),
-                      other,
-                      str.type,
-                      changeDataCache.getProjectState().isUseContentMerge());
-              Boolean conflicts = args.conflictsCache.getIfPresent(conflictsKey);
-              if (conflicts != null) {
-                return conflicts;
-              }
-              try (Repository repo = args.repoManager.openRepository(otherChange.getProject());
-                  CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
-                conflicts =
-                    !args.submitDryRun.run(
-                        str.type,
-                        repo,
-                        rw,
-                        otherChange.getDest(),
-                        changeDataCache.getTestAgainst(),
-                        other,
-                        getAlreadyAccepted(repo, rw));
-                args.conflictsCache.put(conflictsKey, conflicts);
-                return conflicts;
-              } catch (IntegrationException | NoSuchProjectException | IOException e) {
-                throw new OrmException(e);
-              }
-            }
-
-            @Override
-            public int getCost() {
-              return 5;
-            }
-
-            private Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw)
-                throws IntegrationException {
-              try {
-                Set<RevCommit> accepted = new HashSet<>();
-                SubmitDryRun.addCommits(changeDataCache.getAlreadyAccepted(repo), rw, accepted);
-                ObjectId tip = changeDataCache.getTestAgainst();
-                if (tip != null) {
-                  accepted.add(rw.parseCommit(tip));
-                }
-                return accepted;
-              } catch (OrmException | IOException e) {
-                throw new IntegrationException("Failed to determine already accepted commits.", e);
-              }
-            }
-          });
-      changePredicates.add(and(predicatesForOneChange));
+    ChangeDataCache changeDataCache =
+        new ChangeDataCache(c, args.db, args.changeDataFactory, args.projectCache);
+    List<String> files = listFiles(c, args, changeDataCache);
+    if (3 + files.size() > args.indexConfig.maxTerms()) {
+      // Short-circuit with a nice error message if we exceed the index
+      // backend's term limit. This assumes that "conflicts:foo" is the entire
+      // query; if there are more terms in the input, we might not
+      // short-circuit here, which will result in a more generic error message
+      // later on in the query parsing.
+      throw new QueryParseException(TOO_MANY_FILES);
     }
-    return changePredicates;
+
+    List<Predicate<ChangeData>> filePredicates = new ArrayList<>(files.size());
+    for (String file : files) {
+      filePredicates.add(new EqualsPathPredicate(ChangeQueryBuilder.FIELD_PATH, file));
+    }
+
+    List<Predicate<ChangeData>> and = new ArrayList<>(5);
+    and.add(new ProjectPredicate(c.getProject().get()));
+    and.add(new RefPredicate(c.getDest().get()));
+    and.add(Predicate.not(new LegacyChangeIdPredicate(c.getId())));
+    and.add(Predicate.or(filePredicates));
+    and.add(new CheckConflict(ChangeQueryBuilder.FIELD_CONFLICTS, value, args, c, changeDataCache));
+    return Predicate.and(and);
   }
 
-  public static List<String> listFiles(Change c, Arguments args, ChangeDataCache changeDataCache)
+  private static List<String> listFiles(Change c, Arguments args, ChangeDataCache changeDataCache)
       throws OrmException {
     try (Repository repo = args.repoManager.openRepository(c.getProject());
         RevWalk rw = new RevWalk(repo)) {
       RevCommit ps = rw.parseCommit(changeDataCache.getTestAgainst());
       if (ps.getParentCount() > 1) {
         String dest = c.getDest().get();
-        Ref destBranch = repo.getRefDatabase().getRef(dest);
-        destBranch.getObjectId();
+        Ref destBranch = repo.getRefDatabase().exactRef(dest);
         rw.setRevFilter(RevFilter.MERGE_BASE);
         rw.markStart(rw.parseCommit(destBranch.getObjectId()));
         rw.markStart(ps);
@@ -195,9 +111,80 @@
     }
   }
 
-  @Override
-  public String toString() {
-    return ChangeQueryBuilder.FIELD_CONFLICTS + ":" + value;
+  private static final class CheckConflict extends ChangeOperatorPredicate {
+    private final Arguments args;
+    private final Change c;
+    private final ChangeDataCache changeDataCache;
+
+    CheckConflict(
+        String field, String value, Arguments args, Change c, ChangeDataCache changeDataCache) {
+      super(field, value);
+      this.args = args;
+      this.c = c;
+      this.changeDataCache = changeDataCache;
+    }
+
+    @Override
+    public boolean match(ChangeData object) throws OrmException {
+      Change otherChange = object.change();
+      if (otherChange == null || !otherChange.getDest().equals(c.getDest())) {
+        return false;
+      }
+
+      SubmitTypeRecord str = object.submitTypeRecord();
+      if (!str.isOk()) {
+        return false;
+      }
+
+      ObjectId other = ObjectId.fromString(object.currentPatchSet().getRevision().get());
+      ConflictKey conflictsKey =
+          new ConflictKey(
+              changeDataCache.getTestAgainst(),
+              other,
+              str.type,
+              changeDataCache.getProjectState().isUseContentMerge());
+      Boolean conflicts = args.conflictsCache.getIfPresent(conflictsKey);
+      if (conflicts != null) {
+        return conflicts;
+      }
+
+      try (Repository repo = args.repoManager.openRepository(otherChange.getProject());
+          CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+        conflicts =
+            !args.submitDryRun.run(
+                str.type,
+                repo,
+                rw,
+                otherChange.getDest(),
+                changeDataCache.getTestAgainst(),
+                other,
+                getAlreadyAccepted(repo, rw));
+        args.conflictsCache.put(conflictsKey, conflicts);
+        return conflicts;
+      } catch (IntegrationException | NoSuchProjectException | IOException e) {
+        throw new OrmException(e);
+      }
+    }
+
+    @Override
+    public int getCost() {
+      return 5;
+    }
+
+    private Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw)
+        throws IntegrationException {
+      try {
+        Set<RevCommit> accepted = new HashSet<>();
+        SubmitDryRun.addCommits(changeDataCache.getAlreadyAccepted(repo), rw, accepted);
+        ObjectId tip = changeDataCache.getTestAgainst();
+        if (tip != null) {
+          accepted.add(rw.parseCommit(tip));
+        }
+        return accepted;
+      } catch (OrmException | IOException e) {
+        throw new IntegrationException("Failed to determine already accepted commits.", e);
+      }
+    }
   }
 
   public static class ChangeDataCache {
@@ -208,7 +195,7 @@
 
     protected ObjectId testAgainst;
     protected ProjectState projectState;
-    protected Iterable<ObjectId> alreadyAccepted;
+    protected Set<ObjectId> alreadyAccepted;
 
     public ChangeDataCache(
         Change change,
@@ -240,7 +227,7 @@
       return projectState;
     }
 
-    protected Iterable<ObjectId> getAlreadyAccepted(Repository repo) throws IOException {
+    Set<ObjectId> getAlreadyAccepted(Repository repo) throws IOException {
       if (alreadyAccepted == null) {
         alreadyAccepted = SubmitDryRun.getAlreadyAccepted(repo);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java
deleted file mode 100644
index 28fb7cf..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java
+++ /dev/null
@@ -1,50 +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.query.change;
-
-import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
-import com.google.gwtorm.server.OrmException;
-import java.io.IOException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-public class IsMergePredicate extends ChangeOperatorPredicate {
-  protected final Arguments args;
-
-  public IsMergePredicate(Arguments args, String value) {
-    super(ChangeQueryBuilder.FIELD_MERGE, value);
-    this.args = args;
-  }
-
-  @Override
-  public boolean match(ChangeData cd) throws OrmException {
-    ObjectId id = ObjectId.fromString(cd.currentPatchSet().getRevision().get());
-    try (Repository repo = args.repoManager.openRepository(cd.change().getProject());
-        RevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
-      RevCommit commit = rw.parseCommit(id);
-      return commit.getParentCount() > 1;
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-  }
-
-  @Override
-  public int getCost() {
-    return 2;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index b6756c8..3753600 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
@@ -72,6 +73,7 @@
   private final ReviewDb db;
   private final AccountCache accountCache;
   private final Accounts accounts;
+  private final Emails emails;
   private final GitRepositoryManager repoManager;
   private final ChangeQueryBuilder queryBuilder;
   private final ChangeQueryProcessor queryProcessor;
@@ -98,6 +100,7 @@
       ReviewDb db,
       AccountCache accountCache,
       Accounts accounts,
+      Emails emails,
       GitRepositoryManager repoManager,
       ChangeQueryBuilder queryBuilder,
       ChangeQueryProcessor queryProcessor,
@@ -107,6 +110,7 @@
     this.db = db;
     this.accountCache = accountCache;
     this.accounts = accounts;
+    this.emails = emails;
     this.repoManager = repoManager;
     this.queryBuilder = queryBuilder;
     this.queryProcessor = queryProcessor;
@@ -253,7 +257,7 @@
     if (includeSubmitRecords) {
       eventFactory.addSubmitRecords(
           c,
-          new SubmitRuleEvaluator(accountCache, accounts, d)
+          new SubmitRuleEvaluator(accountCache, accounts, emails, d)
               .setAllowClosed(true)
               .setAllowDraft(true)
               .evaluate());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 2a74aee..e3b9b3a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -35,7 +35,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_156> C = Schema_156.class;
+  public static final Class<Schema_157> C = Schema_157.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_157.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_157.java
new file mode 100644
index 0000000..e232e7a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_157.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.sql.SQLException;
+
+/** Drop unused indexes from accounts table. */
+public class Schema_157 extends SchemaVersion {
+  @Inject
+  Schema_157(Provider<Schema_156> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+    JdbcSchema schema = (JdbcSchema) db;
+    SqlDialect dialect = schema.getDialect();
+    try (StatementExecutor e = newExecutor(db)) {
+      dialect.dropIndex(e, "accounts", "accounts_byPreferredEmail");
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/ChangeUpdateExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/ChangeUpdateExecutor.java
index 42199e9..1d957cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/ChangeUpdateExecutor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/ChangeUpdateExecutor.java
@@ -17,13 +17,11 @@
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.gerrit.server.git.ReceiveCommits;
 import com.google.inject.BindingAnnotation;
 import java.lang.annotation.Retention;
 
 /**
- * Marker on the global {@link ListeningExecutorService} used by {@link ReceiveCommits} to create or
- * replace changes.
+ * Marker on the global {@link ListeningExecutorService} used by asynchronous {@link BatchUpdate}s.
  */
 @Retention(RUNTIME)
 @BindingAnnotation
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java
index 7a1de31..d8a5278 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.update;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
@@ -21,9 +22,9 @@
 import com.github.rholder.retry.RetryListener;
 import com.github.rholder.retry.RetryerBuilder;
 import com.github.rholder.retry.StopStrategies;
-import com.github.rholder.retry.StopStrategy;
 import com.github.rholder.retry.WaitStrategies;
 import com.github.rholder.retry.WaitStrategy;
+import com.google.auto.value.AutoValue;
 import com.google.common.base.Throwables;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -32,6 +33,7 @@
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.time.Duration;
 import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.lib.Config;
 
@@ -41,9 +43,49 @@
     T call(BatchUpdate.Factory updateFactory) throws Exception;
   }
 
+  /**
+   * Options for retrying a single operation.
+   *
+   * <p>This class is similar in function to upstream's {@link RetryerBuilder}, but it exists as its
+   * own class in Gerrit for several reasons:
+   *
+   * <ul>
+   *   <li>Gerrit needs to support defaults for some of the options, such as a default timeout.
+   *       {@code RetryerBuilder} doesn't support calling the same setter multiple times, so doing
+   *       this with {@code RetryerBuilder} directly would not be easy.
+   *   <li>Gerrit explicitly does not want callers to have full control over all possible options,
+   *       so this class exposes a curated subset.
+   * </ul>
+   */
+  @AutoValue
+  public abstract static class Options {
+    @Nullable
+    abstract RetryListener listener();
+
+    @Nullable
+    abstract Duration timeout();
+
+    @AutoValue.Builder
+    public abstract static class Builder {
+      public abstract Builder listener(RetryListener listener);
+
+      public abstract Builder timeout(Duration timeout);
+
+      public abstract Options build();
+    }
+  }
+
+  public static Options.Builder options() {
+    return new AutoValue_RetryHelper_Options.Builder();
+  }
+
+  public static Options defaults() {
+    return options().build();
+  }
+
   private final NotesMigration migration;
   private final BatchUpdate.Factory updateFactory;
-  private final StopStrategy stopStrategy;
+  private final Duration defaultTimeout;
   private final WaitStrategy waitStrategy;
 
   @Inject
@@ -55,33 +97,37 @@
     this.migration = migration;
     this.updateFactory =
         new BatchUpdate.Factory(migration, reviewDbBatchUpdateFactory, noteDbBatchUpdateFactory);
-    this.stopStrategy =
-        StopStrategies.stopAfterDelay(
-            cfg.getTimeUnit("noteDb", null, "retryTimeout", SECONDS.toMillis(5), MILLISECONDS),
-            MILLISECONDS);
+    this.defaultTimeout =
+        Duration.ofMillis(
+            cfg.getTimeUnit("noteDb", null, "retryTimeout", SECONDS.toMillis(20), MILLISECONDS));
     this.waitStrategy =
         WaitStrategies.join(
             WaitStrategies.exponentialWait(
-                cfg.getTimeUnit("noteDb", null, "retryMaxWait", SECONDS.toMillis(20), MILLISECONDS),
+                cfg.getTimeUnit("noteDb", null, "retryMaxWait", SECONDS.toMillis(5), MILLISECONDS),
                 MILLISECONDS),
             WaitStrategies.randomWait(50, MILLISECONDS));
   }
 
-  public <T> T execute(Action<T> action) throws RestApiException, UpdateException {
-    return execute(action, null);
+  public Duration getDefaultTimeout() {
+    return defaultTimeout;
   }
 
-  public <T> T execute(Action<T> action, @Nullable RetryListener listener)
-      throws RestApiException, UpdateException {
+  public <T> T execute(Action<T> action) throws RestApiException, UpdateException {
+    return execute(action, defaults());
+  }
+
+  public <T> T execute(Action<T> action, Options opts) throws RestApiException, UpdateException {
     try {
       RetryerBuilder<T> builder = RetryerBuilder.newBuilder();
       if (migration.disableChangeReviewDb()) {
         builder
-            .withStopStrategy(stopStrategy)
+            .withStopStrategy(
+                StopStrategies.stopAfterDelay(
+                    firstNonNull(opts.timeout(), defaultTimeout).toMillis(), MILLISECONDS))
             .withWaitStrategy(waitStrategy)
             .retryIfException(RetryHelper::isLockFailure);
-        if (listener != null) {
-          builder.withRetryListener(listener);
+        if (opts.listener() != null) {
+          builder.withRetryListener(opts.listener());
         }
       } else {
         // Either we aren't full-NoteDb, or the underlying ref storage doesn't support atomic
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/CommentSenderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/CommentSenderTest.java
new file mode 100644
index 0000000..6b6632c
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/CommentSenderTest.java
@@ -0,0 +1,62 @@
+// 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.mail.send;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gwtorm.server.OrmException;
+import java.util.Collections;
+import org.junit.Test;
+
+public class CommentSenderTest {
+  private static class TestSender extends CommentSender {
+    TestSender() throws OrmException {
+      super(null, null, null, null, null);
+    }
+  }
+
+  // A 100-character long string.
+  private static String chars100 = String.join("", Collections.nCopies(25, "abcd"));
+
+  @Test
+  public void shortMessageNotShortened() {
+    String message = "foo bar baz";
+    assertThat(TestSender.getShortenedCommentMessage(message)).isEqualTo(message);
+
+    message = "foo bar baz.";
+    assertThat(TestSender.getShortenedCommentMessage(message)).isEqualTo(message);
+  }
+
+  @Test
+  public void longMessageIsShortened() {
+    String message = chars100 + "x";
+    String expected = chars100 + " […]";
+    assertThat(TestSender.getShortenedCommentMessage(message)).isEqualTo(expected);
+  }
+
+  @Test
+  public void shortenedToFirstLine() {
+    String message = "abc\n" + chars100;
+    String expected = "abc […]";
+    assertThat(TestSender.getShortenedCommentMessage(message)).isEqualTo(expected);
+  }
+
+  @Test
+  public void shortenedToFirstSentence() {
+    String message = "foo bar baz. " + chars100;
+    String expected = "foo bar baz. […]";
+    assertThat(TestSender.getShortenedCommentMessage(message)).isEqualTo(expected);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/CommitsCollectionTest.java
similarity index 84%
rename from gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java
rename to gerrit-server/src/test/java/com/google/gerrit/server/project/CommitsCollectionTest.java
index 92d7a52..0d8080f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/CommitsCollectionTest.java
@@ -55,19 +55,19 @@
 import org.junit.Before;
 import org.junit.Test;
 
-/** Unit tests for {@link ProjectControl}. */
-public class ProjectControlTest {
+/** Unit tests for {@link CommitsCollection}. */
+public class CommitsCollectionTest {
   @Inject private AccountManager accountManager;
   @Inject private IdentifiedUser.GenericFactory userFactory;
   @Inject private InMemoryDatabase schemaFactory;
   @Inject private InMemoryRepositoryManager repoManager;
-  @Inject private ProjectControl.GenericFactory projectControlFactory;
   @Inject private SchemaCreator schemaCreator;
   @Inject private ThreadLocalRequestContext requestContext;
   @Inject protected ProjectCache projectCache;
   @Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
   @Inject protected AllProjectsName allProjects;
   @Inject protected GroupCache groupCache;
+  @Inject private CommitsCollection commits;
 
   private LifecycleManager lifecycle;
   private ReviewDb db;
@@ -135,11 +135,11 @@
   public void canReadCommitWhenAllRefsVisible() throws Exception {
     allow(project, READ, REGISTERED_USERS, "refs/*");
     ObjectId id = repo.branch("master").commit().create();
-    ProjectControl pc = newProjectControl();
+    ProjectState state = readProjectState();
     RevWalk rw = repo.getRevWalk();
     Repository r = repo.getRepository();
 
-    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(id)));
+    assertTrue(commits.canRead(state, r, rw.parseCommit(id)));
   }
 
   @Test
@@ -150,12 +150,12 @@
     ObjectId id1 = repo.branch("branch1").commit().create();
     ObjectId id2 = repo.branch("branch2").commit().create();
 
-    ProjectControl pc = newProjectControl();
+    ProjectState state = readProjectState();
     RevWalk rw = repo.getRevWalk();
     Repository r = repo.getRepository();
 
-    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(id1)));
-    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(id2)));
+    assertTrue(commits.canRead(state, r, rw.parseCommit(id1)));
+    assertTrue(commits.canRead(state, r, rw.parseCommit(id2)));
   }
 
   @Test
@@ -166,12 +166,12 @@
     ObjectId id1 = repo.branch("branch1").commit().create();
     ObjectId id2 = repo.branch("branch2").commit().create();
 
-    ProjectControl pc = newProjectControl();
+    ProjectState state = readProjectState();
     RevWalk rw = repo.getRevWalk();
     Repository r = repo.getRepository();
 
-    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(id1)));
-    assertFalse(pc.canReadCommit(db, r, rw.parseCommit(id2)));
+    assertTrue(commits.canRead(state, r, rw.parseCommit(id1)));
+    assertFalse(commits.canRead(state, r, rw.parseCommit(id2)));
   }
 
   @Test
@@ -185,11 +185,11 @@
     RevCommit parent2 = repo.commit().create();
     repo.branch("branch2").commit().parent(parent2).create();
 
-    ProjectControl pc = newProjectControl();
+    ProjectState state = readProjectState();
     RevWalk rw = repo.getRevWalk();
     Repository r = repo.getRepository();
-    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(parent1)));
-    assertFalse(pc.canReadCommit(db, r, rw.parseCommit(parent2)));
+    assertTrue(commits.canRead(state, r, rw.parseCommit(parent1)));
+    assertFalse(commits.canRead(state, r, rw.parseCommit(parent2)));
   }
 
   @Test
@@ -199,16 +199,16 @@
     RevCommit parent1 = repo.commit().create();
     ObjectId id1 = repo.branch("branch1").commit().parent(parent1).create();
 
-    ProjectControl pc = newProjectControl();
+    ProjectState state = readProjectState();
     RevWalk rw = repo.getRevWalk();
     Repository r = repo.getRepository();
 
-    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(parent1)));
-    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(id1)));
+    assertTrue(commits.canRead(state, r, rw.parseCommit(parent1)));
+    assertTrue(commits.canRead(state, r, rw.parseCommit(id1)));
 
     repo.branch("branch1").update(parent1);
-    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(parent1)));
-    assertFalse(pc.canReadCommit(db, r, rw.parseCommit(id1)));
+    assertTrue(commits.canRead(state, r, rw.parseCommit(parent1)));
+    assertFalse(commits.canRead(state, r, rw.parseCommit(id1)));
   }
 
   @Test
@@ -218,20 +218,20 @@
     RevCommit parent1 = repo.commit().create();
     ObjectId id1 = repo.branch("branch1").commit().parent(parent1).create();
 
-    ProjectControl pc = newProjectControl();
+    ProjectState state = readProjectState();
     RevWalk rw = repo.getRevWalk();
     Repository r = repo.getRepository();
 
-    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(parent1)));
-    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(id1)));
+    assertTrue(commits.canRead(state, r, rw.parseCommit(parent1)));
+    assertTrue(commits.canRead(state, r, rw.parseCommit(id1)));
 
     repo.branch("branch1").update(parent1);
-    assertTrue(pc.canReadCommit(db, r, rw.parseCommit(parent1)));
-    assertFalse(pc.canReadCommit(db, r, rw.parseCommit(id1)));
+    assertTrue(commits.canRead(state, r, rw.parseCommit(parent1)));
+    assertFalse(commits.canRead(state, r, rw.parseCommit(id1)));
   }
 
-  private ProjectControl newProjectControl() throws Exception {
-    return projectControlFactory.controlFor(project.getName(), user);
+  private ProjectState readProjectState() throws Exception {
+    return projectCache.get(project.getName());
   }
 
   protected void allow(ProjectConfig project, String permission, AccountGroup.UUID id, String ref)
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 9b9cfff..f15227e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -60,7 +60,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -212,7 +211,6 @@
   @Inject private SingleVersionListener singleVersionListener;
   @Inject private InMemoryDatabase schemaFactory;
   @Inject private ThreadLocalRequestContext requestContext;
-  @Inject private Provider<InternalChangeQuery> queryProvider;
   @Inject private ProjectControl.Metrics metrics;
 
   @Before
@@ -899,17 +897,14 @@
   }
 
   private ProjectControl user(ProjectConfig local, String name, AccountGroup.UUID... memberOf) {
-    String canonicalWebUrl = "http://localhost";
-
     return new ProjectControl(
         Collections.<AccountGroup.UUID>emptySet(),
         Collections.<AccountGroup.UUID>emptySet(),
         projectCache,
         sectionSorter,
+        null, // commitsCollection
         changeControlFactory,
-        null, // refFilter
-        queryProvider,
-        canonicalWebUrl,
+        "http://localhost", // canonicalWebUrl
         permissionBackend,
         new MockUser(name, memberOf),
         newProjectState(local),
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/Schema_150_to_151_Test.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
index 52acd9f..dcd1ae5 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
@@ -21,94 +21,38 @@
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroup.Id;
 import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.group.CreateGroup;
-import com.google.gerrit.server.util.RequestContext;
-import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gerrit.testutil.InMemoryDatabase;
-import com.google.gerrit.testutil.InMemoryModule;
+import com.google.gerrit.testutil.SchemaUpgradeTestEnvironment;
+import com.google.gerrit.testutil.TestUpdateUI;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.Guice;
 import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Provider;
-import com.google.inject.util.Providers;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneOffset;
-import java.util.List;
-import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
 public class Schema_150_to_151_Test {
-  @Inject private AccountManager accountManager;
-  @Inject private IdentifiedUser.GenericFactory userFactory;
-  @Inject private SchemaFactory<ReviewDb> schemaFactory;
-  @Inject private SchemaCreator schemaCreator;
-  @Inject private ThreadLocalRequestContext requestContext;
-  @Inject private Schema_151 schema151;
+
+  @Rule public SchemaUpgradeTestEnvironment testEnv = new SchemaUpgradeTestEnvironment();
+
   @Inject private CreateGroup.Factory createGroupFactory;
+  @Inject private Schema_151 schema151;
 
-  // Only for use in setting up/tearing down injector.
-  @Inject private InMemoryDatabase inMemoryDatabase;
-
-  private LifecycleManager lifecycle;
   private ReviewDb db;
 
   @Before
   public void setUp() throws Exception {
-    Injector injector = Guice.createInjector(new InMemoryModule());
-    injector.injectMembers(this);
-    lifecycle = new LifecycleManager();
-    lifecycle.add(injector);
-    lifecycle.start();
-
-    try (ReviewDb underlyingDb = inMemoryDatabase.getDatabase().open()) {
-      schemaCreator.create(underlyingDb);
-    }
-    db = schemaFactory.open();
-    Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
-    IdentifiedUser user = userFactory.create(userId);
-
-    requestContext.setContext(
-        new RequestContext() {
-          @Override
-          public CurrentUser getUser() {
-            return user;
-          }
-
-          @Override
-          public Provider<ReviewDb> getReviewDbProvider() {
-            return Providers.of(db);
-          }
-        });
-  }
-
-  @After
-  public void tearDown() {
-    if (lifecycle != null) {
-      lifecycle.stop();
-    }
-    requestContext.setContext(null);
-    if (db != null) {
-      db.close();
-    }
-    InMemoryDatabase.drop(inMemoryDatabase);
+    testEnv.getInjector().injectMembers(this);
+    db = testEnv.getDb();
   }
 
   @Test
@@ -155,23 +99,4 @@
         db.accountGroupMembersAudit().byGroup(groupId);
     db.accountGroupMembersAudit().delete(groupMemberAudits);
   }
-
-  private static class TestUpdateUI implements UpdateUI {
-
-    @Override
-    public void message(String msg) {}
-
-    @Override
-    public boolean yesno(boolean def, String msg) {
-      return false;
-    }
-
-    @Override
-    public boolean isBatch() {
-      return false;
-    }
-
-    @Override
-    public void pruneSchema(StatementExecutor e, List<String> pruneList) throws OrmException {}
-  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/SchemaUpgradeTestEnvironment.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/SchemaUpgradeTestEnvironment.java
new file mode 100644
index 0000000..060d5a1
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/SchemaUpgradeTestEnvironment.java
@@ -0,0 +1,113 @@
+// 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.testutil;
+
+import com.google.gerrit.lifecycle.LifecycleManager;
+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.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public final class SchemaUpgradeTestEnvironment implements TestRule {
+  @Inject private AccountManager accountManager;
+  @Inject private IdentifiedUser.GenericFactory userFactory;
+  @Inject private SchemaFactory<ReviewDb> schemaFactory;
+  @Inject private SchemaCreator schemaCreator;
+  @Inject private ThreadLocalRequestContext requestContext;
+  // Only for use in setting up/tearing down injector.
+  @Inject private InMemoryDatabase inMemoryDatabase;
+
+  private ReviewDb db;
+  private Injector injector;
+  private LifecycleManager lifecycle;
+
+  @Override
+  public Statement apply(Statement statement, Description description) {
+    return new Statement() {
+      @Override
+      public void evaluate() throws Throwable {
+        try {
+          setUp();
+          statement.evaluate();
+        } finally {
+          tearDown();
+        }
+      }
+    };
+  }
+
+  public ReviewDb getDb() {
+    return db;
+  }
+
+  public Injector getInjector() {
+    return injector;
+  }
+
+  private void setUp() throws Exception {
+    injector = Guice.createInjector(new InMemoryModule());
+    injector.injectMembers(this);
+    lifecycle = new LifecycleManager();
+    lifecycle.add(injector);
+    lifecycle.start();
+
+    try (ReviewDb underlyingDb = inMemoryDatabase.getDatabase().open()) {
+      schemaCreator.create(underlyingDb);
+    }
+    db = schemaFactory.open();
+    Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
+    IdentifiedUser user = userFactory.create(userId);
+
+    requestContext.setContext(
+        new RequestContext() {
+          @Override
+          public CurrentUser getUser() {
+            return user;
+          }
+
+          @Override
+          public Provider<ReviewDb> getReviewDbProvider() {
+            return Providers.of(db);
+          }
+        });
+  }
+
+  private void tearDown() {
+    if (lifecycle != null) {
+      lifecycle.stop();
+    }
+    if (requestContext != null) {
+      requestContext.setContext(null);
+    }
+    if (db != null) {
+      db.close();
+    }
+    InMemoryDatabase.drop(inMemoryDatabase);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestUpdateUI.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestUpdateUI.java
new file mode 100644
index 0000000..644f8e2
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestUpdateUI.java
@@ -0,0 +1,38 @@
+// 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.testutil;
+
+import com.google.gerrit.server.schema.UpdateUI;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
+import java.util.List;
+
+public class TestUpdateUI implements UpdateUI {
+  @Override
+  public void message(String msg) {}
+
+  @Override
+  public boolean yesno(boolean def, String msg) {
+    return false;
+  }
+
+  @Override
+  public boolean isBatch() {
+    return false;
+  }
+
+  @Override
+  public void pruneSchema(StatementExecutor e, List<String> pruneList) throws OrmException {}
+}
diff --git a/gerrit-sshd/BUILD b/gerrit-sshd/BUILD
index db27f02..1ae0376 100644
--- a/gerrit-sshd/BUILD
+++ b/gerrit-sshd/BUILD
@@ -14,6 +14,7 @@
         "//gerrit-lucene:lucene",
         "//gerrit-patch-jgit:server",
         "//gerrit-reviewdb:server",
+        "//gerrit-server:receive",
         "//gerrit-server:server",
         "//gerrit-util-cli:cli",
         "//lib:args4j",
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
index 318e67f..58492b2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
 import java.io.IOException;
@@ -45,6 +46,8 @@
   @Inject private IdentifiedUser.GenericFactory userFactory;
 
   protected Repository repo;
+  protected ProjectState state;
+  protected Project.NameKey projectName;
   protected Project project;
 
   @Override
@@ -86,10 +89,12 @@
   }
 
   private void service() throws IOException, PermissionBackendException, Failure {
-    project = projectControl.getProjectState().getProject();
+    state = projectControl.getProjectState();
+    project = state.getProject();
+    projectName = project.getNameKey();
 
     try {
-      repo = repoManager.openRepository(project.getNameKey());
+      repo = repoManager.openRepository(projectName);
     } catch (RepositoryNotFoundException e) {
       throw new Failure(1, "fatal: '" + project.getName() + "': not a git archive", e);
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
index e64ab0e..0801447 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -68,13 +69,13 @@
   }
 
   public void addChange(String id, Map<Change.Id, ChangeResource> changes)
-      throws UnloggedFailure, OrmException {
+      throws UnloggedFailure, OrmException, PermissionBackendException {
     addChange(id, changes, null);
   }
 
   public void addChange(
       String id, Map<Change.Id, ChangeResource> changes, ProjectControl projectControl)
-      throws UnloggedFailure, OrmException {
+      throws UnloggedFailure, OrmException, PermissionBackendException {
     addChange(id, changes, projectControl, true);
   }
 
@@ -83,7 +84,7 @@
       Map<Change.Id, ChangeResource> changes,
       ProjectControl projectControl,
       boolean useIndex)
-      throws UnloggedFailure, OrmException {
+      throws UnloggedFailure, OrmException, PermissionBackendException {
     List<ChangeControl> matched =
         useIndex ? changeFinder.find(id, currentUser) : changeFromNotesFactory(id, currentUser);
     List<ChangeControl> toAdd = new ArrayList<>(changes.size());
@@ -97,7 +98,12 @@
     for (ChangeControl ctl : matched) {
       if (!changes.containsKey(ctl.getId())
           && inProject(projectControl, ctl.getProject())
-          && (canMaintainServer || ctl.isVisible(db))) {
+          && (canMaintainServer
+              || permissionBackend
+                  .user(currentUser)
+                  .change(ctl.getNotes())
+                  .database(db)
+                  .test(ChangePermission.READ))) {
         toAdd.add(ctl);
       }
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index b911044..748277e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -24,8 +24,8 @@
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.AsyncReceiveCommits;
 import com.google.gerrit.server.git.QueueProvider;
+import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
 import com.google.gerrit.server.plugins.ModuleGenerator;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
index 86209fe..821257c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.Index;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -42,7 +43,7 @@
   void addChange(String token) {
     try {
       changeArgumentParser.addChange(token, changes, null, false);
-    } catch (UnloggedFailure | OrmException e) {
+    } catch (UnloggedFailure | OrmException | PermissionBackendException e) {
       writeError("warning", e.getMessage());
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
index 1fbcc17..aa8c562 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
@@ -14,22 +14,22 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.SetMultimap;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.git.AsyncReceiveCommits;
-import com.google.gerrit.server.git.ReceiveCommits;
 import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.sshd.AbstractGitCommand;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshSession;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import org.eclipse.jgit.errors.TooLargeObjectInPackException;
 import org.eclipse.jgit.errors.UnpackException;
 import org.eclipse.jgit.lib.Ref;
@@ -52,8 +52,8 @@
   @Inject private IdentifiedUser currentUser;
   @Inject private SshSession session;
 
-  private final Set<Account.Id> reviewerId = new HashSet<>();
-  private final Set<Account.Id> ccId = new HashSet<>();
+  private final SetMultimap<ReviewerStateInternal, Account.Id> reviewers =
+      MultimapBuilder.hashKeys(2).hashSetValues().build();
 
   @Option(
     name = "--reviewer",
@@ -62,7 +62,7 @@
     usage = "request reviewer for change(s)"
   )
   void addReviewer(Account.Id id) {
-    reviewerId.add(id);
+    reviewers.put(ReviewerStateInternal.REVIEWER, id);
   }
 
   @Option(
@@ -72,7 +72,7 @@
     usage = "CC user on change(s)"
   )
   void addCC(Account.Id id) {
-    ccId.add(id);
+    reviewers.put(ReviewerStateInternal.CC, id);
   }
 
   @Override
@@ -81,17 +81,14 @@
       throw new Failure(1, "fatal: receive-pack not permitted on this server");
     }
 
-    final ReceiveCommits receive = factory.create(projectControl, repo).getReceiveCommits();
+    AsyncReceiveCommits arc = factory.create(projectControl, repo, null, reviewers);
 
-    Capable r = receive.canUpload();
+    Capable r = arc.canUpload();
     if (r != Capable.OK) {
       throw die(r.getMessage());
     }
 
-    receive.init();
-    receive.addReviewers(reviewerId);
-    receive.addExtraCC(ccId);
-    ReceivePack rp = receive.getReceivePack();
+    ReceivePack rp = arc.getReceivePack();
     try {
       rp.receive(in, out, err);
       session.setPeerAgent(rp.getPeerUserAgent());
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 5ea5bf7..026f9b7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.change.DeleteReviewer;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.ReviewerResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -79,6 +80,8 @@
       throw new IllegalArgumentException(e.getMessage(), e);
     } catch (OrmException e) {
       throw new IllegalArgumentException("database is down", e);
+    } catch (PermissionBackendException e) {
+      throw new IllegalArgumentException("can't check permissions", e);
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 093808c..9a3e6ab 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -18,13 +18,13 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.AllowedFormats;
 import com.google.gerrit.server.change.ArchiveFormat;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.CommitsCollection;
 import com.google.gerrit.sshd.AbstractGitCommand;
 import com.google.inject.Inject;
 import java.io.IOException;
@@ -121,9 +121,9 @@
   }
 
   @Inject private PermissionBackend permissionBackend;
+  @Inject private CommitsCollection commits;
   @Inject private IdentifiedUser user;
   @Inject private AllowedFormats allowedFormats;
-  @Inject private ReviewDb db;
   private Options options = new Options();
 
   /**
@@ -244,13 +244,13 @@
 
   private boolean canRead(ObjectId revId) throws IOException, PermissionBackendException {
     try {
-      permissionBackend.user(user).project(project.getNameKey()).check(ProjectPermission.READ);
+      permissionBackend.user(user).project(projectName).check(ProjectPermission.READ);
       return true;
     } catch (AuthException e) {
       // Check reachability of the specific revision.
       try (RevWalk rw = new RevWalk(repo)) {
         RevCommit commit = rw.parseCommit(revId);
-        return projectControl.canReadCommit(db, repo, commit);
+        return commits.canRead(state, repo, commit);
       }
     }
   }
diff --git a/gerrit-war/BUILD b/gerrit-war/BUILD
index 865f940..f2efb5f 100644
--- a/gerrit-war/BUILD
+++ b/gerrit-war/BUILD
@@ -18,7 +18,9 @@
         "//gerrit-pgm:init-api",
         "//gerrit-pgm:util",
         "//gerrit-reviewdb:server",
+        "//gerrit-server:module",
         "//gerrit-server:prolog-common",
+        "//gerrit-server:receive",
         "//gerrit-server:server",
         "//gerrit-sshd:sshd",
         "//lib:guava",
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index cb8860c..2d4c1d1 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -49,9 +49,9 @@
 import com.google.gerrit.server.events.StreamEventsApiListener;
 import com.google.gerrit.server.git.GarbageCollectionModule;
 import com.google.gerrit.server.git.GitRepositoryManagerModule;
-import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
diff --git a/plugins/replication b/plugins/replication
index fae5360..0721b20 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit fae5360380023e8351f39be3d4effd4bb2cd8906
+Subproject commit 0721b208ad863ff2f2c7fe1c89950dc2b921abaa
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index be803eb..fe99eb4 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit be803eb40fdcd5bfa11d9a808863585f86228e06
+Subproject commit fe99eb4399054d3fd67ff2330aa92841d76e53bb
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
index 3ef1450..4f93e98 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
@@ -127,7 +127,7 @@
     },
 
     _readOnly(item) {
-      return item.state === 'READ_ONLY' ? 'Y' : 'N';
+      return item.state === 'READ_ONLY' ? 'Y' : '';
     },
 
     _computeWeblink(project) {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-project-dialog/gr-create-project-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-project-dialog/gr-create-project-dialog.html
index 19eadce..5381b0e 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-project-dialog/gr-create-project-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-project-dialog/gr-create-project-dialog.html
@@ -60,12 +60,14 @@
         </section>
         <section>
           <span class="title">Rights inherit from</span>
-          <gr-autocomplete
-              id="rightsInheritFromInput"
-              text="{{_projectConfig.parent}}"
-              query="[[_query]]"
-              placeholder="Optional, defaults to 'All-Projects'">
-          </gr-autocomplete>
+          <span class="value">
+            <gr-autocomplete
+                id="rightsInheritFromInput"
+                text="{{_projectConfig.parent}}"
+                query="[[_query]]"
+                placeholder="Optional, defaults to 'All-Projects'">
+            </gr-autocomplete>
+          </span>
         </section>
         <section>
           <span class="title">Create initial empty commit</span>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
index 947bbcb..ee910b4 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
@@ -47,10 +47,10 @@
           <td class="type">[[itemType(item.type)]]</td>
           <td class="member">
             <a href$="[[_computeGroupUrl(item.member._account_id)]]">
-              [[item.member.name]]
+              [[_getName(item.member)]]
             </a>
           </td>
-          <td class="by-user">[[item.user.username]]</td>
+          <td class="by-user">[[_getName(item.user)]]</td>
         </tr>
       </template>
     </table>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
index f6a3880..e73e765 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
@@ -73,5 +73,13 @@
       }
       return item;
     },
+
+    _getName(account) {
+      if (account.username) {
+        return account.username + ' (' + account._account_id + ')';
+      } else if (account.name) {
+        return account.name + ' (' + account._account_id + ')';
+      }
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
new file mode 100644
index 0000000..e0cdad2
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-group-audit-log</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-group-audit-log.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-group-audit-log></gr-group-audit-log>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-group-audit-log tests', () => {
+    let element;
+
+    setup(() => {
+      element = fixture('basic');
+    });
+
+    test('test _getName', () => {
+      let account;
+      account = {
+        username: 'test-user',
+        name: 'test-name',
+        _account_id: 12,
+      };
+      assert.deepEqual(element._getName(account), 'test-user (12)');
+
+      account = {
+        name: 'test-name',
+        _account_id: 12,
+      };
+      assert.deepEqual(element._getName(account), 'test-name (12)');
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index e48ac44..4e39304 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -16,13 +16,17 @@
 
   const DEFAULT_SECTIONS = [
     {
+      name: 'Work in progress',
+      query: 'is:open owner:self is:wip',
+    },
+    {
       name: 'Outgoing reviews',
-      query: 'is:open owner:self',
+      query: 'is:open owner:self -is:wip',
     },
     {
       name: 'Incoming reviews',
       query: 'is:open ((reviewer:self -owner:self -is:ignored) OR ' +
-          'assignee:self)',
+          'assignee:self) -is:wip',
     },
     {
       name: 'Recently closed',
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 cb304d8..b1cbe3e 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
@@ -523,7 +523,7 @@
           messages="[[_change.messages]]"
           reviewer-updates="[[_change.reviewer_updates]]"
           comments="[[_comments]]"
-          project-config="[[_projectConfig]]"
+          project-name="[[_change.project]]"
           show-reply-buttons="[[_loggedIn]]"
           on-reply="_handleMessageReply"></gr-messages-list>
     </div>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index 9094933..2b7b0fd 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -18,6 +18,7 @@
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -76,7 +77,7 @@
               class="message"
               no-trailing-margin
               content="[[comment.message]]"
-              config="[[projectConfig.commentlinks]]"></gr-formatted-text>
+              config="[[commentLinks]]"></gr-formatted-text>
         </div>
       </template>
     </template>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index d8cdb02..703f386 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -30,7 +30,8 @@
       changeNum: Number,
       comments: Object,
       patchNum: Number,
-      projectConfig: Object,
+      commentLinks: Object,
+      projectName: String,
     },
 
     _computeFilesFromComments(comments) {
@@ -39,8 +40,8 @@
     },
 
     _computeFileDiffURL(file, changeNum, patchNum) {
-      return this.getBaseUrl() + '/c/' + changeNum + '/' + patchNum + '/' +
-          this.encodeURL(file);
+      return Gerrit.Nav.getUrlForDiffById(this.changeNum, this.projectName,
+          file, patchNum);
     },
 
     _computeFileDisplayName(path) {
@@ -57,13 +58,8 @@
     },
 
     _computeDiffLineURL(file, changeNum, patchNum, comment) {
-      let diffURL = this._computeFileDiffURL(file, changeNum, patchNum);
-      if (comment.line) {
-        diffURL += '#';
-        if (this._isOnParent(comment)) { diffURL += 'b'; }
-        diffURL += comment.line;
-      }
-      return diffURL;
+      return Gerrit.Nav.getUrlForDiffById(this.changeNum, this.projectName,
+          file, patchNum, null, comment.line, this._isOnParent(comment));
     },
 
     _computeCommentsForFile(comments, file) {
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
index d25664e..ed1ece6 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -60,12 +60,6 @@
       assert.deepEqual(element._computeFilesFromComments(null), []);
     });
 
-    test('_computeFileDiffURL', () => {
-      const expected = '/c/change/patch/file';
-      const actual = element._computeFileDiffURL('file', 'change', 'patch');
-      assert.equal(actual, expected);
-    });
-
     test('_computeFileDisplayName', () => {
       assert.equal(element._computeFileDisplayName('/COMMIT_MSG'),
           'Commit message');
@@ -75,27 +69,6 @@
           '/foo/bar/baz');
     });
 
-    test('_computeDiffLineURL', () => {
-      const comment = {line: 123, side: 'REVISION', patch_set: 10};
-      let expected = '/c/change/patch/file#123';
-      let actual = element._computeDiffLineURL('file', 'change', 'patch',
-          comment);
-      assert.equal(actual, expected);
-
-      comment.line = 321;
-      comment.side = 'PARENT';
-
-      expected = '/c/change/patch/file#b321';
-      actual = element._computeDiffLineURL('file', 'change', 'patch', comment);
-    });
-
-    test('_computeDiffLineURL encoding', () => {
-      const comment = {line: 123, side: 'REVISION', patch_set: 10};
-      const expected = '/c/123/2/x%252By.md#123';
-      const actual = element._computeDiffLineURL('x+y.md', '123', '2', comment);
-      assert.equal(actual, expected);
-    });
-
     test('_computePatchDisplayName', () => {
       const comment = {line: 123, side: 'REVISION', patch_set: 10};
 
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index d034f16..78f59db 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -152,12 +152,13 @@
             <gr-formatted-text
                 class="message hideOnCollapsed"
                 content="[[message.message]]"
-                config="[[projectConfig.commentlinks]]"></gr-formatted-text>
+                config="[[_commentLinks]]"></gr-formatted-text>
             <gr-comment-list
                 comments="[[comments]]"
                 change-num="[[changeNum]]"
                 patch-num="[[message._revision_number]]"
-                project-config="[[projectConfig]]"></gr-comment-list>
+                project-name="[[projectName]]"
+                comment-links="[[_commentLinks]]"></gr-comment-list>
           </div>
         </template>
         <template is="dom-if" if="[[_computeIsReviewerUpdate(message)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 3433201..2593869 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -74,7 +74,11 @@
         type: Boolean,
         computed: '_computeShowReplyButton(message, _loggedIn)',
       },
-      projectConfig: Object,
+      projectName: {
+        type: String,
+        observer: '_projectNameChanged',
+      },
+      _commentLinks: Object,
       // Computed property needed to trigger Polymer value observing.
       _expanded: {
         type: Object,
@@ -240,5 +244,11 @@
 
       return this.getAnonymousName(this.config);
     },
+
+    _projectNameChanged(name) {
+      this.$.restAPI.getProjectConfig(name).then(config => {
+        this._commentLinks = config.commentlinks;
+      });
+    },
   });
 })();
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 95ca60e..2eed88b 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
@@ -95,7 +95,7 @@
           message="[[message]]"
           comments="[[_computeCommentsForMessage(comments, message)]]"
           hide-automated="[[_hideAutomated]]"
-          project-config="[[projectConfig]]"
+          project-name="[[projectName]]"
           show-reply-button="[[showReplyButtons]]"
           on-scroll-to="_handleScrollTo"
           data-message-id$="[[message.id]]"></gr-message>
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 ebdb4dc..58e56a4 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
@@ -36,7 +36,7 @@
         value() { return []; },
       },
       comments: Object,
-      projectConfig: Object,
+      projectName: String,
       showReplyButtons: {
         type: Boolean,
         value: false,
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index dd9709d..3fc2c22 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -43,6 +43,10 @@
     //    - `basePatchNum`, optional, Number, the patch for the left-hand-side
     //        of the diff. If `basePatchNum` is provided, then `patchNum` must
     //        also be provided.
+    //    - `lineNum`, optional, Number, the line number to be selected on load.
+    //    - `leftSide`, optional, Boolean, if a `lineNum` is provided, a value
+    //        of true selects the line from base of the patch range. False by
+    //        default.
 
     window.Gerrit = window.Gerrit || {};
 
@@ -166,9 +170,9 @@
 
       /**
        * @param {!Object} change The change object.
-       * @param {number} opt_patchNum
-       * @param {number|string} opt_basePatchNum The string 'PARENT' can be used
-       *     for none.
+       * @param {number=} opt_patchNum
+       * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+       *     used for none.
        * @return {string}
        */
       getUrlForChange(change, opt_patchNum, opt_basePatchNum) {
@@ -187,9 +191,9 @@
 
       /**
        * @param {!Object} change The change object.
-       * @param {number} opt_patchNum
-       * @param {number|string} opt_basePatchNum The string 'PARENT' can be used
-       *     for none.
+       * @param {number=} opt_patchNum
+       * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+       *     used for none.
        * @return {string}
        */
       navigateToChange(change, opt_patchNum, opt_basePatchNum) {
@@ -199,13 +203,30 @@
 
       /**
        * @param {!Object} change The change object.
-       * @param {!String} path The file path.
-       * @param {number} opt_patchNum
-       * @param {number|string} opt_basePatchNum The string 'PARENT' can be used
-       *     for none.
+       * @param {!string} path The file path.
+       * @param {number=} opt_patchNum
+       * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+       *     used for none.
        * @return {string}
        */
       getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum) {
+        return this.getUrlForDiffById(change._number, change.project, path,
+            opt_patchNum, opt_basePatchNum);
+      },
+
+      /**
+       * @param {!number} change The change object.
+       * @param {!string} projectName The name of the project.
+       * @param {!string} path The file path.
+       * @param {number=} opt_patchNum
+       * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+       *     used for none.
+       * @param {number=} opt_lineNum
+       * @param {boolean=} opt_leftSide
+       * @return {string}
+       */
+      getUrlForDiffById(changeNum, projectName, path, opt_patchNum,
+          opt_basePatchNum, opt_lineNum, opt_leftSide) {
         if (opt_basePatchNum === PARENT_PATCHNUM) {
           opt_basePatchNum = undefined;
         }
@@ -213,19 +234,22 @@
         this._checkPatchRange(opt_patchNum, opt_basePatchNum);
         return this._getUrlFor({
           view: Gerrit.Nav.View.DIFF,
-          changeNum: change._number,
+          changeNum,
+          projectName,
           path,
           patchNum: opt_patchNum,
           basePatchNum: opt_basePatchNum,
+          lineNum: opt_lineNum,
+          leftSide: opt_leftSide,
         });
       },
 
       /**
        * @param {!Object} change The change object.
-       * @param {!String} path The file path.
-       * @param {number} opt_patchNum
-       * @param {number|string} opt_basePatchNum The string 'PARENT' can be used
-       *     for none.
+       * @param {!string} path The file path.
+       * @param {number=} opt_patchNum
+       * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+       *     used for none.
        */
       navigateToDiff(change, path, opt_patchNum, opt_basePatchNum) {
         this._navigate(this.getUrlForDiff(change, path, opt_patchNum,
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 f290dd2..f393445 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -580,6 +580,12 @@
         if (range.length) { range = '/' + range; }
 
         url = `/c/${params.changeNum}${range}/${encode(params.path, true)}`;
+
+        if (params.lineNum) {
+          url += '#';
+          if (params.leftSide) { url += 'b'; }
+          url += params.lineNum;
+        }
       } else {
         throw new Error('Can\'t generate');
       }
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 ff1a0ca..92d0fde 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
@@ -93,6 +93,13 @@
         delete params.basePatchNum;
         assert.equal(element._generateUrl(params),
             '/c/42/2/foo+bar/my%252Bfile.txt%2525');
+
+        params.path = 'file.cpp';
+        params.lineNum = 123;
+        assert.equal(element._generateUrl(params), '/c/42/2/file.cpp#123');
+
+        params.leftSide = true;
+        assert.equal(element._generateUrl(params), '/c/42/2/file.cpp#b123');
       });
     });
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
index ca385c2..a1c80ae 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
@@ -37,8 +37,12 @@
         width: 100%;
         will-change: top;
       }
+      .fixedAtTop {
+        border-bottom: 1px solid #a4a4a4;
+        box-shadow: 0 4px 4px rgba(0,0,0,0.1);
+      }
     </style>
-    <header id="header" class$="[[_computeHeaderClass(_headerFloating)]]">
+    <header id="header" class$="[[_computeHeaderClass(_headerFloating, _topLast)]]">
       <content></content>
     </header>
   </template>
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
index dfbaa8a..f4f1a93 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -31,8 +31,17 @@
         type: Boolean,
         value: false,
       },
+
+      /**
+       * Initial offset from the top of the document, in pixels.
+       */
       _topInitial: Number,
+
+      /**
+       * Current offset from the top of the window, in pixels.
+       */
       _topLast: Number,
+
       _headerHeight: Number,
       _headerFloating: {
         type: Boolean,
@@ -73,8 +82,12 @@
       }
     },
 
-    _computeHeaderClass(headerFloating) {
-      return headerFloating ? 'floating' : '';
+    _computeHeaderClass(headerFloating, topLast) {
+      const fixedAtTop = this.keepOnScroll && topLast === 0;
+      return [
+        headerFloating ? 'floating' : '',
+        fixedAtTop ? 'fixedAtTop' : '',
+      ].join(' ');
     },
 
     _getScrollY() {
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
index d813a44..408b7c9 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
@@ -98,6 +98,14 @@
         emulateScrollY(120);
         assert.equal(getHeaderTop(), '0px');
       });
+
+      test('drops a shadow when fixed to the top', () => {
+        element.keepOnScroll = true;
+        emulateScrollY(5);
+        assert.isFalse(element.$.header.classList.contains('fixedAtTop'));
+        emulateScrollY(120);
+        assert.isTrue(element.$.header.classList.contains('fixedAtTop'));
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
new file mode 100644
index 0000000..e99ea20
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
@@ -0,0 +1,172 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function(window) {
+  'use strict';
+
+  // Prevent redefinition.
+  if (window.Gerrit.Auth) { return; }
+
+  const MAX_GET_TOKEN_RETRIES = 2;
+
+  Gerrit.Auth = {
+    TYPE: {
+      XSRF_TOKEN: 'xsrf_token',
+      ACCESS_TOKEN: 'access_token',
+    },
+
+    _type: null,
+    _cachedTokenPromise: null,
+    _defaultOptions: {},
+    _retriesLeft: MAX_GET_TOKEN_RETRIES,
+
+    _getToken() {
+      return Promise.resolve(this._cachedTokenPromise);
+    },
+
+    /**
+     * Enable cross-domain authentication using OAuth access token.
+     *
+     * @param {
+     *   function(): !Promise<{
+     *     access_token: string,
+     *     expires_at: number
+     *   }>
+     * } getToken
+     * @param {?{credentials:string}} defaultOptions
+     */
+    setup(getToken, defaultOptions) {
+      this._retriesLeft = MAX_GET_TOKEN_RETRIES;
+      if (getToken) {
+        this._type = Gerrit.Auth.TYPE.ACCESS_TOKEN;
+        this._cachedTokenPromise = null;
+        this._getToken = getToken;
+      }
+      this._defaultOptions = {};
+      if (defaultOptions) {
+        for (const p of ['credentials']) {
+          this._defaultOptions[p] = defaultOptions[p];
+        }
+      }
+    },
+
+    /**
+     * Perform network fetch with authentication.
+     *
+     * @param {string} url
+     * @param {Object=} opt_options
+     * @return {!Promise<!Response>}
+     */
+    fetch(url, opt_options) {
+      const options = Object.assign({}, this._defaultOptions, opt_options);
+      if (this._type === Gerrit.Auth.TYPE.ACCESS_TOKEN) {
+        return this._getAccessToken().then(
+            accessToken => this._fetchWithAccessToken(url, options, accessToken)
+        );
+      } else {
+        return this._fetchWithXsrfToken(url, options);
+      }
+    },
+
+    _getCookie(name) {
+      const key = name + '=';
+      let result = '';
+      document.cookie.split(';').some(c => {
+        c = c.trim();
+        if (c.startsWith(key)) {
+          result = c.substring(key.length);
+          return true;
+        }
+      });
+      return result;
+    },
+
+    _isTokenValid(token) {
+      if (!token) { return false; }
+      if (!token.access_token || !token.expires_at) { return false; }
+
+      const expiration = new Date(parseInt(token.expires_at, 10) * 1000);
+      if (Date.now() >= expiration.getTime()) { return false; }
+
+      return true;
+    },
+
+    _fetchWithXsrfToken(url, options) {
+      if (options.method && options.method !== 'GET') {
+        const token = this._getCookie('XSRF_TOKEN');
+        if (token) {
+          options.headers = options.headers || new Headers();
+          options.headers.append('X-Gerrit-Auth', token);
+        }
+      }
+      options.credentials = 'same-origin';
+      return fetch(url, options);
+    },
+
+    /**
+     * @return {!Promise<string>}
+     */
+    _getAccessToken() {
+      if (!this._cachedTokenPromise) {
+        this._cachedTokenPromise = this._getToken();
+      }
+      return this._cachedTokenPromise.then(token => {
+        if (this._isTokenValid(token)) {
+          this._retriesLeft = MAX_GET_TOKEN_RETRIES;
+          return token.access_token;
+        }
+        if (this._retriesLeft > 0) {
+          this._retriesLeft--;
+          this._cachedTokenPromise = null;
+          return this._getAccessToken();
+        }
+        // Fall back to anonymous access.
+        return null;
+      });
+    },
+
+    _fetchWithAccessToken(url, options, accessToken) {
+      const method = options.method || 'GET';
+      const params = [];
+      if (accessToken) {
+        params.push(`access_token=${accessToken}`);
+        const baseUrl = Gerrit.BaseUrlBehavior.getBaseUrl();
+        const pathname = baseUrl ?
+              url.substring(url.indexOf(baseUrl) + baseUrl.length) : url;
+        if (!pathname.startsWith('/a/')) {
+          url = url.replace(pathname, '/a' + pathname);
+        }
+      }
+      let contentType = options.headers && options.headers.get('Content-Type');
+      if (contentType) {
+        options.headers.set('Content-Type', 'text/plain');
+      }
+      if (method !== 'GET') {
+        options.method = 'POST';
+        params.push(`$m=${method}`);
+        if (!contentType) {
+          contentType = 'application/json';
+        }
+      }
+      if (contentType) {
+        params.push(`$ct=${encodeURIComponent(contentType)}`);
+      }
+      if (params.length) {
+        url = url + (url.indexOf('?') === -1 ? '?' : '&') + params.join('&');
+      }
+      return fetch(url, options);
+    },
+  };
+
+  window.Gerrit.Auth = Gerrit.Auth;
+})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
new file mode 100644
index 0000000..7bbc0ca
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
@@ -0,0 +1,171 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-auth</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+
+<script src="gr-auth.js"></script>
+
+<script>
+  suite('gr-auth', () => {
+    let auth;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
+      auth = Gerrit.Auth;
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    suite('default (xsrf token header)', () => {
+      test('GET', () => {
+        return auth.fetch('/url', {bar: 'bar'}).then(() => {
+          const [url, options] = fetch.lastCall.args;
+          assert.equal(url, '/url');
+          assert.equal(options.credentials, 'same-origin');
+        });
+      });
+
+      test('POST', () => {
+        sandbox.stub(auth, '_getCookie')
+            .withArgs('XSRF_TOKEN')
+            .returns('foobar');
+        return auth.fetch('/url', {method: 'POST'}).then(() => {
+          const [url, options] = fetch.lastCall.args;
+          assert.equal(url, '/url');
+          assert.equal(options.credentials, 'same-origin');
+          assert.equal(options.headers.get('X-Gerrit-Auth'), 'foobar');
+        });
+      });
+    });
+
+    suite('cors (access token)', () => {
+      let getToken;
+
+      const makeToken = opt_accessToken => {
+        return {
+          access_token: opt_accessToken || 'zbaz',
+          expires_at: new Date(Date.now() + 10e8).getTime(),
+        };
+      };
+
+      setup(() => {
+        getToken = sandbox.stub();
+        getToken.returns(Promise.resolve(makeToken()));
+        auth.setup(getToken);
+      });
+
+      test('base url support', () => {
+        const baseUrl = 'http://foo';
+        sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns(baseUrl);
+        return auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => {
+          const [url] = fetch.lastCall.args;
+          assert.equal(url, 'http://foo/a/url?access_token=zbaz');
+        });
+      });
+
+      test('fetch not signed in', () => {
+        getToken.returns(Promise.resolve());
+        return auth.fetch('/url', {bar: 'bar'}).then(() => {
+          const [url, options] = fetch.lastCall.args;
+          assert.equal(url, '/url');
+          assert.equal(options.bar, 'bar');
+          assert.isUndefined(options.headers);
+        });
+      });
+
+      test('fetch signed in', () => {
+        return auth.fetch('/url', {bar: 'bar'}).then(() => {
+          const [url, options] = fetch.lastCall.args;
+          assert.equal(url, '/a/url?access_token=zbaz');
+          assert.equal(options.bar, 'bar');
+        });
+      });
+
+      test('getToken calls are cached', () => {
+        return Promise.all([
+          auth.fetch('/url-one'), auth.fetch('/url-two')]).then(() => {
+            assert.equal(getToken.callCount, 1);
+          });
+      });
+
+      test('getToken refreshes token', () => {
+        sandbox.stub(auth, '_isTokenValid');
+        auth._isTokenValid
+            .onFirstCall().returns(true)
+            .onSecondCall().returns(false)
+            .onThirdCall().returns(true);
+        return auth.fetch('/url-one').then(() => {
+          getToken.returns(Promise.resolve(makeToken('bzzbb')));
+          return auth.fetch('/url-two');
+        }).then(() => {
+          const [[firstUrl], [secondUrl]] = fetch.args;
+          assert.equal(firstUrl, '/a/url-one?access_token=zbaz');
+          assert.equal(secondUrl, '/a/url-two?access_token=bzzbb');
+        });
+      });
+
+      test('signed in token error falls back to anonymous', () => {
+        getToken.returns(Promise.resolve('rubbish'));
+        return auth.fetch('/url', {bar: 'bar'}).then(() => {
+          const [url, options] = fetch.lastCall.args;
+          assert.equal(url, '/url');
+          assert.equal(options.bar, 'bar');
+        });
+      });
+
+      test('_isTokenValid', () => {
+        assert.isFalse(auth._isTokenValid());
+        assert.isFalse(auth._isTokenValid({}));
+        assert.isFalse(auth._isTokenValid({access_token: 'foo'}));
+        assert.isFalse(auth._isTokenValid({
+          access_token: 'foo',
+          expires_at: Date.now()/1000 - 1,
+        }));
+        assert.isTrue(auth._isTokenValid({
+          access_token: 'foo',
+          expires_at: Date.now()/1000 + 1,
+        }));
+      });
+
+      test('HTTP PUT', () => {
+        const originalOptions = {
+          method: 'PUT',
+          headers: new Headers({'Content-Type': 'mail/pigeon'}),
+        };
+        return auth.fetch('/url', originalOptions).then(() => {
+          assert.isTrue(getToken.called);
+          const [url, options] = fetch.lastCall.args;
+          assert.include(url, '$ct=mail%2Fpigeon');
+          assert.include(url, '$m=PUT');
+          assert.include(url, 'access_token=zbaz');
+          assert.equal(options.method, 'POST');
+          assert.equal(options.headers.get('Content-Type'), 'text/plain');
+        });
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth.js
deleted file mode 100644
index 8cf2096..0000000
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth.js
+++ /dev/null
@@ -1,197 +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(window) {
-  'use strict';
-
-  // Prevent redefinition.
-  if (window.GrGapiAuth) { return; }
-
-  const EMAIL_SCOPE = 'email';
-
-  function GrGapiAuth() {}
-
-  GrGapiAuth._loadGapiPromise = null;
-  GrGapiAuth._setupPromise = null;
-  GrGapiAuth._refreshTokenPromise = null;
-  GrGapiAuth._sharedAuthToken = null;
-  GrGapiAuth._oauthClientId = null;
-  GrGapiAuth._oauthEmail = null;
-
-  GrGapiAuth.prototype.fetch = function(url, options) {
-    options = Object.assign({}, options);
-    return this._getAccessToken().then(
-        token => window.FASTER_GERRIT_CORS ?
-            this._fasterGerritCors(url, options, token) :
-            this._defaultFetch(url, options, token)
-    );
-  };
-
-  GrGapiAuth.prototype._defaultFetch = function(url, options, token) {
-    if (token) {
-      options.headers = options.headers || new Headers();
-      options.headers.append('Authorization', `Bearer ${token}`);
-      const baseUrl = Gerrit.BaseUrlBehavior.getBaseUrl();
-      const pathname = baseUrl ?
-            url.substring(url.indexOf(baseUrl) + baseUrl.length) : url;
-      if (!pathname.startsWith('/a/')) {
-        url = url.replace(pathname, '/a' + pathname);
-      }
-    }
-    return fetch(url, options);
-  };
-
-  GrGapiAuth.prototype._fasterGerritCors = function(url, options, token) {
-    const method = options.method || 'GET';
-    if (method === 'GET') {
-      return fetch(url, options);
-    }
-    const params = [];
-    if (token) {
-      params.push(`access_token=${token}`);
-    }
-    const contentType = options.headers && options.headers.get('Content-Type');
-    if (contentType) {
-      options.headers.set('Content-Type', 'text/plain');
-      params.push(`$ct=${encodeURIComponent(contentType)}`);
-    }
-    params.push(`$m=${method}`);
-    url = url + (url.indexOf('?') === -1 ? '?' : '') + params.join('&');
-    options.method = 'POST';
-    return fetch(url, options);
-  };
-
-  GrGapiAuth.prototype._getAccessToken = function() {
-    if (this._isTokenValid(GrGapiAuth._sharedAuthToken)) {
-      return Promise.resolve(GrGapiAuth._sharedAuthToken.access_token);
-    }
-    if (!GrGapiAuth._refreshTokenPromise) {
-      GrGapiAuth._refreshTokenPromise = this._loadGapi()
-        .then(() => this._configureOAuthLibrary())
-        .then(() => this._refreshToken())
-        .then(token => {
-          GrGapiAuth._sharedAuthToken = token;
-          GrGapiAuth._refreshTokenPromise = null;
-          return this._getAccessToken();
-        }).catch(err => {
-          console.error(err);
-        });
-    }
-    return GrGapiAuth._refreshTokenPromise;
-  };
-
-  GrGapiAuth.prototype._isTokenValid = function(token) {
-    if (!token) { return false; }
-    if (!token.access_token || !token.expires_at) { return false; }
-
-    const expiration = new Date(parseInt(token.expires_at, 10) * 1000);
-    if (Date.now() >= expiration.getTime()) { return false; }
-
-    return true;
-  };
-
-  GrGapiAuth.prototype._loadGapi = function() {
-    if (!GrGapiAuth._loadGapiPromise) {
-      GrGapiAuth._loadGapiPromise = new Promise((resolve, reject) => {
-        const scriptEl = document.createElement('script');
-        scriptEl.defer = true;
-        scriptEl.async = true;
-        scriptEl.src = 'https://apis.google.com/js/platform.js';
-        scriptEl.onerror = reject;
-        scriptEl.onload = resolve;
-        document.body.appendChild(scriptEl);
-      });
-    }
-    return GrGapiAuth._loadGapiPromise;
-  };
-
-  GrGapiAuth.prototype._configureOAuthLibrary = function() {
-    if (!GrGapiAuth._setupPromise) {
-      GrGapiAuth._setupPromise = new Promise(
-        resolve => gapi.load('config_min', resolve)
-      )
-        .then(() => this._getOAuthConfig())
-        .then(config => {
-          if (config.hasOwnProperty('auth_url') && config.auth_url) {
-            gapi.config.update('oauth-flow/authUrl', config.auth_url);
-          }
-          if (config.hasOwnProperty('proxy_url') && config.proxy_url) {
-            gapi.config.update('oauth-flow/proxyUrl', config.proxy_url);
-          }
-          GrGapiAuth._oauthClientId = config.client_id;
-          GrGapiAuth._oauthEmail = config.email;
-
-          // Loading auth has a side-effect. The URLs should be set before
-          // loading it.
-          return new Promise(
-            resolve => gapi.load('auth', () => gapi.auth.init(resolve))
-          );
-        });
-    }
-    return GrGapiAuth._setupPromise;
-  };
-
-  GrGapiAuth.prototype._refreshToken = function() {
-    const opts = {
-      client_id: GrGapiAuth._oauthClientId,
-      immediate: true,
-      scope: EMAIL_SCOPE,
-      login_hint: GrGapiAuth._oauthEmail,
-    };
-    return new Promise((resolve, reject) => {
-      gapi.auth.authorize(opts, token => {
-        if (!token) {
-          reject('No token returned');
-        } else if (token.error) {
-          reject(token.error);
-        } else {
-          resolve(token);
-        }
-      });
-    });
-  };
-
-  GrGapiAuth.prototype._getOAuthConfig = function() {
-    const baseUrl = Gerrit.BaseUrlBehavior.getBaseUrl();
-    const authConfigURL = baseUrl + '/accounts/self/oauthconfig';
-    const opts = {
-      headers: new Headers({Accept: 'application/json'}),
-      credentials: 'same-origin',
-    };
-    return fetch(authConfigURL, opts).then(response => {
-      if (!response.ok) {
-        console.error(response.statusText);
-        if (response.body && response.body.then) {
-          return response.body.then(text => {
-            return Promise.reject(text);
-          });
-        }
-        if (response.statusText) {
-          return Promise.reject(response.statusText);
-        } else {
-          return Promise.reject('_getOAuthConfig' + response.status);
-        }
-      }
-      return this._getResponseObject(response);
-    });
-  };
-
-  GrGapiAuth.prototype._getResponseObject = function(response) {
-    const JSON_PREFIX = ')]}\'';
-    return response.text().then(text => {
-      return JSON.parse(text.substring(JSON_PREFIX.length));
-    });
-  },
-
-  window.GrGapiAuth = GrGapiAuth;
-})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth_test.html
deleted file mode 100644
index f5c0d18..0000000
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth_test.html
+++ /dev/null
@@ -1,218 +0,0 @@
-<!DOCTYPE html>
-<!--
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-gapi-auth</title>
-
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-
-<script src="gr-gapi-auth.js"></script>
-
-<script>
-  suite('gr-rest-api-interface tests', () => {
-    let auth;
-    let sandbox;
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-      auth = new GrGapiAuth();
-      window.gapi = {
-        load: sandbox.stub().callsArg(1),
-        config: {
-          update: sandbox.stub(),
-        },
-        auth: {
-          init: sandbox.stub().callsArg(0),
-          authorize: sandbox.stub(),
-        },
-      };
-      sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
-    });
-
-    teardown(() => {
-      delete window.gapi;
-      sandbox.restore();
-    });
-
-    test('exists', () => {
-      assert.isOk(auth);
-    });
-
-    test('base url support', () => {
-      const baseUrl = 'http://foo';
-      sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns(baseUrl);
-      sandbox.stub(auth, '_getAccessToken').returns(Promise.resolve('baz'));
-      return auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => {
-        const [url] = fetch.lastCall.args;
-        assert.equal(url, 'http://foo/a/url');
-      });
-    });
-
-    test('fetch signed in', () => {
-      sandbox.stub(auth, '_getAccessToken').returns(Promise.resolve('foo'));
-      return auth.fetch('/url', {bar: 'bar'}).then(() => {
-        assert.isTrue(auth._getAccessToken.called);
-        const [url, options] = fetch.lastCall.args;
-        assert.equal(url, '/a/url');
-        assert.equal(options.bar, 'bar');
-        assert.equal(options.headers.get('Authorization'), 'Bearer foo');
-      });
-    });
-
-    test('fetch not signed in', () => {
-      sandbox.stub(auth, '_getAccessToken').returns(Promise.resolve());
-      return auth.fetch('/url', {bar: 'bar'}).then(() => {
-        assert.isTrue(auth._getAccessToken.called);
-        const [url, options] = fetch.lastCall.args;
-        assert.equal(url, '/url');
-        assert.equal(options.bar, 'bar');
-        assert.isUndefined(options.headers);
-      });
-    });
-
-    test('_getAccessToken returns valid shared token', () => {
-      GrGapiAuth._sharedAuthToken = {access_token: 'foo'};
-      sandbox.stub(auth, '_isTokenValid').returns(true);
-      return auth._getAccessToken().then(token => {
-        assert.equal(token, 'foo');
-      });
-    });
-
-    test('_getAccessToken refreshes token', () => {
-      const token = {access_token: 'foo'};
-      sandbox.stub(auth, '_loadGapi').returns(Promise.resolve());
-      sandbox.stub(auth, '_configureOAuthLibrary').returns(Promise.resolve());
-      sandbox.stub(auth, '_refreshToken').returns(Promise.resolve(token));
-      sandbox.stub(auth, '_isTokenValid').returns(true)
-          .onFirstCall().returns(false);
-      return auth._getAccessToken().then(token => {
-        assert.isTrue(auth._loadGapi.called);
-        assert.isTrue(auth._configureOAuthLibrary.called);
-        assert.isTrue(auth._refreshToken.called);
-        assert.equal(token, 'foo');
-      });
-    });
-
-    test('_isTokenValid', () => {
-      assert.isFalse(auth._isTokenValid());
-      assert.isFalse(auth._isTokenValid({}));
-      assert.isFalse(auth._isTokenValid({access_token: 'foo'}));
-      assert.isFalse(auth._isTokenValid({
-        access_token: 'foo',
-        expires_at: Date.now()/1000 - 1,
-      }));
-      assert.isTrue(auth._isTokenValid({
-        access_token: 'foo',
-        expires_at: Date.now()/1000 + 1,
-      }));
-    });
-
-    test('_configureOAuthLibrary', () => {
-      sandbox.stub(auth, '_getOAuthConfig').returns({
-        auth_url: 'some_auth_url',
-        proxy_url: 'some_proxy_url',
-        client_id: 'some_client_id',
-        email: 'some_email',
-      });
-      return auth._configureOAuthLibrary().then(() => {
-        assert.isTrue(gapi.load.calledWith('config_min'));
-        assert.isTrue(auth._getOAuthConfig.called);
-        assert.isTrue(gapi.config.update.calledWith(
-            'oauth-flow/authUrl', 'some_auth_url'));
-        assert.isTrue(gapi.config.update.calledWith(
-            'oauth-flow/proxyUrl', 'some_proxy_url'));
-        assert.equal(GrGapiAuth._oauthClientId, 'some_client_id');
-        assert.equal(GrGapiAuth._oauthEmail, 'some_email');
-        assert.isTrue(gapi.auth.init.called);
-        assert.isTrue(gapi.load.calledWith('auth'));
-      });
-    });
-
-    test('_refreshToken no token', () => {
-      gapi.auth.authorize.callsArgWith(1, null);
-      return auth._refreshToken().catch(reason => {
-        assert.equal(reason, 'No token returned');
-      });
-    });
-
-    test('_refreshToken error', () => {
-      gapi.auth.authorize.callsArgWith(1, {error: 'some error'});
-      return auth._refreshToken().catch(reason => {
-        assert.equal(reason, 'some error');
-      });
-    });
-
-    test('_refreshToken', () => {
-      const token = {};
-      gapi.auth.authorize.callsArgWith(1, token);
-      return auth._refreshToken().then(t => {
-        assert.strictEqual(token, t);
-      });
-    });
-
-    test('_getOAuthConfig', () => {
-      const config = {};
-      sandbox.stub(auth, '_getResponseObject').returns(config);
-      return auth._getOAuthConfig().then(c => {
-        const [url, options] = fetch.lastCall.args;
-        assert.equal(url, '/accounts/self/oauthconfig');
-        assert.equal(options.credentials, 'same-origin');
-        assert.equal(options.headers.get('Accept'), 'application/json');
-        assert.strictEqual(c, config);
-      });
-    });
-
-    test('BaseUrlBehavior', () => {
-      sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('http://foo');
-      sandbox.stub(auth, '_getResponseObject').returns({});
-      return auth._getOAuthConfig().then(c => {
-        const [url] = fetch.lastCall.args;
-        assert.equal(url, 'http://foo/accounts/self/oauthconfig');
-      });
-    });
-
-    suite('faster gerrit cors', () => {
-      setup(() => {
-        window.FASTER_GERRIT_CORS = true;
-      });
-
-      teardown(() => {
-        delete window.FASTER_GERRIT_CORS;
-      });
-
-      test('PUT works', () => {
-        sandbox.stub(auth, '_getAccessToken').returns(Promise.resolve('foo'));
-        const originalOptions = {
-          method: 'PUT',
-          headers: new Headers({'Content-Type': 'mail/pigeon'}),
-        };
-        return auth.fetch('/url', originalOptions).then(() => {
-          assert.isTrue(auth._getAccessToken.called);
-          const [url, options] = fetch.lastCall.args;
-          assert.include(url, '$ct=mail%2Fpigeon');
-          assert.include(url, '$m=PUT');
-          assert.include(url, 'access_token=foo');
-          assert.equal(options.method, 'POST');
-          assert.equal(options.headers.get('Content-Type'), 'text/plain');
-        });
-      });
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gerrit-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gerrit-auth.js
deleted file mode 100644
index b70790d..0000000
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gerrit-auth.js
+++ /dev/null
@@ -1,49 +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(window) {
-  'use strict';
-
-  // Prevent redefinition.
-  if (window.GrGerritAuth) { return; }
-
-  function GrGerritAuth() {}
-
-  GrGerritAuth.prototype._getCookie = function(name) {
-    const key = name + '=';
-    let result = '';
-    document.cookie.split(';').some(c => {
-      c = c.trim();
-      if (c.startsWith(key)) {
-        result = c.substring(key.length);
-        return true;
-      }
-    });
-    return result;
-  };
-
-  GrGerritAuth.prototype.fetch = function(url, opt_options) {
-    const options = Object.assign({}, opt_options);
-    if (options.method && options.method !== 'GET') {
-      const token = this._getCookie('XSRF_TOKEN');
-      if (token) {
-        options.headers = options.headers || new Headers();
-        options.headers.append('X-Gerrit-Auth', token);
-      }
-    }
-    options.credentials = 'same-origin';
-    return fetch(url, options);
-  };
-
-  window.GrGerritAuth = GrGerritAuth;
-})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index b8ed52a..5afed95 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -26,8 +26,7 @@
 
 <dom-module id="gr-rest-api-interface">
   <!-- NB: Order is important, because of namespaced classes. -->
-  <script src="gr-gerrit-auth.js"></script>
-  <script src="gr-gapi-auth.js"></script>
+  <script src="gr-auth.js"></script>
   <script src="gr-reviewer-updates-parser.js"></script>
   <script src="gr-rest-api-interface.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index f20255b..9317425 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
@@ -30,8 +30,6 @@
     SEND_DIFF_DRAFT: 'sendDiffDraft',
   };
 
-  let auth = null;
-
   Polymer({
     is: 'gr-rest-api-interface',
 
@@ -82,14 +80,14 @@
         type: Object,
         value: {}, // Intentional to share the object across instances.
       },
+      _auth: {
+        type: Object,
+        value: Gerrit.Auth, // Share across instances.
+      },
     },
 
     JSON_PREFIX,
 
-    created() {
-      auth = window.USE_GAPI_AUTH ? new GrGapiAuth() : new GrGerritAuth();
-    },
-
     /**
      * Fetch JSON from url provided.
      * Returns a Promise that resolves to a native Response.
@@ -104,7 +102,7 @@
     _fetchRawJSON(url, opt_errFn, opt_cancelCondition, opt_params,
         opt_options) {
       const urlWithParams = this._urlWithParams(url, opt_params);
-      return auth.fetch(urlWithParams, opt_options).then(response => {
+      return this._auth.fetch(urlWithParams, opt_options).then(response => {
         if (opt_cancelCondition && opt_cancelCondition()) {
           response.body.cancel();
           return;
@@ -984,23 +982,24 @@
         }
         options.body = opt_body;
       }
-      return auth.fetch(this.getBaseUrl() + url, options).then(response => {
-        if (!response.ok) {
-          if (opt_errFn) {
-            return opt_errFn.call(opt_ctx || null, response);
-          }
-          this.fire('server-error', {response});
-        }
+      return this._auth.fetch(this.getBaseUrl() + url, options)
+          .then(response => {
+            if (!response.ok) {
+              if (opt_errFn) {
+                return opt_errFn.call(opt_ctx || null, response);
+              }
+              this.fire('server-error', {response});
+            }
 
-        return response;
-      }).catch(err => {
-        this.fire('network-error', {error: err});
-        if (opt_errFn) {
-          return opt_errFn.call(opt_ctx, null, err);
-        } else {
-          throw err;
-        }
-      });
+            return response;
+          }).catch(err => {
+            this.fire('network-error', {error: err});
+            if (opt_errFn) {
+              return opt_errFn.call(opt_ctx, null, err);
+            } else {
+              throw err;
+            }
+          });
     },
 
     getDiff(changeNum, basePatchNum, patchNum, path,
@@ -1187,7 +1186,7 @@
     },
 
     _fetchB64File(url) {
-      return auth.fetch(this.getBaseUrl() + url)
+      return this._auth.fetch(this.getBaseUrl() + url)
           .then(response => {
             if (!response.ok) { return Promise.reject(response.statusText); }
             const type = response.headers.get('X-FYI-Content-Type');
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 8248d5b..2cda640 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -736,19 +736,10 @@
           .calledWithExactly('/groups/?n=26&S=0&r=%5Etest.*'));
     });
 
-    test('gerrit auth is used by default', () => {
-      sandbox.stub(GrGerritAuth.prototype, 'fetch').returns(Promise.resolve());
+    test('gerrit auth is used', () => {
+      sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve());
       element.fetchJSON('foo');
-      assert(GrGerritAuth.prototype.fetch.called);
-    });
-
-    test('gapi auth is enabled with USE_GAPI_AUTH', () => {
-      window.USE_GAPI_AUTH = true;
-      sandbox.stub(GrGapiAuth.prototype, 'fetch').returns(Promise.resolve());
-      element = fixture('basic');
-      element.fetchJSON('foo');
-      assert(GrGapiAuth.prototype.fetch.called);
-      delete window.USE_GAPI_AUTH;
+      assert(Gerrit.Auth.fetch.called);
     });
 
     test('getSuggestedAccounts does not return fetchJSON', () => {
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index ed898f9..c9035be 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -38,6 +38,7 @@
     'admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html',
     'admin/gr-create-project-dialog/gr-create-project-dialog_test.html',
     'admin/gr-group/gr-group_test.html',
+    'admin/gr-group-audit-log/gr-group-audit-log_test.html',
     'admin/gr-plugin-list/gr-plugin-list_test.html',
     'admin/gr-project/gr-project_test.html',
     'admin/gr-project-detail-list/gr-project-detail-list_test.html',
@@ -129,7 +130,7 @@
     'shared/gr-linked-chip/gr-linked-chip_test.html',
     'shared/gr-linked-text/gr-linked-text_test.html',
     'shared/gr-list-view/gr-list-view_test.html',
-    'shared/gr-rest-api-interface/gr-gapi-auth_test.html',
+    'shared/gr-rest-api-interface/gr-auth_test.html',
     'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
     'shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html',
     'shared/gr-select/gr-select_test.html',