Merge changes Icdf4293f,I5eda9d41

* changes:
  Remove unused logger instance
  Rework SearchingChangeCacheImpl
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 699b2f9..6572816 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupDescription;
@@ -82,7 +83,6 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -277,17 +277,17 @@
   protected boolean testRequiresSsh;
   protected BlockStrategy noSleepBlockStrategy = t -> {}; // Don't sleep in tests.
 
-  @Inject private ChangeIndexCollection changeIndexes;
-  @Inject private AccountIndexCollection accountIndexes;
-  @Inject private ProjectIndexCollection projectIndexes;
-  @Inject private EventRecorder.Factory eventRecorderFactory;
-  @Inject private InProcessProtocol inProcessProtocol;
-  @Inject private Provider<AnonymousUser> anonymousUser;
-  @Inject private AccountIndexer accountIndexer;
-  @Inject private Groups groups;
-  @Inject private GroupIndexer groupIndexer;
   @Inject private AbstractChangeNotes.Args changeNotesArgs;
+  @Inject private AccountIndexCollection accountIndexes;
+  @Inject private AccountIndexer accountIndexer;
+  @Inject private ChangeIndexCollection changeIndexes;
+  @Inject private EventRecorder.Factory eventRecorderFactory;
+  @Inject private GroupIndexer groupIndexer;
+  @Inject private Groups groups;
+  @Inject private InProcessProtocol inProcessProtocol;
+  @Inject private ProjectIndexCollection projectIndexes;
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private ProjectResetter resetter;
   private List<Repository> toClose;
@@ -785,26 +785,23 @@
   }
 
   private Context newRequestContext(TestAccount account) {
-    return atrScope.newContext(
-        new SshSession(sshKeys, server, account), identifiedUserFactory.create(account.getId()));
+    requestScopeOperations.setApiUser(account.getId());
+    return atrScope.get();
   }
 
-  /**
-   * Enforce a new request context for the current API user.
-   *
-   * <p>This recreates the IdentifiedUser, hence everything which is cached in the IdentifiedUser is
-   * reloaded (e.g. the email addresses of the user).
-   */
+  @Deprecated // Tests should inject and use their own RequestScopeOperations.
   protected Context resetCurrentApiUser() {
-    return atrScope.set(newRequestContext(atrScope.get().getSession().getAccount()));
+    return requestScopeOperations.resetCurrentApiUser();
   }
 
+  @Deprecated // Tests should inject and use their own RequestScopeOperations.
   protected Context setApiUser(TestAccount account) {
-    return atrScope.set(newRequestContext(account));
+    return requestScopeOperations.setApiUser(account.getId());
   }
 
+  @Deprecated // Tests should inject and use their own RequestScopeOperations.
   protected Context setApiUserAnonymous() {
-    return atrScope.set(atrScope.newContext(null, anonymousUser.get()));
+    return requestScopeOperations.setApiUserAnonymous();
   }
 
   protected Account getAccount(Account.Id accountId) {
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index 8b79ee9..4877f05 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -26,6 +26,7 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
 import com.google.common.truth.Truth;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -41,6 +42,7 @@
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.FakeEmailSender.Message;
+import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -54,9 +56,11 @@
 import org.junit.Before;
 
 public abstract class AbstractNotificationTest extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Before
   public void enableReviewerByEmail() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     ConfigInput conf = new ConfigInput();
     conf.enableReviewerByEmail = InheritableBoolean.TRUE;
     gApi.projects().name(project.get()).config(conf);
@@ -82,7 +86,7 @@
     if (record) {
       accountsModifyingEmailStrategy.add(account);
     }
-    setApiUser(account);
+    requestScopeOperations.setApiUser(account.getId());
     GeneralPreferencesInfo prefs = gApi.accounts().self().getPreferences();
     prefs.emailStrategy = strategy;
     gApi.accounts().self().setPreferences(prefs);
@@ -356,7 +360,7 @@
         assignee = testAccount("assignee");
 
         watchingProjectOwner = testAccount("watchingProjectOwner", "Administrators");
-        setApiUser(watchingProjectOwner);
+        requestScopeOperations.setApiUser(watchingProjectOwner.getId());
         watch(allProjects.get(), pwi -> pwi.notifyNewChanges = true);
 
         for (NotifyType watch : NotifyType.values()) {
@@ -364,7 +368,7 @@
             continue;
           }
           TestAccount watcher = testAccount(watch.toString());
-          setApiUser(watcher);
+          requestScopeOperations.setApiUser(watcher.getId());
           watch(
               allProjects.get(),
               pwi -> {
@@ -457,7 +461,7 @@
       if (pushOptions != null) {
         ref = ref + '%' + Joiner.on(',').join(pushOptions);
       }
-      setApiUser(owner);
+      requestScopeOperations.setApiUser(owner.getId());
       repo = cloneProject(project, owner);
       PushOneCommit push = pushFactory.create(owner.getIdent(), repo);
       result = push.to(ref);
@@ -479,10 +483,10 @@
     StagedChange(String ref) throws Exception {
       super(ref);
 
-      setApiUser(starrer);
+      requestScopeOperations.setApiUser(starrer.getId());
       gApi.accounts().self().starChange(result.getChangeId());
 
-      setApiUser(owner);
+      requestScopeOperations.setApiUser(owner.getId());
       addReviewers(result);
       sender.clear();
     }
@@ -498,14 +502,14 @@
 
   protected StagedChange stageReviewableWipChange() throws Exception {
     StagedChange sc = stageReviewableChange();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     gApi.changes().id(sc.changeId).setWorkInProgress();
     return sc;
   }
 
   protected StagedChange stageAbandonedReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChange();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     gApi.changes().id(sc.changeId).abandon();
     sender.clear();
     return sc;
@@ -513,7 +517,7 @@
 
   protected StagedChange stageAbandonedReviewableWipChange() throws Exception {
     StagedChange sc = stageReviewableWipChange();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     gApi.changes().id(sc.changeId).abandon();
     sender.clear();
     return sc;
@@ -521,7 +525,7 @@
 
   protected StagedChange stageAbandonedWipChange() throws Exception {
     StagedChange sc = stageWipChange();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     gApi.changes().id(sc.changeId).abandon();
     sender.clear();
     return sc;
diff --git a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index 5abb0c0..8420ff2 100644
--- a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -55,7 +55,7 @@
       finished = p.finished;
     }
 
-    SshSession getSession() {
+    public SshSession getSession() {
       return session;
     }
 
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index dd28d20..f829842 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
 import static java.util.Objects.requireNonNull;
 import static org.apache.log4j.Logger.getLogger;
 
@@ -30,6 +31,8 @@
 import com.google.gerrit.acceptance.testsuite.group.GroupOperationsImpl;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperationsImpl;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperationsImpl;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.lucene.LuceneIndexModule;
@@ -48,10 +51,14 @@
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.SshMode;
 import com.google.inject.AbstractModule;
+import com.google.inject.BindingAnnotation;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
 import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
 import java.lang.reflect.Field;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -87,6 +94,11 @@
     }
   }
 
+  /** Marker on {@link InetSocketAddress} for test SSH server. */
+  @Retention(RUNTIME)
+  @BindingAnnotation
+  public @interface TestSshServerAddress {}
+
   @AutoValue
   public abstract static class Description {
     public static Description forTestClass(
@@ -502,12 +514,25 @@
             bind(AccountOperations.class).to(AccountOperationsImpl.class);
             bind(GroupOperations.class).to(GroupOperationsImpl.class);
             bind(ProjectOperations.class).to(ProjectOperationsImpl.class);
+            bind(RequestScopeOperations.class).to(RequestScopeOperationsImpl.class);
             factory(PushOneCommit.Factory.class);
             install(InProcessProtocol.module());
             install(new NoSshModule());
             install(new AsyncReceiveCommits.Module());
             factory(ProjectResetter.Builder.Factory.class);
           }
+
+          @Provides
+          @Singleton
+          @Nullable
+          @TestSshServerAddress
+          InetSocketAddress getSshAddress(@GerritServerConfig Config cfg) {
+            String addr = cfg.getString("sshd", null, "listenAddress");
+            // We do not use InitSshd.isOff to avoid coupling GerritServer to the SSH code.
+            return !"off".equalsIgnoreCase(addr)
+                ? SocketUtil.resolve(cfg.getString("sshd", null, "listenAddress"), 0)
+                : null;
+          }
         };
     return sysInjector.createChildInjector(module);
   }
@@ -532,7 +557,6 @@
   private ExecutorService daemonService;
   private Injector testInjector;
   private String url;
-  private InetSocketAddress sshdAddress;
   private InetSocketAddress httpAddress;
 
   private GerritServer(
@@ -550,12 +574,6 @@
     Config cfg = testInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
     url = cfg.getString("gerrit", null, "canonicalWebUrl");
     URI uri = URI.create(url);
-
-    String addr = cfg.getString("sshd", null, "listenAddress");
-    // We do not use InitSshd.isOff to avoid coupling GerritServer to the SSH code.
-    if (!"off".equalsIgnoreCase(addr)) {
-      sshdAddress = SocketUtil.resolve(cfg.getString("sshd", null, "listenAddress"), 0);
-    }
     httpAddress = new InetSocketAddress(uri.getHost(), uri.getPort());
   }
 
@@ -563,10 +581,6 @@
     return url;
   }
 
-  InetSocketAddress getSshdAddress() {
-    return sshdAddress;
-  }
-
   InetSocketAddress getHttpAddress() {
     return httpAddress;
   }
diff --git a/java/com/google/gerrit/acceptance/SshSession.java b/java/com/google/gerrit/acceptance/SshSession.java
index 52d7f28..fa0bc90 100644
--- a/java/com/google/gerrit/acceptance/SshSession.java
+++ b/java/com/google/gerrit/acceptance/SshSession.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.jcraft.jsch.ChannelExec;
 import com.jcraft.jsch.JSch;
@@ -35,9 +36,9 @@
   private Session session;
   private String error;
 
-  public SshSession(TestSshKeys sshKeys, GerritServer server, TestAccount account) {
+  public SshSession(TestSshKeys sshKeys, InetSocketAddress addr, TestAccount account) {
     this.sshKeys = sshKeys;
-    this.addr = server.getSshdAddress();
+    this.addr = addr;
     this.account = account;
   }
 
@@ -112,8 +113,14 @@
       JSch jsch = new JSch();
       jsch.addIdentity(
           "KeyPair", TestSshKeys.privateKey(keyPair), keyPair.getPublicKeyBlob(), null);
-      session =
-          jsch.getSession(account.username, addr.getAddress().getHostAddress(), addr.getPort());
+      String username =
+          account
+              .username()
+              .orElseThrow(
+                  () ->
+                      new IllegalStateException(
+                          "account " + account.accountId() + " must have a username to use SSH"));
+      session = jsch.getSession(username, addr.getAddress().getHostAddress(), addr.getPort());
       session.setConfig("StrictHostKeyChecking", "no");
       session.connect();
     }
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
new file mode 100644
index 0000000..a63d28a
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.request;
+
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
+import com.google.gerrit.reviewdb.client.Account;
+
+/**
+ * An aggregation of operations on Guice request scopes for test purposes.
+ *
+ * <p>To execute the operations, no Gerrit permissions are necessary.
+ */
+public interface RequestScopeOperations {
+  /**
+   * Sets the Guice request scope to the given account.
+   *
+   * <p>The resulting context has an SSH session attached. In order to use the SSH session returned
+   * by {@link AcceptanceTestRequestScope.Context#getSession()}, SSH must be enabled in the test and
+   * the account must have a username set. However, these are not requirements simply to call this
+   * method.
+   *
+   * @param accountId account ID. Must exist; throws an unchecked exception otherwise.
+   * @return the previous request scope.
+   */
+  AcceptanceTestRequestScope.Context setApiUser(Account.Id accountId);
+
+  /**
+   * Sets the Guice request scope to the given account.
+   *
+   * <p>The resulting context has an SSH session attached. In order to use the SSH session returned
+   * by {@link AcceptanceTestRequestScope.Context#getSession()}, SSH must be enabled in the test and
+   * the account must have a username set. However, these are not requirements simply to call this
+   * method.
+   *
+   * @param testAccount test account from {@code AccountOperations}.
+   * @return the previous request scope.
+   */
+  AcceptanceTestRequestScope.Context setApiUser(TestAccount testAccount);
+
+  /**
+   * Enforces a new request context for the current API user.
+   *
+   * <p>This recreates the {@code IdentifiedUser}, hence everything which is cached in the {@code
+   * IdentifiedUser} is reloaded (e.g. the email addresses of the user).
+   *
+   * <p>The current user must be an identified user.
+   *
+   * @return the previous request scope.
+   */
+  AcceptanceTestRequestScope.Context resetCurrentApiUser();
+
+  /**
+   * Sets the Guice request scope to the anonymous user.
+   *
+   * @return the previous request scope.
+   */
+  AcceptanceTestRequestScope.Context setApiUserAnonymous();
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
new file mode 100644
index 0000000..27b71b9
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.request;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
+import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.net.InetSocketAddress;
+
+/**
+ * The implementation of {@code RequestScopeOperations}.
+ *
+ * <p>There is only one implementation of {@code RequestScopeOperations}. Nevertheless, we keep the
+ * separation between interface and implementation to enhance clarity.
+ */
+@Singleton
+public class RequestScopeOperationsImpl implements RequestScopeOperations {
+  private final AcceptanceTestRequestScope atrScope;
+  private final AccountCache accountCache;
+  private final AccountOperations accountOperations;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final Provider<AnonymousUser> anonymousUserProvider;
+  private final InetSocketAddress sshAddress;
+  private final TestSshKeys testSshKeys;
+
+  @Inject
+  RequestScopeOperationsImpl(
+      AcceptanceTestRequestScope atrScope,
+      AccountCache accountCache,
+      AccountOperations accountOperations,
+      GenericFactory userFactory,
+      Provider<AnonymousUser> anonymousUserProvider,
+      @Nullable @TestSshServerAddress InetSocketAddress sshAddress,
+      TestSshKeys testSshKeys) {
+    this.atrScope = atrScope;
+    this.accountCache = accountCache;
+    this.accountOperations = accountOperations;
+    this.userFactory = userFactory;
+    this.anonymousUserProvider = anonymousUserProvider;
+    this.sshAddress = sshAddress;
+    this.testSshKeys = testSshKeys;
+  }
+
+  @Override
+  public AcceptanceTestRequestScope.Context setApiUser(Account.Id accountId) {
+    return setApiUser(accountOperations.account(accountId).get());
+  }
+
+  @Override
+  public AcceptanceTestRequestScope.Context setApiUser(TestAccount testAccount) {
+    return atrScope.set(
+        atrScope.newContext(
+            new SshSession(testSshKeys, sshAddress, testAccount),
+            createIdentifiedUser(testAccount.accountId())));
+  }
+
+  @Override
+  public AcceptanceTestRequestScope.Context resetCurrentApiUser() {
+    CurrentUser user = atrScope.get().getUser();
+    // More special cases for anonymous users etc. can be added as needed.
+    checkState(user.isIdentifiedUser(), "can only reset IdentifiedUser, not %s", user);
+    return setApiUser(user.getAccountId());
+  }
+
+  @Override
+  public AcceptanceTestRequestScope.Context setApiUserAnonymous() {
+    return atrScope.set(atrScope.newContext(null, anonymousUserProvider.get()));
+  }
+
+  private IdentifiedUser createIdentifiedUser(Account.Id accountId) {
+    return userFactory.create(
+        accountCache
+            .get(requireNonNull(accountId))
+            .orElseThrow(
+                () -> new IllegalArgumentException("account does not exist: " + accountId)));
+  }
+}
diff --git a/java/com/google/gerrit/index/query/InternalQuery.java b/java/com/google/gerrit/index/query/InternalQuery.java
index 3a4b372..ca5cc9b 100644
--- a/java/com/google/gerrit/index/query/InternalQuery.java
+++ b/java/com/google/gerrit/index/query/InternalQuery.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.stream.Collectors.toSet;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.gerrit.index.FieldDef;
@@ -27,6 +28,7 @@
 import com.google.gwtorm.server.OrmException;
 import java.util.Arrays;
 import java.util.List;
+import java.util.function.Supplier;
 
 /**
  * Execute a single query over a secondary index, for use by Gerrit internals.
@@ -38,7 +40,7 @@
  * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than
  * holding on to a single instance.
  */
-public class InternalQuery<T> {
+public class InternalQuery<T, Q extends InternalQuery<T, Q>> {
   private final QueryProcessor<T> queryProcessor;
   private final IndexCollection<?, T, ? extends Index<?, T>> indexes;
 
@@ -53,32 +55,46 @@
     this.indexConfig = indexConfig;
   }
 
-  public InternalQuery<T> setLimit(int n) {
+  @SuppressWarnings("unchecked")
+  protected final Q self() {
+    return (Q) this;
+  }
+
+  final Q setStart(int start) {
+    queryProcessor.setStart(start);
+    return self();
+  }
+
+  public final Q setLimit(int n) {
     queryProcessor.setUserProvidedLimit(n);
-    return this;
+    return self();
   }
 
-  public InternalQuery<T> enforceVisibility(boolean enforce) {
+  public final Q enforceVisibility(boolean enforce) {
     queryProcessor.enforceVisibility(enforce);
-    return this;
+    return self();
   }
 
-  @SuppressWarnings("unchecked") // Can't set @SafeVarargs on a non-final method.
-  public InternalQuery<T> setRequestedFields(FieldDef<T, ?>... fields) {
+  @SafeVarargs
+  public final Q setRequestedFields(FieldDef<T, ?>... fields) {
     checkArgument(fields.length > 0, "requested field list is empty");
     queryProcessor.setRequestedFields(
         Arrays.stream(fields).map(FieldDef::getName).collect(toSet()));
-    return this;
+    return self();
   }
 
-  public InternalQuery<T> noFields() {
+  public final Q noFields() {
     queryProcessor.setRequestedFields(ImmutableSet.of());
-    return this;
+    return self();
   }
 
-  public List<T> query(Predicate<T> p) throws OrmException {
+  public final List<T> query(Predicate<T> p) throws OrmException {
+    return queryResults(p).entities();
+  }
+
+  final QueryResult<T> queryResults(Predicate<T> p) throws OrmException {
     try {
-      return queryProcessor.query(p).entities();
+      return queryProcessor.query(p);
     } catch (QueryParseException e) {
       throw new OrmException(e);
     }
@@ -94,7 +110,7 @@
    * @return results of the queries, one list of results per input query, in the same order as the
    *     input.
    */
-  public List<List<T>> query(List<Predicate<T>> queries) throws OrmException {
+  public final List<List<T>> query(List<Predicate<T>> queries) throws OrmException {
     try {
       return Lists.transform(queryProcessor.query(queries), QueryResult::entities);
     } catch (QueryParseException e) {
@@ -102,8 +118,52 @@
     }
   }
 
-  protected Schema<T> schema() {
+  protected final Schema<T> schema() {
     Index<?, T> index = indexes != null ? indexes.getSearchIndex() : null;
     return index != null ? index.getSchema() : null;
   }
+
+  /**
+   * Query a predicate repeatedly until all results are exhausted.
+   *
+   * <p>Capable of iterating through all results regardless of limits. The passed {@code
+   * querySupplier} may choose to pre-set limits or not; this only affects the number of queries
+   * that may be issued, not the size of the final results.
+   *
+   * <p>Since multiple queries may be issued, this method is subject to races when the result set
+   * changes mid-iteration. This may result in skipped results, if an entity gets modified to jump
+   * to the front of the list after this method has passed it. It may also result in duplicate
+   * results, if an entity at the end of one batch of results gets pushed back further, putting it
+   * at the beginning of the next batch. This race cannot be avoided unless we change the underlying
+   * index interface to support true continuation tokens.
+   *
+   * @param querySupplier supplier for queries. Callers will generally pass a lambda that invokes an
+   *     underlying {@code Provider<InternalFooQuery>}, since the instances are not reusable. The
+   *     lambda may also call additional methods on the newly-created query, such as {@link
+   *     #enforceVisibility(boolean)}.
+   * @param predicate predicate to search for.
+   * @param <T> result type.
+   * @return exhaustive list of results, subject to the race condition described above.
+   * @throws OrmException if an error occurred.
+   */
+  protected static <T> ImmutableList<T> queryExhaustively(
+      Supplier<? extends InternalQuery<T, ?>> querySupplier, Predicate<T> predicate)
+      throws OrmException {
+    ImmutableList.Builder<T> b = null;
+    int start = 0;
+    while (true) {
+      QueryResult<T> qr = querySupplier.get().setStart(start).queryResults(predicate);
+      if (b == null) {
+        if (!qr.more()) {
+          return qr.entities();
+        }
+        b = ImmutableList.builder();
+      }
+      b.addAll(qr.entities());
+      if (!qr.more()) {
+        return b.build();
+      }
+      start += qr.entities().size();
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index d0840d6..6f3194e 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -42,7 +42,7 @@
  * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than
  * holding on to a single instance.
  */
-public class InternalAccountQuery extends InternalQuery<AccountState> {
+public class InternalAccountQuery extends InternalQuery<AccountState, InternalAccountQuery> {
   @Inject
   InternalAccountQuery(
       AccountQueryProcessor queryProcessor,
@@ -51,31 +51,6 @@
     super(queryProcessor, indexes, indexConfig);
   }
 
-  @Override
-  public InternalAccountQuery setLimit(int n) {
-    super.setLimit(n);
-    return this;
-  }
-
-  @Override
-  public InternalAccountQuery enforceVisibility(boolean enforce) {
-    super.enforceVisibility(enforce);
-    return this;
-  }
-
-  @SafeVarargs
-  @Override
-  public final InternalAccountQuery setRequestedFields(FieldDef<AccountState, ?>... fields) {
-    super.setRequestedFields(fields);
-    return this;
-  }
-
-  @Override
-  public InternalAccountQuery noFields() {
-    super.noFields();
-    return this;
-  }
-
   public List<AccountState> byDefault(String query) throws OrmException {
     return query(AccountPredicates.defaultPredicate(schema(), true, query));
   }
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index dc89c88..973c451 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -25,7 +25,6 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.query.InternalQuery;
 import com.google.gerrit.index.query.Predicate;
@@ -45,6 +44,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Supplier;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -55,7 +55,7 @@
  * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than
  * holding on to a single instance.
  */
-public class InternalChangeQuery extends InternalQuery<ChangeData> {
+public class InternalChangeQuery extends InternalQuery<ChangeData, InternalChangeQuery> {
   private static Predicate<ChangeData> ref(Branch.NameKey branch) {
     return new RefPredicate(branch.get());
   }
@@ -91,31 +91,6 @@
     this.notesFactory = notesFactory;
   }
 
-  @Override
-  public InternalChangeQuery setLimit(int n) {
-    super.setLimit(n);
-    return this;
-  }
-
-  @Override
-  public InternalChangeQuery enforceVisibility(boolean enforce) {
-    super.enforceVisibility(enforce);
-    return this;
-  }
-
-  @SafeVarargs
-  @Override
-  public final InternalChangeQuery setRequestedFields(FieldDef<ChangeData, ?>... fields) {
-    super.setRequestedFields(fields);
-    return this;
-  }
-
-  @Override
-  public InternalChangeQuery noFields() {
-    super.noFields();
-    return this;
-  }
-
   public List<ChangeData> byKey(Change.Key key) throws OrmException {
     return byKeyPrefix(key.get());
   }
@@ -300,34 +275,42 @@
     return query(new SubmissionIdPredicate(cs));
   }
 
-  private List<ChangeData> byProjectGroups(Project.NameKey project, Collection<String> groups)
-      throws OrmException {
+  private static Predicate<ChangeData> byProjectGroupsPredicate(
+      IndexConfig indexConfig, Project.NameKey project, Collection<String> groups) {
     int n = indexConfig.maxTerms() - 1;
     checkArgument(groups.size() <= n, "cannot exceed %s groups", n);
     List<GroupPredicate> groupPredicates = new ArrayList<>(groups.size());
     for (String g : groups) {
       groupPredicates.add(new GroupPredicate(g));
     }
-    return query(and(project(project), or(groupPredicates)));
+    return and(project(project), or(groupPredicates));
   }
 
-  // Batching via multiple queries requires passing in a Provider since the underlying
-  // QueryProcessor instance is not reusable.
   public static List<ChangeData> byProjectGroups(
       Provider<InternalChangeQuery> queryProvider,
       IndexConfig indexConfig,
       Project.NameKey project,
       Collection<String> groups)
       throws OrmException {
+    // These queries may be complex along multiple dimensions:
+    //  * Many groups per change, if there are very many patch sets. This requires partitioning the
+    //    list of predicates and combining results.
+    //  * Many changes with the same set of groups, if the relation chain is very long. This
+    //    requires querying exhaustively with pagination.
+    // For both cases, we need to invoke the queryProvider multiple times, since each
+    // InternalChangeQuery is single-use.
+
+    Supplier<InternalChangeQuery> querySupplier = () -> queryProvider.get().enforceVisibility(true);
     int batchSize = indexConfig.maxTerms() - 1;
     if (groups.size() <= batchSize) {
-      return queryProvider.get().enforceVisibility(true).byProjectGroups(project, groups);
+      return queryExhaustively(
+          querySupplier, byProjectGroupsPredicate(indexConfig, project, groups));
     }
     Set<Change.Id> seen = new HashSet<>();
     List<ChangeData> result = new ArrayList<>();
     for (List<String> part : Iterables.partition(groups, batchSize)) {
       for (ChangeData cd :
-          queryProvider.get().enforceVisibility(true).byProjectGroups(project, part)) {
+          queryExhaustively(querySupplier, byProjectGroupsPredicate(indexConfig, project, part))) {
         if (!seen.add(cd.getId())) {
           result.add(cd);
         }
diff --git a/java/com/google/gerrit/server/query/group/InternalGroupQuery.java b/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
index d9808f2..42b38bf 100644
--- a/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
+++ b/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
@@ -37,7 +37,7 @@
  * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than
  * holding on to a single instance.
  */
-public class InternalGroupQuery extends InternalQuery<InternalGroup> {
+public class InternalGroupQuery extends InternalQuery<InternalGroup, InternalGroupQuery> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Inject
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index b0380b8..6dc03ae 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -59,6 +59,7 @@
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -195,21 +196,22 @@
     return cfg;
   }
 
-  @Inject private Provider<PublicKeyStore> publicKeyStoreProvider;
   @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
-  @Inject private ExternalIds externalIds;
+  @Inject private AccountIndexer accountIndexer;
   @Inject private DynamicSet<AccountIndexedListener> accountIndexedListeners;
   @Inject private DynamicSet<GitReferenceUpdatedListener> refUpdateListeners;
-  @Inject private Sequences seq;
-  @Inject private Provider<InternalAccountQuery> accountQueryProvider;
-  @Inject private StalenessChecker stalenessChecker;
-  @Inject private AccountIndexer accountIndexer;
-  @Inject private GitReferenceUpdated gitReferenceUpdated;
-  @Inject private RetryHelper.Metrics retryMetrics;
-  @Inject private Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
   @Inject private ExternalIdNotes.Factory extIdNotesFactory;
-  @Inject private VersionedAuthorizedKeys.Accessor authorizedKeys;
+  @Inject private ExternalIds externalIds;
+  @Inject private GitReferenceUpdated gitReferenceUpdated;
   @Inject private ProjectOperations projectOperations;
+  @Inject private Provider<InternalAccountQuery> accountQueryProvider;
+  @Inject private Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
+  @Inject private Provider<PublicKeyStore> publicKeyStoreProvider;
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private RetryHelper.Metrics retryMetrics;
+  @Inject private Sequences seq;
+  @Inject private StalenessChecker stalenessChecker;
+  @Inject private VersionedAuthorizedKeys.Accessor authorizedKeys;
 
   @Inject protected Emails emails;
 
@@ -708,7 +710,7 @@
 
     accountIndexedCounter.assertNoReindex();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to get stars of another account");
     gApi.accounts().id(Integer.toString((admin.id.get()))).getStars(triplet);
@@ -771,11 +773,11 @@
     in.reviewer = user2.email;
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
 
     sender.clear();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(r.getChangeId()).abandon();
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
@@ -787,11 +789,11 @@
   public void addReviewerToIgnoredChange() throws Exception {
     PushOneCommit.Result r = createChange();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
 
     sender.clear();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
 
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = user.email;
@@ -832,7 +834,7 @@
     String status = "OOO";
     gApi.accounts().id(foo.id.get()).setStatus(status);
 
-    setApiUser(foo);
+    requestScopeOperations.setApiUser(foo.getId());
     AccountDetailInfo detail = gApi.accounts().id(foo.id.get()).detail();
     assertThat(detail._accountId).isEqualTo(foo.id.get());
     assertThat(detail.name).isEqualTo(name);
@@ -854,7 +856,7 @@
     EmailInput input = newEmailInput(secondaryEmail);
     gApi.accounts().id(foo.id.get()).addEmail(input);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     AccountDetailInfo detail = gApi.accounts().id(foo.id.get()).detail();
     assertThat(detail.secondaryEmails).isNull();
   }
@@ -876,15 +878,15 @@
     String email = "preferred@example.com";
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
 
-    setApiUser(foo);
+    requestScopeOperations.setApiUser(foo.getId());
     assertThat(getEmails()).containsExactly(email);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     String secondaryEmail = "secondary@example.com";
     EmailInput input = newEmailInput(secondaryEmail);
     gApi.accounts().id(foo.id.hashCode()).addEmail(input);
 
-    setApiUser(foo);
+    requestScopeOperations.setApiUser(foo.getId());
     assertThat(getEmails()).containsExactly(email, secondaryEmail);
   }
 
@@ -893,7 +895,7 @@
     String email = "preferred2@example.com";
     TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("modify account not permitted");
     gApi.accounts().id(foo.id.get()).getEmails();
@@ -928,7 +930,7 @@
       accountIndexedCounter.assertReindexOf(admin);
     }
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     assertThat(getEmails()).containsAllIn(emails);
   }
 
@@ -963,7 +965,7 @@
   public void cannotAddNonConfirmedEmailWithoutModifyAccountPermission() throws Exception {
     TestAccount account = accountCreator.create(name("user"));
     EmailInput input = newEmailInput("test@test.com");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.accounts().id(account.username).addEmail(input);
   }
@@ -998,7 +1000,7 @@
       value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
   public void addEmailToBeConfirmedToOwnAccount() throws Exception {
     TestAccount user = accountCreator.create();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     String email = "self@example.com";
     EmailInput input = newEmailInput(email, false);
@@ -1009,7 +1011,7 @@
   public void cannotAddEmailToBeConfirmedToOtherAccountWithoutModifyAccountPermission()
       throws Exception {
     TestAccount user = accountCreator.create();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     exception.expect(AuthException.class);
     exception.expectMessage("modify account not permitted");
@@ -1032,14 +1034,14 @@
     EmailInput input = newEmailInput(email);
     gApi.accounts().self().addEmail(input);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     assertThat(getEmails()).contains(email);
 
     accountIndexedCounter.clear();
     gApi.accounts().self().deleteEmail(input.email);
     accountIndexedCounter.assertReindexOf(admin);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     assertThat(getEmails()).doesNotContain(email);
   }
 
@@ -1063,13 +1065,13 @@
             gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
         .containsAllOf(extId1, extId2);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     assertThat(getEmails()).contains(email);
 
     gApi.accounts().self().deleteEmail(email);
     accountIndexedCounter.assertReindexOf(admin);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     assertThat(getEmails()).doesNotContain(email);
     assertThat(
             gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
@@ -1085,15 +1087,15 @@
     gApi.accounts().id(user.id.get()).addEmail(input);
     accountIndexedCounter.assertReindexOf(user);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(getEmails()).contains(email);
 
     // admin can delete email of user
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.accounts().id(user.id.get()).deleteEmail(email);
     accountIndexedCounter.assertReindexOf(user);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(getEmails()).doesNotContain(email);
 
     // user cannot delete email of admin
@@ -1190,7 +1192,7 @@
 
   @Test
   public void userCannotSetNameOfOtherUser() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.accounts().id(admin.username).setName("Admin McAdminface");
   }
@@ -1205,7 +1207,7 @@
 
   @Test
   public void fetchUserBranch() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
     String userRefName = RefNames.refsUsers(user.id);
@@ -1357,7 +1359,7 @@
     accountIndexedCounter.assertNoReindex();
     assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
 
-    setApiUser(foo);
+    requestScopeOperations.setApiUser(foo.getId());
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
     gApi.changes().id(r.getChangeId()).current().submit();
 
@@ -1550,7 +1552,7 @@
   @Test
   public void pushAccountConfigToUserBranch() throws Exception {
     TestAccount oooUser = accountCreator.create("away", "away@mail.invalid", "Ambrose Way");
-    setApiUser(oooUser);
+    requestScopeOperations.setApiUser(oooUser.getId());
 
     // Must clone as oooUser to ensure the push is allowed.
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, oooUser);
@@ -1879,7 +1881,7 @@
     assertKeyMapContains(key, addGpgKey(key.getPublicKeyArmored()));
     assertKeys(key);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(ResourceNotFoundException.class);
     exception.expectMessage(id);
     gApi.accounts().self().gpgKey(id).get();
@@ -1916,7 +1918,7 @@
 
     TestKey key = validKeyWithSecondUserId();
     addGpgKey(key.getPublicKeyArmored());
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     exception.expect(ResourceConflictException.class);
     exception.expectMessage("GPG key already associated with another account");
@@ -2059,12 +2061,12 @@
   @Test
   public void reindexPermissions() throws Exception {
     // admin can reindex any account
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.accounts().id(user.username).index();
     accountIndexedCounter.assertReindexOf(user);
 
     // user can reindex own account
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.accounts().self().index();
     accountIndexedCounter.assertReindexOf(user);
 
@@ -2077,7 +2079,7 @@
   @Test
   public void checkConsistency() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
 
     // Create an account with a preferred email.
     String username = name("foo");
@@ -2600,7 +2602,7 @@
               null);
 
       // Create 2 drafts each on both changes for user.
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       createDraft(r1, PushOneCommit.FILE_NAME, "draft 1a");
       createDraft(r1, PushOneCommit.FILE_NAME, "draft 1b");
       createDraft(r2, PushOneCommit.FILE_NAME, "draft 2a");
@@ -2609,12 +2611,12 @@
       assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(2);
 
       // Create 1 draft on first change for admin.
-      setApiUser(admin);
+      requestScopeOperations.setApiUser(admin.getId());
       createDraft(r1, PushOneCommit.FILE_NAME, "admin draft");
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
 
       // Delete user's draft comments; leave admin's alone.
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       List<DeletedDraftCommentInfo> result =
           gApi.accounts().self().deleteDraftComments(new DeleteDraftCommentsInput());
 
@@ -2630,7 +2632,7 @@
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).isEmpty();
       assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).isEmpty();
 
-      setApiUser(admin);
+      requestScopeOperations.setApiUser(admin.getId());
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
     } finally {
       cleanUpDrafts();
@@ -2667,9 +2669,9 @@
   public void deleteOtherUsersDraftCommentsDisallowed() throws Exception {
     try {
       PushOneCommit.Result r = createChange();
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       createDraft(r, PushOneCommit.FILE_NAME, "draft");
-      setApiUser(admin);
+      requestScopeOperations.setApiUser(admin.getId());
       try {
         gApi.accounts().id(user.id.get()).deleteDraftComments(new DeleteDraftCommentsInput());
         assert_().fail("expected AuthException");
@@ -2688,7 +2690,7 @@
       PushOneCommit.Result r1 = createChange();
       PushOneCommit.Result r2 = createChange("refs/for/secret");
 
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       createDraft(r1, PushOneCommit.FILE_NAME, "draft a");
       createDraft(r2, PushOneCommit.FILE_NAME, "draft b");
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
@@ -2732,7 +2734,7 @@
 
   private void cleanUpDrafts() throws Exception {
     for (TestAccount testAccount : accountCreator.getAll()) {
-      setApiUser(testAccount);
+      requestScopeOperations.setApiUser(testAccount.getId());
       for (ChangeInfo changeInfo : gApi.changes().query("has:draft").get()) {
         for (CommentInfo c :
             gApi.changes()
@@ -2864,7 +2866,7 @@
                 u.addExternalId(
                     ExternalId.createWithEmail(name("test"), email, account.getId(), email)));
     accountIndexedCounter.assertReindexOf(account);
-    setApiUser(account);
+    requestScopeOperations.setApiUser(account.getId());
   }
 
   private Map<String, GpgKeyInfo> addGpgKey(String armored) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index 0a64a1d..29d4aa0 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GroupReference;
@@ -62,6 +63,7 @@
   private ContributorAgreement caAutoVerify;
   private ContributorAgreement caNoAutoVerify;
   @Inject private GroupOperations groupOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   protected void setUseContributorAgreements(InheritableBoolean value) throws Exception {
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
@@ -123,7 +125,7 @@
   public void setUp() throws Exception {
     caAutoVerify = configureContributorAgreement(true);
     caNoAutoVerify = configureContributorAgreement(false);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
   }
 
   @Test
@@ -168,7 +170,7 @@
     gApi.accounts().self().signAgreement(caAutoVerify.getName());
 
     // Explicitly reset the user to force a new request context
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     // Verify that the agreement was signed
     result = gApi.accounts().self().listAgreements();
@@ -193,7 +195,7 @@
 
   @Test
   public void signAgreementAnonymous() throws Exception {
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     exception.expect(AuthException.class);
     exception.expectMessage("Authentication required");
     gApi.accounts().self().signAgreement(caAutoVerify.getName());
@@ -224,12 +226,12 @@
     ChangeInfo change = gApi.changes().create(newChangeInput()).get();
 
     // Approve and submit it
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(change.changeId).current().review(ReviewInput.approve());
     gApi.changes().id(change.changeId).current().submit(new SubmitInput());
 
     // Revert is not allowed when CLA is required but not signed
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     setUseContributorAgreements(InheritableBoolean.TRUE);
     exception.expect(AuthException.class);
     exception.expectMessage("Contributor Agreement");
@@ -248,12 +250,12 @@
     ChangeInfo change = gApi.changes().create(newChangeInput()).get();
 
     // Approve and submit it
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(change.changeId).current().review(ReviewInput.approve());
     gApi.changes().id(change.changeId).current().submit(new SubmitInput());
 
     // Revert in excluded project is allowed even when CLA is required but not signed
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     setUseContributorAgreements(InheritableBoolean.TRUE);
     gApi.changes().id(change.changeId).revert();
   }
@@ -263,7 +265,7 @@
     assume().that(isContributorAgreementsEnabled()).isTrue();
 
     // Create a new branch
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     BranchInfo dest =
         gApi.projects()
             .name(project.get())
@@ -280,7 +282,7 @@
     gApi.changes().id(change.changeId).current().submit(new SubmitInput());
 
     // Cherry-pick is not allowed when CLA is required but not signed
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     setUseContributorAgreements(InheritableBoolean.TRUE);
     CherryPickInput in = new CherryPickInput();
     in.destination = dest.ref;
@@ -311,7 +313,7 @@
     gApi.accounts().self().signAgreement(caAutoVerify.getName());
 
     // Explicitly reset the user to force a new request context
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     // Create a change succeeds after signing the agreement
     gApi.changes().create(newChangeInput());
@@ -350,7 +352,7 @@
   @Test
   @GerritConfig(name = "auth.contributorAgreements", value = "true")
   public void anonymousAccessServerInfoEvenWithCLAs() throws Exception {
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     gApi.config().server().getInfo();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
index 05eca2a..e655053 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -44,6 +45,7 @@
 
 public class AbandonIT extends AbstractDaemonTest {
   @Inject private AbandonUtil abandonUtil;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void abandon() throws Exception {
@@ -127,7 +129,7 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("abandon not permitted");
     gApi.changes().id(changeId).abandon();
@@ -139,7 +141,7 @@
     String changeId = r.getChangeId();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
     grant(project, "refs/heads/master", Permission.ABANDON, false, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).abandon();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED);
     gApi.changes().id(changeId).restore();
@@ -170,7 +172,7 @@
     String changeId = r.getChangeId();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
     gApi.changes().id(changeId).abandon();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED);
     exception.expect(AuthException.class);
     exception.expectMessage("restore not permitted");
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 3714cce..81acb3f 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -68,6 +68,7 @@
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
@@ -181,17 +182,14 @@
 public class ChangeIT extends AbstractDaemonTest {
   private String systemTimeZone;
 
-  @Inject private DynamicSet<ChangeMessageModifier> changeMessageModifiers;
-
-  @Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
-
   @Inject private AccountOperations accountOperations;
-
   @Inject private ChangeIndexCollection changeIndexCollection;
+  @Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
+  @Inject private DynamicSet<ChangeMessageModifier> changeMessageModifiers;
+  @Inject private GroupOperations groupOperations;
   @Inject private IndexConfig indexConfig;
-
-  @Inject protected GroupOperations groupOperations;
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private ChangeIndexedCounter changeIndexedCounter;
   private RegistrationHandle changeIndexedCounterHandle;
@@ -261,7 +259,7 @@
     PushOneCommit.Result result =
         pushFactory.create(user.getIdent(), userRepo).to("refs/for/master");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     String changeId = result.getChangeId();
     assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
 
@@ -302,7 +300,7 @@
     assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
 
     gApi.changes().id(changeId).setPrivate(true, null);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     ChangeInfo info = gApi.changes().id(changeId).get();
     assertThat(info.isPrivate).isTrue();
   }
@@ -310,7 +308,7 @@
   @Test
   public void cannotSetOtherUsersChangePrivate() throws Exception {
     PushOneCommit.Result result = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to mark private");
     gApi.changes().id(result.getChangeId()).setPrivate(true, null);
@@ -322,7 +320,7 @@
     PushOneCommit.Result result =
         pushFactory.create(user.getIdent(), userRepo).to("refs/for/master");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(result.getChangeId()).setPrivate(true, null);
     // Owner can always access its private changes.
     assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
@@ -331,7 +329,7 @@
     gApi.changes().id(result.getChangeId()).addReviewer(admin.getId().toString());
 
     // This change should be visible for admin as a reviewer.
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
 
     // Remove admin from reviewers.
@@ -349,7 +347,7 @@
     gApi.changes().id(result.getChangeId()).setPrivate(true, null);
 
     allow("refs/*", Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
   }
 
@@ -385,7 +383,7 @@
 
     merge(result);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to mark private");
     gApi.changes().id(changeId).setPrivate(true, null);
@@ -405,7 +403,7 @@
 
     merge(result);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).setPrivate(false, null);
     assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
   }
@@ -415,7 +413,7 @@
     PushOneCommit.Result rwip = createChange();
     String changeId = rwip.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to toggle work in progress");
     gApi.changes().id(changeId).setWorkInProgress();
@@ -423,24 +421,24 @@
 
   @Test
   public void setWorkInProgressAllowedAsAdmin() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).setWorkInProgress();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
   }
 
   @Test
   public void setWorkInProgressAllowedAsProjectOwner() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
 
     com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
     grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.getId());
     gApi.changes().id(changeId).setWorkInProgress();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
   }
@@ -461,7 +459,7 @@
     String changeId = rready.getChangeId();
     gApi.changes().id(changeId).setWorkInProgress();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to toggle work in progress");
     gApi.changes().id(changeId).setReadyForReview();
@@ -469,26 +467,26 @@
 
   @Test
   public void setReadyForReviewAllowedAsAdmin() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
     gApi.changes().id(changeId).setWorkInProgress();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).setReadyForReview();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
   }
 
   @Test
   public void setReadyForReviewAllowedAsProjectOwner() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
     gApi.changes().id(changeId).setWorkInProgress();
 
     com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
     grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.getId());
     gApi.changes().id(changeId).setReadyForReview();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
   }
@@ -700,7 +698,7 @@
     r.assertOkStatus();
     assertThat(r.getChange().change().getOwner()).isEqualTo(user.id);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
     gApi.changes().id(r.getChangeId()).current().review(in);
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -715,7 +713,7 @@
     r.assertOkStatus();
     assertThat(r.getChange().change().getOwner()).isEqualTo(user.id);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
     gApi.changes().id(r.getChangeId()).current().review(in);
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -726,7 +724,7 @@
   public void reviewWithWorkInProgressByNonOwnerReturnsError() throws Exception {
     PushOneCommit.Result r = createChange();
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to toggle work in progress");
     gApi.changes().id(r.getChangeId()).current().review(in);
@@ -736,7 +734,7 @@
   public void reviewWithReadyByNonOwnerReturnsError() throws Exception {
     PushOneCommit.Result r = createChange();
     ReviewInput in = ReviewInput.noScore().setReady(true);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to toggle work in progress");
     gApi.changes().id(r.getChangeId()).current().review(in);
@@ -838,7 +836,7 @@
 
     // expect both the original reviewers and CCs to be preserved
     // original owner should be added as reviewer, user requesting the revert (new owner) removed
-    setApiUser(accountCreator.admin2());
+    requestScopeOperations.setApiUser(accountCreator.admin2().getId());
     Map<ReviewerState, Collection<AccountInfo>> result =
         gApi.changes().id(r.getChangeId()).revert().get().reviewers;
     assertThat(result).containsKey(ReviewerState.REVIEWER);
@@ -1020,7 +1018,7 @@
 
     // Rebase the second
     String changeId = r2.getChangeId();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("rebase not permitted");
     gApi.changes().id(changeId).rebase();
@@ -1042,7 +1040,7 @@
 
     // Rebase the second
     String changeId = r2.getChangeId();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).rebase();
   }
 
@@ -1063,7 +1061,7 @@
 
     // Rebase the second
     String changeId = r2.getChangeId();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("rebase not permitted");
     gApi.changes().id(changeId).rebase();
@@ -1102,7 +1100,7 @@
         pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
     String changeId = changeResult.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("delete not permitted");
     gApi.changes().id(changeId).delete();
@@ -1161,7 +1159,7 @@
       com.google.gerrit.acceptance.TestAccount deleteAs)
       throws Exception {
     try {
-      setApiUser(owner);
+      requestScopeOperations.setApiUser(owner.getId());
       ChangeInput in = new ChangeInput();
       in.project = projectName.get();
       in.branch = "refs/heads/master";
@@ -1173,7 +1171,7 @@
 
       assertThat(gApi.changes().id(changeId).info().owner._accountId).isEqualTo(owner.id.get());
 
-      setApiUser(deleteAs);
+      requestScopeOperations.setApiUser(deleteAs.getId());
       gApi.changes().id(changeId).delete();
 
       assertThat(query(changeId)).isEmpty();
@@ -1200,7 +1198,7 @@
       PushOneCommit.Result changeResult = createChange();
       String changeId = changeResult.getChangeId();
 
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       exception.expect(AuthException.class);
       exception.expectMessage("delete not permitted");
       gApi.changes().id(changeId).delete();
@@ -1227,7 +1225,7 @@
         pushFactory.create(user.getIdent(), testRepo).to("refs/for/master");
     String changeId = changeResult.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).abandon();
 
     exception.expect(AuthException.class);
@@ -1273,7 +1271,7 @@
 
       merge(changeResult);
 
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       exception.expect(MethodNotAllowedException.class);
       exception.expectMessage("delete not permitted");
       gApi.changes().id(changeId).delete();
@@ -1536,7 +1534,7 @@
     assertThat(commit.committer.email).isEqualTo(user.email);
 
     // check the user cannot see the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     try {
       gApi.changes().id(result.getChangeId()).get();
       fail("Expected ResourceNotFoundException");
@@ -1613,7 +1611,7 @@
     result.assertOkStatus();
 
     // check that 'user' cannot see the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     try {
       gApi.changes().id(result.getChangeId()).get();
       fail("Expected ResourceNotFoundException");
@@ -1622,7 +1620,7 @@
     }
 
     // check that 'user' was NOT added as cc ('user' can't see the change)
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
     assertThat(change.reviewers.get(REVIEWER)).isNull();
     assertThat(change.reviewers.get(CC)).isNull();
@@ -1646,7 +1644,7 @@
     result.assertOkStatus();
 
     // check the user cannot see the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     try {
       gApi.changes().id(result.getChangeId()).get();
       fail("Expected ResourceNotFoundException");
@@ -1655,7 +1653,7 @@
     }
 
     // try to add user as reviewer
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = user.email;
     AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
@@ -1907,7 +1905,7 @@
 
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = user.email;
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     // There should be no email notification when adding self
@@ -1944,7 +1942,7 @@
   private void testImplicitlyCcOnNonVotingReviewPgStyle(
       com.google.gerrit.acceptance.TestAccount testAccount) throws Exception {
     PushOneCommit.Result r = createChange();
-    setApiUser(testAccount);
+    requestScopeOperations.setApiUser(testAccount.getId());
     assertThat(getReviewerState(r.getChangeId(), testAccount.id)).isEmpty();
 
     // Exact request format made by PG UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8.
@@ -1961,7 +1959,7 @@
   @Test
   public void implicitlyAddReviewerOnVotingReview() throws Exception {
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes()
         .id(r.getChangeId())
         .revision(r.getCommit().name())
@@ -1973,12 +1971,12 @@
 
     // Further test: remove the vote, then comment again. The user should be
     // implicitly re-added to the ReviewerSet, as a CC if we're using NoteDb.
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).remove();
     c = gApi.changes().id(r.getChangeId()).get();
     assertThat(c.reviewers.values()).isEmpty();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes()
         .id(r.getChangeId())
         .revision(r.getCommit().name())
@@ -2080,7 +2078,7 @@
     assertThat(m).hasSize(1);
     assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 2));
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.dislike());
 
     m = gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).votes();
@@ -2152,7 +2150,7 @@
     String changeId = r.getChangeId();
     gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.recommend());
 
     Collection<AccountInfo> reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER);
@@ -2163,7 +2161,7 @@
     assertThat(reviewerIt.next()._accountId).isEqualTo(user.getId().get());
 
     sender.clear();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     DeleteReviewerInput input = new DeleteReviewerInput();
     if (!notify) {
       input.notify = NotifyHandling.NONE;
@@ -2194,7 +2192,7 @@
     String changeId = r.getChangeId();
     gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).remove();
@@ -2205,14 +2203,14 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(changeId);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     approve(changeId);
     gApi.changes().id(changeId).revision(r.getCommit().name()).submit();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer("self").remove();
@@ -2223,13 +2221,13 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(changeId);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).abandon();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).reviewer("self").remove();
     eventRecorder.assertReviewerDeletedEvents(changeId, user.email);
   }
@@ -2239,14 +2237,14 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(changeId);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     approve(changeId);
     gApi.changes().id(changeId).abandon();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).remove();
@@ -2257,10 +2255,10 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     sender.clear();
     gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote("Code-Review");
 
@@ -2292,10 +2290,10 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     sender.clear();
     DeleteVoteInput in = new DeleteVoteInput();
     in.label = "Code-Review";
@@ -2321,9 +2319,9 @@
         .preferredEmail(email)
         .fullname("User2")
         .create();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     sender.clear();
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.TO, new NotifyInfo(ImmutableList.of(email)));
@@ -2331,9 +2329,9 @@
     assertNotifyTo(email, "User2");
 
     // notify unrelated account as CC
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     sender.clear();
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.CC, new NotifyInfo(ImmutableList.of(email)));
@@ -2341,9 +2339,9 @@
     assertNotifyCc(email, "User2");
 
     // notify unrelated account as BCC
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     sender.clear();
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.BCC, new NotifyInfo(ImmutableList.of(email)));
@@ -2356,7 +2354,7 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("delete vote not permitted");
     gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).deleteVote("Code-Review");
@@ -2400,12 +2398,12 @@
 
     // Approve the change as user, then remove the approval
     // (only to confirm that the user does have Code-Review+2 permission)
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).revision(commit).review(ReviewInput.approve());
     gApi.changes().id(changeId).revision(commit).review(ReviewInput.noScore());
 
     // Submit the change
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).revision(commit).submit();
 
     // User should still be on the change
@@ -2522,7 +2520,7 @@
     assertThat(
             Iterables.getOnlyElement(query("project:{" + project.get() + "} owner:self")).changeId)
         .isEqualTo(r.getChangeId());
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(query("owner:self project:{" + project.get() + "}")).isEmpty();
   }
 
@@ -2533,7 +2531,7 @@
     in.reviewer = user.email;
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(get(r.getChangeId(), REVIEWED).reviewed).isNull();
 
     revision(r).review(ReviewInput.recommend());
@@ -2554,7 +2552,7 @@
   public void editTopicWithoutPermissionNotAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("edit topic name not permitted");
     gApi.changes().id(r.getChangeId()).topic("mytopic");
@@ -2565,7 +2563,7 @@
     PushOneCommit.Result r = createChange();
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
     grant(project, "refs/heads/master", Permission.EDIT_TOPIC_NAME, false, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).topic("mytopic");
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("mytopic");
   }
@@ -2609,7 +2607,7 @@
   public void submitNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("submit not permitted");
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
@@ -2620,7 +2618,7 @@
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
     grant(project, "refs/heads/master", Permission.SUBMIT, false, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
     assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(ChangeStatus.MERGED);
   }
@@ -2726,7 +2724,7 @@
 
   @Test
   public void defaultSearchDoesNotTouchDatabase() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     PushOneCommit.Result r1 = createChange();
     gApi.changes()
         .id(r1.getChangeId())
@@ -2736,7 +2734,7 @@
 
     createChange();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     AcceptanceTestRequestScope.Context ctx = disableDb();
     try {
       assertThat(
@@ -2803,7 +2801,7 @@
 
   @Test
   public void anonymousRestApi() throws Exception {
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     PushOneCommit.Result r = createChange();
 
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -3038,7 +3036,7 @@
     gApi.changes().id(baseChange).setPrivate(true, "set private");
 
     // Create the destination change on 'master' branch.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     testRepo.reset(initialHead);
     String changeId = createChange().getChangeId();
 
@@ -3259,10 +3257,10 @@
 
     PushOneCommit.Result r = createChange();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
 
     ChangeInfo change = gApi.changes().id(r.getChangeId()).get();
@@ -3482,7 +3480,7 @@
         .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
 
     for (com.google.gerrit.acceptance.TestAccount acc : ImmutableList.of(admin, user)) {
-      setApiUser(acc);
+      requestScopeOperations.setApiUser(acc.getId());
       String newMessage =
           "modified commit by " + acc.username + "\n\nChange-Id: " + r.getChangeId() + "\n";
       gApi.changes().id(r.getChangeId()).setMessage(newMessage);
@@ -3501,7 +3499,7 @@
 
     // Move the change to WIP and edit the commit message again, to observe a
     // different tag. Must switch to change owner to move into WIP.
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(r.getChangeId()).setWorkInProgress();
     String newMessage = "modified commit in WIP change\n\nChange-Id: " + r.getChangeId() + "\n";
     gApi.changes().id(r.getChangeId()).setMessage(newMessage);
@@ -3747,7 +3745,7 @@
       u.save();
     }
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
 
@@ -3767,7 +3765,7 @@
         .containsExactly((short) 2, (short) 1);
     assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     // Remove user's permission for 'Label'.
     try (ProjectConfigUpdate u = updateProject(project)) {
       Util.remove(u.getConfig(), Permission.forLabel(label), registered, "refs/heads/*");
@@ -3779,7 +3777,7 @@
     }
 
     // Verify user's new permitted range.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     change = gApi.changes().id(changeId).get();
     assertPermitted(change, label);
     assertPermitted(change, codeReviewLabel, -1, 0, 1);
@@ -3788,7 +3786,7 @@
         .containsExactly((short) 2, (short) 1);
     assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).current().submit();
   }
 
@@ -3964,18 +3962,18 @@
     in.reviewer = email;
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).ignore(true);
     assertThat(gApi.changes().id(r.getChangeId()).ignored()).isTrue();
 
     sender.clear();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(r.getChangeId()).abandon();
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     assertThat(messages.get(0).rcpt()).containsExactly(new Address(fullname, email));
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).ignore(false);
     assertThat(gApi.changes().id(r.getChangeId()).ignored()).isFalse();
   }
@@ -3993,7 +3991,7 @@
   public void cannotIgnoreStarredChange() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.accounts().self().starChange(changeId);
     assertThat(gApi.changes().id(changeId).get().starred).isTrue();
 
@@ -4011,7 +4009,7 @@
   public void cannotStarIgnoredChange() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).ignore(true);
     assertThat(gApi.changes().id(changeId).ignored()).isTrue();
 
@@ -4035,16 +4033,16 @@
     in.reviewer = user.email;
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull();
     gApi.changes().id(r.getChangeId()).markAsReviewed(true);
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isTrue();
 
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.getId());
     sender.clear();
     amendChange(r.getChangeId());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull();
 
     List<Message> messages = sender.getMessages();
@@ -4056,7 +4054,7 @@
   public void cannotSetUnreviewedLabelForPatchSetThatAlreadyHasReviewedLabel() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).markAsReviewed(true);
     assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
 
@@ -4081,7 +4079,7 @@
   public void cannotSetReviewedLabelForPatchSetThatAlreadyHasUnreviewedLabel() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).markAsReviewed(false);
     assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
 
@@ -4106,7 +4104,7 @@
   public void setReviewedAndUnreviewedLabelsForDifferentPatchSets() throws Exception {
     String changeId = createChange().getChangeId();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).markAsReviewed(true);
     assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index 81fd3d1..2a295b2 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
@@ -46,6 +47,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.project.testing.Util;
+import com.google.inject.Inject;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Set;
@@ -58,6 +60,7 @@
 
 @NoHttpd
 public class StickyApprovalsIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
   public void setup() throws Exception {
@@ -455,7 +458,7 @@
   }
 
   private void trivialRebase(String changeId) throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     testRepo.reset(getRemoteHead());
     PushOneCommit push =
         pushFactory.create(
@@ -553,20 +556,20 @@
   }
 
   private void vote(TestAccount user, String changeId, String label, int vote) throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).current().review(new ReviewInput().label(label, vote));
   }
 
   private void vote(TestAccount user, String changeId, int codeReviewVote, int verifiedVote)
       throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     ReviewInput in =
         new ReviewInput().label("Code-Review", codeReviewVote).label("Verified", verifiedVote);
     gApi.changes().id(changeId).current().review(in);
   }
 
   private void deleteVote(TestAccount user, String changeId, String label) throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(changeId).reviewer(user.getId().toString()).deleteVote(label);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 19c7eeb..eb0f7e8 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupReference;
@@ -118,17 +119,18 @@
 
 @NoHttpd
 public class GroupsIT extends AbstractDaemonTest {
-  @Inject private Groups groups;
   @Inject @ServerInitiated private GroupsUpdate groupsUpdate;
+  @Inject private AccountOperations accountOperations;
+  @Inject private DynamicSet<GroupIndexedListener> groupIndexedListeners;
   @Inject private GroupIncludeCache groupIncludeCache;
-  @Inject private StalenessChecker stalenessChecker;
   @Inject private GroupIndexer groupIndexer;
+  @Inject private GroupOperations groupOperations;
+  @Inject private Groups groups;
   @Inject private GroupsConsistencyChecker consistencyChecker;
   @Inject private PeriodicGroupIndexer slaveGroupIndexer;
-  @Inject private DynamicSet<GroupIndexedListener> groupIndexedListeners;
+  @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private Sequences seq;
-  @Inject private AccountOperations accountOperations;
-  @Inject private GroupOperations groupOperations;
+  @Inject private StalenessChecker stalenessChecker;
 
   @Before
   public void setTimeForTesting() {
@@ -408,7 +410,7 @@
 
   @Test
   public void createGroupWithoutCapability_Forbidden() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.groups().create(name("newGroup"));
   }
@@ -730,7 +732,7 @@
     AccountGroup.UUID group = groupOperations.newGroup().create();
     gApi.groups().id(group.get()).addMembers(user.username);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertMembers(gApi.groups().id(group.get()).members(true), user.fullName);
   }
 
@@ -741,7 +743,7 @@
     gApi.groups().id(group1.get()).addGroups(group2.get());
     gApi.groups().id(group2.get()).addMembers(user.username);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     List<AccountInfo> listedMembers = gApi.groups().id(group1.get()).members(true);
 
     assertMembers(listedMembers);
@@ -769,7 +771,7 @@
     gApi.groups().id(ownerGroup.get()).addMembers(user.username);
     gApi.groups().id(group2.get()).addMembers(user.username);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     List<AccountInfo> listedMembers = gApi.groups().id(group1.get()).members(true);
 
     assertMembers(listedMembers, user.fullName);
@@ -830,13 +832,13 @@
     in.ownerId = adminGroupUuid().get();
     gApi.groups().create(in);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(gApi.groups().list().getAsMap()).doesNotContainKey(newGroupName);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.groups().id(newGroupName).addMembers(user.username);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(gApi.groups().list().getAsMap()).containsKey(newGroupName);
   }
 
@@ -1009,15 +1011,15 @@
     GroupInfo group = gApi.groups().create(in).get();
 
     // admin can reindex any group
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.groups().id(group.id).index();
 
     // group owner can reindex own group (group is owned by itself)
-    setApiUser(groupOwner);
+    requestScopeOperations.setApiUser(groupOwner.getId());
     gApi.groups().id(group.id).index();
 
     // user cannot reindex any group
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to index group");
     gApi.groups().id(group.id).index();
@@ -1291,7 +1293,7 @@
     gApi.groups().create(groupInput).get();
     restartAsSlave();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     List<GroupInfo> groups = gApi.groups().list().withUser(user.username).get();
     ImmutableList<String> groupNames =
         groups.stream().map(group -> group.name).collect(toImmutableList());
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index 27f737f..752d5f1 100644
--- a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.extensions.api.plugins.InstallPluginInput;
 import com.google.gerrit.extensions.api.plugins.PluginApi;
@@ -33,6 +34,7 @@
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.inject.Inject;
 import java.util.List;
 import org.junit.Test;
 
@@ -49,6 +51,8 @@
       ImmutableList.of(
           "plugin-a.js", "plugin-b.html", "plugin-c.js", "plugin-d.html", "plugin_e.js");
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   @GerritConfig(name = "plugins.allowRemoteAdmin", value = "true")
   public void pluginManagement() throws Exception {
@@ -112,7 +116,7 @@
     deprecatedInput();
 
     // Non-admin cannot disable
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     try {
       gApi.plugins().name("plugin-a").disable();
       fail("Expected AuthException");
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 6b6f85f..9267bc3 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -76,6 +77,7 @@
 
   @Inject private DynamicSet<ProjectIndexedListener> projectIndexedListeners;
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Inject
   @IndexExecutor(BATCH)
@@ -351,7 +353,7 @@
   @Test
   public void nonOwnerCannotSetConfig() throws Exception {
     ConfigInput input = createTestConfigInput();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("write refs/meta/config not permitted");
     gApi.projects().name(project.get()).config(input);
@@ -386,7 +388,7 @@
   @Test
   public void setHeadNotAllowed() throws Exception {
     gApi.projects().name(project.get()).branch("test").create(new BranchInput());
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not permitted: set HEAD on refs/heads/test");
     gApi.projects().name(project.get()).head("test");
diff --git a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
index c21798c..e428e89 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -35,11 +36,12 @@
 public class SetParentIT extends AbstractDaemonTest {
 
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void setParentNotAllowed() throws Exception {
     String parent = projectOperations.newProject().create().get();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.projects().name(project.get()).parent(parent);
   }
@@ -48,7 +50,7 @@
   @GerritConfig(name = "receive.allowProjectOwnersToChangeParent", value = "true")
   public void setParentNotAllowedForNonOwners() throws Exception {
     String parent = projectOperations.newProject().create().get();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.projects().name(project.get()).parent(parent);
   }
@@ -72,7 +74,7 @@
   @GerritConfig(name = "receive.allowProjectOwnersToChangeParent", value = "true")
   public void setParentAllowedForOwners() throws Exception {
     String parent = projectOperations.newProject().create().get();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     grant(project, "refs/*", Permission.OWNER, false, SystemGroupBackend.REGISTERED_USERS);
     gApi.projects().name(project.get()).parent(parent);
     assertThat(gApi.projects().name(project.get()).parent()).isEqualTo(parent);
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 2c0f35d..c87f7d1 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
@@ -112,9 +113,10 @@
 
 public class RevisionIT extends AbstractDaemonTest {
 
-  @Inject private GetRevisionActions getRevisionActions;
-  @Inject private DynamicSet<PatchSetWebLink> patchSetLinks;
   @Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
+  @Inject private DynamicSet<PatchSetWebLink> patchSetLinks;
+  @Inject private GetRevisionActions getRevisionActions;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void reviewTriplet() throws Exception {
@@ -215,13 +217,13 @@
     PushOneCommit.Result r = createChange();
     String changeId = project.get() + "~master~" + r.getChangeId();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     revision(r).review(ReviewInput.approve());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     revision(r).review(ReviewInput.recommend());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).reviewer(user.username).deleteVote("Code-Review");
     Optional<ApprovalInfo> crUser =
         get(changeId, DETAILED_LABELS)
@@ -236,7 +238,7 @@
 
     revision(r).submit();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     ReviewInput in = new ReviewInput();
     in.label("Code-Review", 1);
     in.message = "Still LGTM";
@@ -302,7 +304,7 @@
   @Test
   public void voteNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("is restricted");
     gApi.changes().id(r.getChange().getId().get()).current().review(ReviewInput.approve());
@@ -736,7 +738,7 @@
 
     // 'user' cherry-picks the change to a new branch, the source change's author/committer('admin')
     // will be added as a reviewer of the newly created change.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     CherryPickInput input = new CherryPickInput();
     input.message = "it goes to a new branch";
 
@@ -772,13 +774,13 @@
     // Change is created by 'admin'.
     PushOneCommit.Result r = createChange();
     // Change is approved by 'admin2'. Change is CC'd to 'user'.
-    setApiUser(accountCreator.admin2());
+    requestScopeOperations.setApiUser(accountCreator.admin2().getId());
     ReviewInput in = ReviewInput.approve();
     in.reviewer(user.email, ReviewerState.CC, true);
     gApi.changes().id(r.getChangeId()).current().review(in);
 
     // Change is cherrypicked by 'user2'.
-    setApiUser(accountCreator.user2());
+    requestScopeOperations.setApiUser(accountCreator.user2().getId());
     CherryPickInput cin = new CherryPickInput();
     cin.message = "this need to go to stable";
     cin.destination = "stable";
@@ -857,7 +859,7 @@
     input.base = dstChange.getCommit().name();
     input.message = srcChange.getCommit().getFullMessage();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(UnprocessableEntityException.class);
     exception.expectMessage(
         String.format("Commit %s does not exist on branch refs/heads/foo", input.base));
@@ -1123,7 +1125,7 @@
   public void setDescriptionNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     assertDescription(r, "");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("edit description not permitted");
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
@@ -1134,7 +1136,7 @@
     PushOneCommit.Result r = createChange();
     assertDescription(r, "");
     grant(project, "refs/heads/master", Permission.OWNER, false, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
     assertDescription(r, "test");
   }
@@ -1397,11 +1399,11 @@
     amendChange(r.getChangeId());
 
     // code-review
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
 
     // check if it's blocked to delete a vote on a non-current patch set.
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     exception.expect(MethodNotAllowedException.class);
     exception.expectMessage("Cannot access on non-current patch set");
     gApi.changes()
@@ -1420,10 +1422,10 @@
     amendChange(r.getChangeId());
 
     // code-review
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes()
         .id(r.getChangeId())
         .current()
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 53a2168..85b66c2 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
@@ -91,6 +92,7 @@
   private static final byte[] CONTENT_NEW2 = CONTENT_NEW2_STR.getBytes(UTF_8);
 
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private String changeId;
   private String changeId2;
@@ -625,11 +627,11 @@
     gApi.changes().id(changeId2).edit().publish(publishInput);
     assertThat(queryEdits()).isEmpty();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     createEmptyEditFor(changeId);
     assertThat(queryEdits()).hasSize(1);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     assertThat(queryEdits()).isEmpty();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 3d3ddc3..0fc3c80 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -52,6 +52,7 @@
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
@@ -94,6 +95,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
@@ -130,6 +132,8 @@
     HTTP
   }
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   private LabelType patchSetLock;
 
   @BeforeClass
@@ -162,7 +166,7 @@
 
   @After
   public void resetPublishCommentOnPushOption() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
     prefs.publishCommentsOnPush = false;
     gApi.accounts().id(admin.id.get()).setPreferences(prefs);
@@ -520,7 +524,7 @@
     pwi.filter = "*";
     pwi.notifyNewChanges = true;
     projectsToWatch.add(pwi);
-    setApiUser(user3);
+    requestScopeOperations.setApiUser(user3.getId());
     gApi.accounts().self().setWatchedProjects(projectsToWatch);
 
     TestAccount user2 = accountCreator.user2();
diff --git a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
index 907ad7f..61f40f7 100644
--- a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
@@ -36,6 +37,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.testing.Util;
+import com.google.inject.Inject;
 import java.util.Arrays;
 import java.util.function.Consumer;
 import org.eclipse.jgit.api.PushCommand;
@@ -52,6 +54,8 @@
 import org.junit.Test;
 
 public class PushPermissionsIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Before
   public void setUp() throws Exception {
     try (ProjectConfigUpdate u = updateProject(allProjects)) {
@@ -264,14 +268,14 @@
   @Test
   public void addPatchSetDenied() throws Exception {
     grant(project, "refs/for/refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     ChangeInput ci = new ChangeInput();
     ci.project = project.get();
     ci.branch = "master";
     ci.subject = "A change";
     Change.Id id = new Change.Id(gApi.changes().create(ci).get()._number);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     ObjectId ps1Id = forceFetch(new PatchSet.Id(id, 1).toRefName());
     ObjectId ps2Id = testRepo.amend(ps1Id).add("file", "content").create();
     PushResult r = push(ps2Id.name() + ":refs/for/master");
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index a0242dc..79a6957 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -74,9 +75,10 @@
 
 @NoHttpd
 public class RefAdvertisementIT extends AbstractDaemonTest {
-  @Inject private PermissionBackend permissionBackend;
-  @Inject private ChangeNoteUtil noteUtil;
   @Inject private AllUsersName allUsersName;
+  @Inject private ChangeNoteUtil noteUtil;
+  @Inject private PermissionBackend permissionBackend;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private AccountGroup.UUID admins;
   private AccountGroup.UUID nonInteractiveUsers;
@@ -187,7 +189,7 @@
       u.save();
     }
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertUploadPackRefs(
         "HEAD",
         psRef1,
@@ -231,7 +233,7 @@
     allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
     deny("refs/heads/branch", Permission.READ, REGISTERED_USERS);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertUploadPackRefs(
         "HEAD", psRef1, metaRef1, psRef3, metaRef3, "refs/heads/master", "refs/tags/master-tag");
   }
@@ -241,7 +243,7 @@
     deny("refs/heads/master", Permission.READ, REGISTERED_USERS);
     allow("refs/heads/branch", Permission.READ, REGISTERED_USERS);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertUploadPackRefs(
         psRef2,
         metaRef2,
@@ -259,11 +261,11 @@
     allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
 
     // Admin's edit is not visible.
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(cd3.getId().get()).edit().create();
 
     // User's edit is visible.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(cd3.getId().get()).edit().create();
 
     assertUploadPackRefs(
@@ -284,14 +286,14 @@
     allow("refs/*", Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS);
 
     // Admin's edit on change3 is visible.
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(cd3.getId().get()).edit().create();
 
     // Admin's edit on change4 is not visible since user cannot see the change.
     gApi.changes().id(cd4.getId().get()).edit().create();
 
     // User's edit is visible.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(cd3.getId().get()).edit().create();
 
     assertUploadPackRefs(
@@ -313,9 +315,9 @@
     deny("refs/heads/master", Permission.READ, REGISTERED_USERS);
     allow("refs/heads/branch", Permission.READ, REGISTERED_USERS);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(cd3.getId().get()).edit().create();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     assertUploadPackRefs(
         // Change 1 is visible due to accessDatabase capability, even though
@@ -350,7 +352,7 @@
   private void uploadPackNoSearchingChangeCacheImpl() throws Exception {
     allow("refs/heads/*", Permission.READ, REGISTERED_USERS);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     try (Repository repo = repoManager.openRepository(project)) {
       assertRefs(
           repo,
@@ -389,7 +391,7 @@
     gApi.changes().id(cd4.getId().id).delete();
     gApi.projects().name(project.get()).branch("refs/heads/branch").delete();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertUploadPackRefs(
         "HEAD",
         "refs/meta/config",
@@ -423,7 +425,7 @@
   public void receivePackRespectsVisibilityOfOpenChanges() throws Exception {
     allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
     deny("refs/heads/branch", Permission.READ, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(cd3, 1));
   }
@@ -616,7 +618,7 @@
     allow(project, "refs/*", Permission.READ, REGISTERED_USERS);
     allow(allUsersName, "refs/*", Permission.READ, REGISTERED_USERS);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     DraftInput draftInput = new DraftInput();
     draftInput.line = 1;
     draftInput.message = "nit: trailing whitespace";
@@ -636,7 +638,7 @@
     allow(project, "refs/*", Permission.READ, REGISTERED_USERS);
     allow(allUsersName, "refs/*", Permission.READ, REGISTERED_USERS);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.accounts().self().starChange(cd3.getId().toString());
     String starredChangesRef = RefNames.refsStarredChanges(cd3.getId(), user.id);
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
index fd4d7f4..9749d67 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.accounts.EmailApi;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.common.EmailInfo;
@@ -52,14 +53,15 @@
 import org.junit.Test;
 
 public class EmailIT extends AbstractDaemonTest {
-  @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
-  @Inject private ExternalIds externalIds;
-  @Inject private AuthConfig authConfig;
   @Inject private @AnonymousCowardName String anonymousCowardName;
   @Inject private @CanonicalWebUrl Provider<String> canonicalUrl;
   @Inject private @DisableReverseDnsLookup Boolean disableReverseDnsLookup;
+  @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
+  @Inject private AuthConfig authConfig;
   @Inject private EmailExpander emailExpander;
+  @Inject private ExternalIds externalIds;
   @Inject private Provider<Emails> emails;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void addEmail() throws Exception {
@@ -120,7 +122,7 @@
     createEmail(email);
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     gApi.accounts().self().email(email).setPreferred();
     assertThat(gApi.accounts().self().get().email).isEqualTo(email);
   }
@@ -139,7 +141,7 @@
                         ExternalId.SCHEME_EXTERNAL, "foo", admin.id, email)));
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     gApi.accounts().self().email(email).setPreferred();
     assertThat(gApi.accounts().self().get().email).isEqualTo(email);
   }
@@ -165,7 +167,7 @@
     createEmail(email);
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     String emailOtherCase = email.toUpperCase();
     gApi.accounts().self().email(emailOtherCase).setPreferred();
     assertThat(gApi.accounts().self().get().email).isEqualTo(email);
@@ -221,7 +223,7 @@
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
     // Get email
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     EmailApi emailApi = gApi.accounts().self().email(email);
     EmailInfo emailInfo = emailApi.get();
     assertThat(emailInfo.email).isEqualTo(email);
@@ -233,7 +235,7 @@
     assertThat(gApi.accounts().self().get().email).isEqualTo(email);
 
     // Get email again (now it's the preferred email)
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     emailApi = gApi.accounts().self().email(email);
     emailInfo = emailApi.get();
     assertThat(emailInfo.email).isEqualTo(email);
@@ -245,7 +247,7 @@
     assertThat(getEmails()).doesNotContain(email);
 
     // Now the email is no longer found
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
     emailApi = gApi.accounts().self().email(email);
     exception.expect(ResourceNotFoundException.class);
     emailApi.get();
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index 4e0def7..8e0aa01 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
@@ -90,6 +91,7 @@
   @Inject private ExternalIds externalIds;
   @Inject private ExternalIdReader externalIdReader;
   @Inject private ExternalIdNotes.Factory externalIdNotesFactory;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void getExternalIds() throws Exception {
@@ -109,7 +111,7 @@
 
   @Test
   public void getExternalIdsOfOtherUserNotAllowed() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("access database not permitted");
     gApi.accounts().id(admin.id.get()).getExternalIds();
@@ -135,7 +137,7 @@
 
   @Test
   public void deleteExternalIds() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     List<AccountExternalIdInfo> externalIds = gApi.accounts().self().getExternalIds();
 
     List<String> toDelete = new ArrayList<>();
@@ -162,7 +164,7 @@
   @Test
   public void deleteExternalIdsOfOtherUserNotAllowed() throws Exception {
     List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("access database not permitted");
     gApi.accounts()
@@ -173,7 +175,7 @@
   @Test
   public void deleteExternalIdOfOtherUserUnderOwnAccount_UnprocessableEntity() throws Exception {
     List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(UnprocessableEntityException.class);
     exception.expectMessage(String.format("External id %s does not exist", extIds.get(0).identity));
     gApi.accounts()
@@ -199,7 +201,7 @@
 
     assertThat(toDelete).hasSize(1);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     RestResponse response =
         userRestSession.post("/accounts/" + admin.id + "/external.ids:delete", toDelete);
     response.assertNoContent();
@@ -402,7 +404,7 @@
   @Test
   public void readExternalIdsWhenInvalidExternalIdsExist() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
 
     insertValidExternalIds();
     insertInvalidButParsableExternalIds();
@@ -423,7 +425,7 @@
   @Test
   public void checkConsistency() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
 
     insertValidExternalIds();
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index dd26347..2ae706a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
@@ -70,12 +71,10 @@
 
 public class ImpersonationIT extends AbstractDaemonTest {
   @Inject private AccountControl.Factory accountControlFactory;
-
   @Inject private ApprovalsUtil approvalsUtil;
-
   @Inject private ChangeMessagesUtil cmUtil;
-
   @Inject private CommentsUtil commentsUtil;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private RestSession anonRestSession;
   private TestAccount admin2;
@@ -254,7 +253,7 @@
     allowCodeReviewOnBehalfOf();
     PushOneCommit.Result r = createChange();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     DraftInput di = new DraftInput();
     di.path = Patch.COMMIT_MSG;
     di.side = Side.REVISION;
@@ -262,7 +261,7 @@
     di.message = "message";
     gApi.changes().id(r.getChangeId()).current().createDraft(di);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     ReviewInput in = new ReviewInput();
     in.onBehalfOf = user.id.toString();
     in.label("Code-Review", 1);
@@ -310,7 +309,7 @@
   @Test
   public void voteOnBehalfOfInvisibleUserNotAllowed() throws Exception {
     allowCodeReviewOnBehalfOf();
-    setApiUser(accountCreator.user2());
+    requestScopeOperations.setApiUser(accountCreator.user2().getId());
     assertThat(accountControlFactory.get().canSee(user.id)).isFalse();
 
     PushOneCommit.Result r = createChange();
@@ -391,7 +390,7 @@
   @Test
   public void submitOnBehalfOfInvisibleUserNotAllowed() throws Exception {
     allowSubmitOnBehalfOf();
-    setApiUser(accountCreator.user2());
+    requestScopeOperations.setApiUser(accountCreator.user2().getId());
     assertThat(accountControlFactory.get().canSee(user.id)).isFalse();
 
     PushOneCommit.Result r = createChange();
@@ -452,14 +451,14 @@
     allowRunAs();
     PushOneCommit.Result r = createChange();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     DraftInput di = new DraftInput();
     di.path = Patch.COMMIT_MSG;
     di.side = Side.REVISION;
     di.line = 1;
     di.message = "inline comment";
     gApi.changes().id(r.getChangeId()).current().createDraft(di);
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
 
     // Things that aren't allowed with on_behalf_of:
     //  - no labels.
@@ -481,7 +480,7 @@
     assertThat(c.author._accountId).isEqualTo(user.id.get());
     assertThat(c.message).isEqualTo(di.message);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(gApi.changes().id(r.getChangeId()).drafts()).isEmpty();
   }
 
@@ -533,7 +532,7 @@
     in.message = "Message on behalf of";
     in.label("Code-Review", 1);
 
-    setApiUser(accountCreator.user2());
+    requestScopeOperations.setApiUser(accountCreator.user2().getId());
     gApi.changes().id(r.getChangeId()).revision(r.getPatchSetId().getId()).review(in);
 
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get(MESSAGES);
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
index f9bc539..3a68323 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -29,6 +30,7 @@
 
 public class WatchedProjectsIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private static final String NEW_PROJECT_NAME = "newProjectAccess";
 
@@ -154,7 +156,7 @@
     String projectName = project.get();
 
     // Let another user watch a project
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
 
     ProjectWatchInfo pwi = new ProjectWatchInfo();
@@ -171,7 +173,7 @@
     gApi.accounts().self().deleteWatchedProjects(d);
 
     // Check that trying to delete a non-existing watch doesn't fail
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.accounts().self().deleteWatchedProjects(d);
   }
 
@@ -180,7 +182,7 @@
     String projectName = project.get();
 
     // Let another user watch a project
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
 
     ProjectWatchInfo pwi = new ProjectWatchInfo();
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
index 7f55744..dd6e1a5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.gpg.testing.TestKey;
 import com.google.gerrit.server.ServerInitiated;
@@ -41,6 +42,7 @@
  */
 public class AccountsRestApiBindingsIT extends AbstractDaemonTest {
   @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   /**
    * Account REST endpoints to be tested, each URL contains a placeholder for the account
@@ -169,7 +171,7 @@
                 u.addExternalId(
                     ExternalId.createWithEmail(name("test"), email, admin.getId(), email)));
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.accounts()
         .self()
         .putGpgKeys(ImmutableList.of(key.getPublicKeyArmored()), ImmutableList.of());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index a0e53b3..68622e2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
@@ -116,15 +117,13 @@
   }
 
   @Inject private ApprovalsUtil approvalsUtil;
-
-  @Inject private Submit submitHandler;
-
+  @Inject private DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
   @Inject private IdentifiedUser.GenericFactory userFactory;
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private Submit submitHandler;
 
-  @Inject private DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
   private RegistrationHandle onSubmitValidatorHandle;
-
   private String systemTimeZone;
 
   @Before
@@ -343,7 +342,7 @@
 
     submit(result.getChangeId(), new SubmitInput(), AuthException.class, "submit not permitted");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     submit(result.getChangeId());
   }
 
@@ -367,10 +366,10 @@
     ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
     assertThat(change.owner._accountId).isEqualTo(admin.id.get());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     submit(result.getChangeId(), new SubmitInput(), AuthException.class, "submit not permitted");
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     submit(result.getChangeId());
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
index 21ab4c4..31813e3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -31,6 +32,7 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.testing.Util;
+import com.google.inject.Inject;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -38,6 +40,7 @@
 import org.junit.Test;
 
 public abstract class AbstractSubmitByRebase extends AbstractSubmit {
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Override
   protected abstract SubmitType getSubmitType();
@@ -68,7 +71,7 @@
   }
 
   private void submitWithRebase(TestAccount submitter) throws Exception {
-    setApiUser(submitter);
+    requestScopeOperations.setApiUser(submitter.getId());
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index 60a6308..bd31c5f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ActionVisitor;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -54,9 +55,9 @@
     return submitWholeTopicEnabledConfig();
   }
 
-  @Inject private RevisionJson.Factory revisionJsonFactory;
-
   @Inject private DynamicSet<ActionVisitor> actionVisitors;
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private RevisionJson.Factory revisionJsonFactory;
 
   private RegistrationHandle visitorHandle;
 
@@ -156,25 +157,25 @@
     String change = createChangeWithTopic().getChangeId();
     approve(change);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag1 = getETag(change);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     approve(parent);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag2 = getETag(change);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     String changeWithSameTopic = createChangeWithTopic().getChangeId();
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag3 = getETag(change);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     approve(changeWithSameTopic);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag4 = getETag(change);
 
     if (isSubmitWholeTopicEnabled()) {
@@ -193,13 +194,13 @@
     String change = createChange().getChangeId();
     approve(change);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag1 = getETag(change);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     approve(parent);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     String etag2 = getETag(change);
     assertThat(etag2).isEqualTo(etag1);
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
index c925d88..621a52e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AssigneeInput;
 import com.google.gerrit.extensions.client.ReviewerState;
@@ -32,6 +33,7 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.Inject;
 import java.util.Iterator;
 import java.util.List;
 import org.eclipse.jgit.transport.RefSpec;
@@ -41,6 +43,7 @@
 
 @NoHttpd
 public class AssigneeIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @BeforeClass
   public static void setTimeForTesting() {
@@ -144,7 +147,7 @@
   @Test
   public void setAssigneeNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("not permitted");
     setAssignee(r, user.email);
@@ -154,7 +157,7 @@
   public void setAssigneeAllowedWithPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     grant(project, "refs/heads/master", Permission.EDIT_ASSIGNEE, false, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index 48cb050..3873c9d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.api.changes.DeleteChangeMessageInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -40,6 +41,7 @@
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.Inject;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -54,6 +56,8 @@
 
 @RunWith(ConfigSuite.class)
 public class ChangeMessagesIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   private String systemTimeZone;
 
   @Before
@@ -139,7 +143,7 @@
   @Test
   public void deleteCannotBeAppliedWithoutAdministrateServerCapability() throws Exception {
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     try {
       deleteOneChangeMessage(changeNum, 0, user, "spam");
@@ -153,7 +157,7 @@
   public void deleteCanBeAppliedWithAdministrateServerCapability() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     deleteOneChangeMessage(changeNum, 0, user, "spam");
   }
 
@@ -209,22 +213,22 @@
   private int createOneChangeWithMultipleChangeMessagesInHistory() throws Exception {
     // Creates the following commit history on the meta branch of the test change.
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     // Commit 1: create a change.
     PushOneCommit.Result result = createChange();
     String changeId = result.getChangeId();
     // Commit 2: post a review with message "message 1".
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     addOneReview(changeId, "message 1");
     // Commit 3: amend a new patch set.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     amendChange(changeId);
     // Commit 4: post a review with message "message 2".
     addOneReview(changeId, "message 2");
     // Commit 5: amend a new patch set.
     amendChange(changeId);
     // Commit 6: approve the change.
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).current().review(ReviewInput.approve());
     // commit 7: submit the change.
     gApi.changes().id(changeId).current().submit();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index 73416ee..993c10e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -32,13 +33,14 @@
 import org.junit.Test;
 
 public class ChangeOwnerIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private TestAccount user2;
-  @Inject private ProjectOperations projectOperations;
 
   @Before
   public void setUp() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     user2 = accountCreator.user2();
   }
 
@@ -64,22 +66,22 @@
 
   @Test
   public void testChangeOwner_OwnerACLGrantedOnParentProject() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     grantApproveToChangeOwner(project);
     Project.NameKey child = projectOperations.newProject().parent(project).create();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     TestRepository<InMemoryRepository> childRepo = cloneProject(child, user);
     approve(user, createMyChange(childRepo));
   }
 
   @Test
   public void testChangeOwner_BlockedOnParentProject() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     blockApproveForChangeOwner(project);
     Project.NameKey child = projectOperations.newProject().parent(project).create();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     grantApproveToAll(child);
     TestRepository<InMemoryRepository> childRepo = cloneProject(child, user);
     String changeId = createMyChange(childRepo);
@@ -93,11 +95,11 @@
 
   @Test
   public void testChangeOwner_BlockedOnParentProjectAndExclusiveAllowOnChild() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     blockApproveForChangeOwner(project);
     Project.NameKey child = projectOperations.newProject().parent(project).create();
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     grantExclusiveApproveToAll(child);
     TestRepository<InMemoryRepository> childRepo = cloneProject(child, user);
     String changeId = createMyChange(childRepo);
@@ -110,7 +112,7 @@
   }
 
   private void approve(TestAccount a, String changeId) throws Exception {
-    Context old = setApiUser(a);
+    Context old = requestScopeOperations.setApiUser(a.getId());
     try {
       gApi.changes().id(changeId).current().review(ReviewInput.approve());
     } finally {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index 7b302c4..13c76e3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -36,12 +37,14 @@
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
 import java.lang.reflect.Type;
 import java.util.List;
 import org.junit.Before;
 import org.junit.Test;
 
 public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
   public void setUp() throws Exception {
@@ -195,9 +198,9 @@
       // Review change as user
       ReviewInput reviewInput = new ReviewInput();
       reviewInput.message = "I have a comment";
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       revision(r).review(reviewInput);
-      setApiUser(admin);
+      requestScopeOperations.setApiUser(admin.getId());
 
       sender.clear();
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 069607a..dec839c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
@@ -64,7 +65,8 @@
 
 public class ChangeReviewersIT extends AbstractDaemonTest {
 
-  @Inject protected GroupOperations groupOperations;
+  @Inject private GroupOperations groupOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void addGroupAsReviewer() throws Exception {
@@ -499,7 +501,7 @@
 
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     // NoteDb adds reviewer to a change on every review.
     gApi.changes().id(changeId).current().review(ReviewInput.dislike());
 
@@ -657,9 +659,9 @@
     PushOneCommit.Result r = createChange();
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).current().review(new ReviewInput().label("Code-Review", 1));
-    setApiUser(newUser);
+    requestScopeOperations.setApiUser(newUser.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
@@ -676,7 +678,7 @@
 
     gApi.changes().id(r.getChangeId()).addReviewer(user.email);
     assertThatUserIsOnlyReviewer(r.getChangeId());
-    setApiUser(newUser);
+    requestScopeOperations.setApiUser(newUser.getId());
     gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
     assertThat(gApi.changes().id(r.getChangeId()).get().reviewers).isEmpty();
   }
@@ -687,7 +689,7 @@
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
 
     gApi.changes().id(r.getChangeId()).addReviewer(user.email);
-    setApiUser(newUser);
+    requestScopeOperations.setApiUser(newUser.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
@@ -702,7 +704,7 @@
     input.reviewer = user.email;
     input.state = ReviewerState.CC;
     gApi.changes().id(r.getChangeId()).addReviewer(input);
-    setApiUser(newUser);
+    requestScopeOperations.setApiUser(newUser.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("remove reviewer not permitted");
     gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
index 29946b4..b6547f0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -31,6 +32,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.project.testing.Util;
+import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -43,6 +45,8 @@
 import org.junit.Test;
 
 public class ConfigChangeIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Before
   public void setUp() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
@@ -52,7 +56,7 @@
       u.save();
     }
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     fetchRefsMetaConfig();
   }
 
@@ -96,13 +100,13 @@
   @Test
   @TestProjectInput(cloneAs = "user")
   public void onlyAdminMayUpdateProjectParent() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     ProjectInput parent = new ProjectInput();
     parent.name = name("parent");
     parent.permissionsOnly = true;
     gApi.projects().create(parent);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     Config cfg = readProjectConfig();
     assertThat(cfg.getString("access", null, "inheritFrom")).isAnyOf(null, allProjects.get());
     cfg.setString("access", null, "inheritFrom", parent.name);
@@ -132,7 +136,7 @@
     assertThat(readProjectConfig().getString("access", null, "inheritFrom"))
         .isAnyOf(null, allProjects.get());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(id).current().submit();
     assertThat(gApi.changes().id(id).info().status).isEqualTo(ChangeStatus.MERGED);
     assertThat(gApi.projects().name(project.get()).get().parent).isEqualTo(parent.name);
@@ -142,7 +146,7 @@
 
   @Test
   public void rejectDoubleInheritance() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     // Create separate projects to test the config
     Project.NameKey parent = createProjectOverAPI("projectToInheritFrom", null, true, null);
     Project.NameKey child = createProjectOverAPI("projectWithMalformedConfig", null, true, null);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 66e429a..08daa18 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -50,6 +51,7 @@
 import com.google.gerrit.server.submit.ChangeAlreadyMergedException;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.Inject;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -64,6 +66,8 @@
 import org.junit.Test;
 
 public class CreateChangeIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @BeforeClass
   public static void setTimeForTesting() {
     TestTimeUtil.resetWithClockStep(1, SECONDS);
@@ -149,11 +153,11 @@
 
   @Test
   public void notificationsOnChangeCreation() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     watch(project.get());
 
     // check that watcher is notified
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
 
     List<Message> messages = sender.getMessages();
@@ -502,7 +506,7 @@
       assertThat(o.signedOffBy).isNull();
     }
 
-    resetCurrentApiUser();
+    requestScopeOperations.resetCurrentApiUser();
   }
 
   private ChangeInput newMergeChangeInput(String targetBranch, String sourceRef, String strategy) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
index 7e5ebdb..1c1c455 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -29,12 +30,15 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import org.junit.Test;
 
 public class DeleteVoteIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void deleteVoteOnChange() throws Exception {
     deleteVote(false);
@@ -51,7 +55,7 @@
 
     PushOneCommit.Result r2 = amendChange(r.getChangeId());
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     recommend(r.getChangeId());
 
     sender.clear();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index 47ec0d2..2b0ba4e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -25,12 +25,14 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.Inject;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -47,6 +49,8 @@
     TestTimeUtil.useSystemTime();
   }
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void getNoHashtags() throws Exception {
     // Get on a change with no hashtags returns an empty list.
@@ -253,7 +257,7 @@
   @Test
   public void addHashtagWithoutPermissionNotAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("edit hashtags not permitted");
     addHashtags(r, "MyHashtag");
@@ -263,7 +267,7 @@
   public void addHashtagWithPermissionAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
     grant(project, "refs/heads/master", Permission.EDIT_HASHTAGS, false, REGISTERED_USERS);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     addHashtags(r, "MyHashtag");
     assertThatGet(r).containsExactly("MyHashtag");
     assertMessage(r, "Hashtag added: MyHashtag");
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index f4afc37..cf5136b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -34,9 +35,9 @@
 import org.junit.Test;
 
 public class IndexChangeIT extends AbstractDaemonTest {
-
-  @Inject protected GroupOperations groupOperations;
+  @Inject private GroupOperations groupOperations;
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void indexChange() throws Exception {
@@ -80,32 +81,32 @@
     String changeId = result.getChangeId();
 
     // User can see the change and it is mergeable
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     List<ChangeInfo> changes = gApi.changes().query(changeId).get();
     assertThat(changes).hasSize(1);
     assertThat(changes.get(0).mergeable).isNotNull();
 
     // Other user can see the change and it is mergeable
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.getId());
     changes = gApi.changes().query(changeId).get();
     assertThat(changes).hasSize(1);
     assertThat(changes.get(0).mergeable).isTrue();
 
     // Remove the user from the group so they can no longer see the project
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.groups().id(group).removeMembers("user");
 
     // User can no longer see the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     changes = gApi.changes().query(changeId).get();
     assertThat(changes).isEmpty();
 
     // Reindex the change
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(changeId).index();
 
     // Other user can still see the change and it is still mergeable
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.getId());
     changes = gApi.changes().query(changeId).get();
     assertThat(changes).hasSize(1);
     assertThat(changes.get(0).mergeable).isTrue();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
index 8d12886..7d97967 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
@@ -38,6 +39,7 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.testing.Util;
+import com.google.inject.Inject;
 import java.util.Arrays;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -46,6 +48,8 @@
 
 @NoHttpd
 public class MoveChangeIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void moveChangeWithShortRef() throws Exception {
     // Move change to a different branch using short ref name
@@ -182,7 +186,7 @@
         r.getChange().change().getDest().get(),
         Permission.ABANDON,
         systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("move not permitted");
     move(r.getChangeId(), newBranch.get());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index a757044..94090a6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
@@ -54,6 +55,7 @@
 
 public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -628,7 +630,7 @@
     gApi.changes().id(changeResult.getChangeId()).move(secretBranch.get());
     block(secretBranch.get(), "read", ANONYMOUS_USERS);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     // Verify that user cannot see the first change.
     try {
@@ -675,7 +677,7 @@
     // Mark the first change private so that it's not visible to user.
     gApi.changes().id(changeResult.getChangeId()).setPrivate(true, "nobody should see this");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     // Verify that user cannot see the first change.
     try {
@@ -735,7 +737,7 @@
     // Mark change2a private so that it's not visible to user.
     gApi.changes().id(change2a.getChangeId()).setPrivate(true, "nobody should see this");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     // Verify that user cannot see change2a
     try {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 981ba34..af206f1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -44,9 +45,10 @@
 import org.junit.Test;
 
 public class SuggestReviewersIT extends AbstractDaemonTest {
-  @Inject private ProjectOperations projectOperations;
   @Inject private AccountOperations accountOperations;
   @Inject private GroupOperations groupOperations;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private AccountGroup.UUID group1;
   private AccountGroup.UUID group2;
@@ -133,16 +135,16 @@
     assertThat(reviewers).hasSize(1);
     assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.getId());
     reviewers = suggestReviewers(changeId, user2.fullName, 2);
     assertThat(reviewers).isEmpty();
 
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.getId());
     reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).hasSize(1);
     assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
 
-    setApiUser(user3);
+    requestScopeOperations.setApiUser(user3.getId());
     reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).hasSize(1);
     assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName);
@@ -153,7 +155,7 @@
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    setApiUser(user3);
+    requestScopeOperations.setApiUser(user3.getId());
     block("refs/*", "read", ANONYMOUS_USERS);
     allow("refs/*", "read", group1);
     reviewers = suggestReviewers(changeId, user2.username, 2);
@@ -166,11 +168,12 @@
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.getId());
     reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).isEmpty();
 
-    setApiUser(user1); // Clear cached group info.
+    // Clear cached group info.
+    requestScopeOperations.setApiUser(user1.getId());
     allowGlobalCapabilities(group1, GlobalCapability.VIEW_ALL_ACCOUNTS);
     reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).hasSize(1);
@@ -330,22 +333,22 @@
     TestAccount reviewer1 = user("customuser2", "User2");
     TestAccount reviewer2 = user("customuser3", "User3");
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.getId());
     String changeId1 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.getId());
     reviewChange(changeId1);
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.getId());
     String changeId2 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.getId());
     reviewChange(changeId2);
 
-    setApiUser(reviewer2);
+    requestScopeOperations.setApiUser(reviewer2.getId());
     reviewChange(changeId2);
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.getId());
     String changeId3 = createChangeFromApi();
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId3, null, 4);
     assertThat(reviewers.stream().map(r -> r.account._accountId).collect(toList()))
@@ -363,7 +366,7 @@
   @Test
   public void defaultReviewerSuggestionOnFirstChange() throws Exception {
     TestAccount user1 = user("customuser1", "User1");
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.getId());
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChange().getChangeId(), "", 4);
     assertThat(reviewers).isEmpty();
   }
@@ -382,23 +385,23 @@
     TestAccount userWhoLooksForSuggestions = user("customuser5", fullName);
 
     // Create a change as userWhoOwns and add some reviews
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.getId());
     String changeId1 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.getId());
     reviewChange(changeId1);
 
-    setApiUser(user1);
+    requestScopeOperations.setApiUser(user1.getId());
     String changeId2 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.getId());
     reviewChange(changeId2);
 
-    setApiUser(reviewer2);
+    requestScopeOperations.setApiUser(reviewer2.getId());
     reviewChange(changeId2);
 
     // Create a comment as a different user
-    setApiUser(userWhoComments);
+    requestScopeOperations.setApiUser(userWhoComments.getId());
     ReviewInput ri = new ReviewInput();
     ri.message = "Test";
     gApi.changes().id(changeId1).revision(1).review(ri);
@@ -406,7 +409,7 @@
     // Create a change as a new user to assert that we receive the correct
     // ranking
 
-    setApiUser(userWhoLooksForSuggestions);
+    requestScopeOperations.setApiUser(userWhoLooksForSuggestions.getId());
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChangeFromApi(), "Pri", 4);
     assertThat(reviewers.stream().map(r -> r.account._accountId).collect(toList()))
         .containsExactly(
@@ -425,25 +428,25 @@
     TestAccount reviewer1 = user("customuser2", fullName);
     TestAccount reviewer2 = user("customuser3", fullName);
 
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.getId());
     String changeId1 = createChangeFromApi();
 
-    setApiUser(reviewer1);
+    requestScopeOperations.setApiUser(reviewer1.getId());
     reviewChange(changeId1);
 
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.getId());
     String changeId2 = createChangeFromApi(newProject);
 
-    setApiUser(reviewer2);
+    requestScopeOperations.setApiUser(reviewer2.getId());
     reviewChange(changeId2);
 
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.getId());
     String changeId3 = createChangeFromApi(newProject);
 
-    setApiUser(reviewer2);
+    requestScopeOperations.setApiUser(reviewer2.getId());
     reviewChange(changeId3);
 
-    setApiUser(userWhoOwns);
+    requestScopeOperations.setApiUser(userWhoOwns.getId());
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(createChangeFromApi(), "Prim", 4);
 
     // Assert that reviewer1 is on top, even though reviewer2 has more reviews
@@ -489,7 +492,7 @@
     String secondaryEmail = "foo.secondary@example.com";
     createAccountWithSecondaryEmail("foo", secondaryEmail);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     List<SuggestedReviewerInfo> reviewers =
         suggestReviewers(createChange().getChangeId(), secondaryEmail, 4);
     assertThat(reviewers).isEmpty();
@@ -509,7 +512,7 @@
     assertThat(Iterables.getOnlyElement(reviewers).account.secondaryEmails)
         .containsExactly(secondaryEmail);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     reviewers = suggestReviewers(createChange().getChangeId(), "foo", 4);
     assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
     assertThat(Iterables.getOnlyElement(reviewers).account.secondaryEmails).isNull();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
index 02e82d8..259c4d2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -33,9 +34,11 @@
 import org.junit.Test;
 
 public class WorkInProgressByDefaultIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   private Project.NameKey project1;
   private Project.NameKey project2;
-  @Inject private ProjectOperations projectOperations;
 
   @Before
   public void setUp() throws Exception {
@@ -45,7 +48,7 @@
 
   @After
   public void tearDown() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
     prefs.workInProgressByDefault = false;
     gApi.accounts().id(admin.id.get()).setPreferences(prefs);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index a51af89..d1a80c7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
@@ -69,10 +70,11 @@
 
   private static final String LABEL_CODE_REVIEW = "Code-Review";
 
-  private Project.NameKey newProjectName;
-
   @Inject private DynamicSet<FileHistoryWebLink> fileHistoryWebLinkDynamicSet;
-  @Inject protected ProjectOperations projectOperations;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
+  private Project.NameKey newProjectName;
 
   @Before
   public void setUp() throws Exception {
@@ -169,7 +171,7 @@
   public void createAccessChange() throws Exception {
     allow(newProjectName, RefNames.REFS_CONFIG, Permission.READ, REGISTERED_USERS);
     // User can see the branch
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     pApi().branch("refs/heads/master").get();
 
     ProjectAccessInput accessInput = newProjectAccessInput();
@@ -184,7 +186,7 @@
     accessSection.permissions.put(Permission.READ, read);
     accessInput.add.put(REFS_HEADS, accessSection);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     ChangeInfo out = pApi().accessChange(accessInput);
 
     assertThat(out.project).isEqualTo(newProjectName.get());
@@ -192,7 +194,7 @@
     assertThat(out.status).isEqualTo(ChangeStatus.NEW);
     assertThat(out.submitted).isNull();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
 
     ChangeInfo c = gApi.changes().id(out._number).get(MESSAGES);
     assertThat(c.messages.stream().map(m -> m.message)).containsExactly("Uploaded patch set 1");
@@ -203,7 +205,7 @@
     gApi.changes().id(out._number).current().submit();
 
     // check that the change took effect.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     try {
       BranchInfo info = pApi().branch("refs/heads/master").get();
       fail("wanted failure, got " + newGson().toJson(info));
@@ -214,16 +216,16 @@
     // Restore.
     accessInput.add.clear();
     accessInput.remove.put(REFS_HEADS, accessSection);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     out = pApi().accessChange(accessInput);
 
     gApi.changes().id(out._number).current().review(reviewIn);
     gApi.changes().id(out._number).current().submit();
 
     // Now it works again.
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     pApi().branch("refs/heads/master").get();
   }
 
@@ -323,7 +325,7 @@
     accessInput.add.put(REFS_ALL, accessSectionInfo);
     pApi().access(accessInput);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(ResourceNotFoundException.class);
     pApi().access();
   }
@@ -343,7 +345,7 @@
     AccessSectionInfo accessSectionInfoToApply = createDefaultAccessSectionInfo();
     accessInfoToApply.add.put(REFS_HEADS, accessSectionInfoToApply);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(ResourceNotFoundException.class);
     pApi().access();
   }
@@ -390,7 +392,7 @@
     assertThat(owners.includes).isNull();
 
     // PROJECT_OWNERS is invisible to anonymous user, but GetAccess disregards visibility.
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     ProjectAccessInfo anonResult = pApi().access();
     assertThat(anonResult.groups.keySet())
         .containsExactly(
@@ -406,7 +408,7 @@
     ProjectAccessInput accessInput = newProjectAccessInput();
     accessInput.parent = newParentProjectName;
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     exception.expectMessage("administrate server not permitted");
     pApi().access(accessInput);
@@ -433,7 +435,7 @@
 
     accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.projects().name(allProjects.get()).access(accessInput);
   }
@@ -489,7 +491,7 @@
 
     accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.projects().name(allProjects.get()).access(accessInput);
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 0aaf016..8943125 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -22,6 +22,7 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
@@ -33,10 +34,13 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.inject.Inject;
 import org.junit.Before;
 import org.junit.Test;
 
 public class CreateBranchIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   private Branch.NameKey testBranch;
 
   @Before
@@ -59,7 +63,7 @@
 
   @Test
   public void createBranch_Forbidden() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertCreateFails(testBranch, AuthException.class, "not permitted: create on refs/heads/test");
   }
 
@@ -77,7 +81,7 @@
   @Test
   public void createBranchByProjectOwner() throws Exception {
     grantOwner();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertCreateSucceeds(testBranch);
   }
 
@@ -91,7 +95,7 @@
   public void createBranchByProjectOwnerCreateReferenceBlocked_Forbidden() throws Exception {
     grantOwner();
     blockCreateReference();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertCreateFails(testBranch, AuthException.class, "not permitted: create on refs/heads/test");
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 023c540..cd29bf8 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
@@ -48,6 +49,7 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
 import java.util.Collections;
 import java.util.Optional;
 import java.util.Set;
@@ -70,6 +72,8 @@
 import org.junit.Test;
 
 public class CreateProjectIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void createProjectHttp() throws Exception {
     String newProjectName = name("newProject");
@@ -299,7 +303,7 @@
   public void createProjectWithCapability() throws Exception {
     allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
     try {
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       ProjectInput in = new ProjectInput();
       in.name = name("newProject");
       ProjectInfo p = gApi.projects().create(in).get();
@@ -312,7 +316,7 @@
 
   @Test
   public void createProjectWithoutCapability_Forbidden() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     ProjectInput in = new ProjectInput();
     in.name = name("newProject");
     assertCreateFails(in, AuthException.class);
@@ -331,7 +335,7 @@
     parent.setState(com.google.gerrit.extensions.client.ProjectState.HIDDEN);
     allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
     try {
-      setApiUser(user);
+      requestScopeOperations.setApiUser(user.getId());
       ProjectInput in = new ProjectInput();
       in.name = name("newProject");
       ProjectInfo p = gApi.projects().create(in).get();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 31cf3fe..8ca6b75 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -37,6 +38,7 @@
 
 public class DeleteBranchIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private Branch.NameKey testBranch;
 
@@ -49,7 +51,7 @@
 
   @Test
   public void deleteBranch_Forbidden() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteForbidden(testBranch);
   }
 
@@ -61,7 +63,7 @@
   @Test
   public void deleteBranchByProjectOwner() throws Exception {
     grantOwner();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteSucceeds(testBranch);
   }
 
@@ -75,21 +77,21 @@
   public void deleteBranchByProjectOwnerForcePushBlocked_Forbidden() throws Exception {
     grantOwner();
     blockForcePush();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteForbidden(testBranch);
   }
 
   @Test
   public void deleteBranchByUserWithForcePushPermission() throws Exception {
     grantForcePush();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteSucceeds(testBranch);
   }
 
   @Test
   public void deleteBranchByUserWithDeletePermission() throws Exception {
     grantDelete();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteSucceeds(testBranch);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
index c1bd8f1..a85004e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
@@ -32,6 +33,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.inject.Inject;
 import java.util.HashMap;
 import java.util.List;
 import org.eclipse.jgit.lib.Repository;
@@ -44,6 +46,8 @@
   private static final ImmutableList<String> BRANCHES =
       ImmutableList.of("refs/heads/test-1", "refs/heads/test-2", "test-3", "refs/meta/foo");
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Before
   public void setUp() throws Exception {
     allow("refs/*", Permission.CREATE, REGISTERED_USERS);
@@ -70,14 +74,14 @@
 
     DeleteBranchesInput input = new DeleteBranchesInput();
     input.branches = branchToDelete;
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     try {
       project().deleteBranches(input);
       fail("Expected AuthException");
     } catch (AuthException e) {
       assertThat(e).hasMessageThat().isEqualTo("not permitted: delete on refs/heads/test-1");
     }
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     assertBranches(BRANCHES);
   }
 
@@ -85,14 +89,14 @@
   public void deleteMultiBranchesWithoutPermissionForbidden() throws Exception {
     DeleteBranchesInput input = new DeleteBranchesInput();
     input.branches = BRANCHES;
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     try {
       project().deleteBranches(input);
       fail("Expected ResourceConflictException");
     } catch (ResourceConflictException e) {
       assertThat(e).hasMessageThat().isEqualTo(errorMessageForBranches(BRANCHES));
     }
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     assertBranches(BRANCHES);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
index 3ae0b44..27de4b1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
@@ -21,18 +21,22 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.TagApi;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.api.projects.TagInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.inject.Inject;
 import org.junit.Before;
 import org.junit.Test;
 
 public class DeleteTagIT extends AbstractDaemonTest {
   private static final String TAG = "refs/tags/test";
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Before
   public void setUp() throws Exception {
     tag().create(new TagInput());
@@ -40,7 +44,7 @@
 
   @Test
   public void deleteTag_Forbidden() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteForbidden();
   }
 
@@ -52,7 +56,7 @@
   @Test
   public void deleteTagByProjectOwner() throws Exception {
     grantOwner();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteSucceeds();
   }
 
@@ -66,21 +70,21 @@
   public void deleteTagByProjectOwnerForcePushBlocked_Forbidden() throws Exception {
     grantOwner();
     blockForcePush();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteForbidden();
   }
 
   @Test
   public void deleteTagByUserWithForcePushPermission() throws Exception {
     grantForcePush();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteSucceeds();
   }
 
   @Test
   public void deleteTagByUserWithDeletePermission() throws Exception {
     grantDelete();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertDeleteSucceeds();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
index 5691e36..6d8689a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
@@ -23,11 +23,13 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.projects.DeleteTagsInput;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.api.projects.TagInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.inject.Inject;
 import java.util.HashMap;
 import java.util.List;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -39,6 +41,8 @@
   private static final ImmutableList<String> TAGS =
       ImmutableList.of("refs/tags/test-1", "refs/tags/test-2", "refs/tags/test-3", "test-4");
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Before
   public void setUp() throws Exception {
     for (String name : TAGS) {
@@ -61,14 +65,14 @@
   public void deleteTagsForbidden() throws Exception {
     DeleteTagsInput input = new DeleteTagsInput();
     input.tags = TAGS;
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     try {
       project().deleteTags(input);
       fail("Expected ResourceConflictException");
     } catch (ResourceConflictException e) {
       assertThat(e).hasMessageThat().isEqualTo(errorMessageForTags(TAGS));
     }
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     assertTags(TAGS);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index b46b087..7f2b35e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -20,15 +20,19 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.inject.Inject;
 import org.junit.Test;
 
 @NoHttpd
 public class ListBranchesIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void listBranchesOfNonExistingProject_NotFound() throws Exception {
     exception.expect(ResourceNotFoundException.class);
@@ -38,7 +42,7 @@
   @Test
   public void listBranchesOfNonVisibleProject_NotFound() throws Exception {
     blockRead("refs/*");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(ResourceNotFoundException.class);
     gApi.projects().name(project.get()).branches().get();
   }
@@ -70,7 +74,7 @@
     blockRead("refs/heads/dev");
     String master = pushTo("refs/heads/master").getCommit().name();
     pushTo("refs/heads/dev");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     // refs/meta/config is hidden since user is no project owner
     assertRefs(
         ImmutableList.of(
@@ -83,7 +87,7 @@
     blockRead("refs/heads/master");
     pushTo("refs/heads/master");
     String dev = pushTo("refs/heads/dev").getCommit().name();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     // refs/meta/config is hidden since user is no project owner
     assertRefs(ImmutableList.of(branch("refs/heads/dev", dev, false)), list().get());
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index 7009e76..14dbffb 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
@@ -43,6 +44,7 @@
 @Sandboxed
 public class ListProjectsIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void listProjects() throws Exception {
@@ -54,7 +56,7 @@
 
   @Test
   public void listProjectsFiltersInvisibleProjects() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     assertThatNameList(gApi.projects().list().get()).contains(project);
 
     try (ProjectConfigUpdate u = updateProject(project)) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 58acbfb..c0f3732 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
 import com.google.gerrit.extensions.api.projects.TagApi;
@@ -33,6 +34,7 @@
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.inject.Inject;
 import java.sql.Timestamp;
 import java.util.List;
 import org.junit.Test;
@@ -56,6 +58,8 @@
           + "=XFeC\n"
           + "-----END PGP SIGNATURE-----";
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void listTagsOfNonExistingProject() throws Exception {
     exception.expect(ResourceNotFoundException.class);
@@ -71,7 +75,7 @@
   @Test
   public void listTagsOfNonVisibleProject() throws Exception {
     blockRead("refs/*");
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(ResourceNotFoundException.class);
     gApi.projects().name(project.get()).tags().get();
   }
@@ -187,7 +191,7 @@
     assertThat(result.canDelete).isTrue();
     assertThat(result.created).isEqualTo(timestamp(r));
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     result = tag(input.ref).get();
     assertThat(result.canDelete).isNull();
 
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index f726669..0a4d972 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.DeleteCommentInput;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -76,20 +77,17 @@
 
 @NoHttpd
 public class CommentsIT extends AbstractDaemonTest {
-
-  @Inject private Provider<ChangesCollection> changes;
-
-  @Inject private Provider<PostReview> postReview;
-
-  @Inject private FakeEmailSender email;
-
   @Inject private ChangeNoteUtil noteUtil;
+  @Inject private FakeEmailSender email;
+  @Inject private Provider<ChangesCollection> changes;
+  @Inject private Provider<PostReview> postReview;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private final Integer[] lines = {0, 1};
 
   @Before
   public void setUp() {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
   }
 
   @Test
@@ -445,7 +443,7 @@
             .create(admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
             .to("refs/for/master");
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     addDraft(
         r1.getChangeId(),
         r1.getCommit().getName(),
@@ -455,13 +453,13 @@
         r2.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "typo: content"));
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     addDraft(
         r2.getChangeId(),
         r2.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "+1, please fix"));
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     Map<String, List<CommentInfo>> actual = gApi.changes().id(r1.getChangeId()).drafts();
     assertThat(actual.keySet()).containsExactly(FILE_NAME);
     List<CommentInfo> comments = actual.get(FILE_NAME);
@@ -569,11 +567,11 @@
         other.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "unrelated comment"));
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     // Drafts by other users aren't returned.
     addDraft(
         r2.getChangeId(), r2.getCommit().getName(), newDraft(FILE_NAME, Side.REVISION, 2, "oops"));
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     ReviewInput reviewInput = new ReviewInput();
     reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
@@ -766,7 +764,7 @@
     String uuid = commentsMap.get(targetComment.path).get(0).id;
     DeleteCommentInput input = new DeleteCommentInput("contains confidential information");
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.changes().id(result.getChangeId()).current().comment(uuid).delete(input);
   }
@@ -837,7 +835,7 @@
     // PS4 has comments [c7, c8].
     assertThat(getRevisionComments(changeId, ps4)).hasSize(2);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     for (int i = 0; i < commentsBeforeDelete.size(); i++) {
       List<RevCommit> commitsBeforeDelete = getChangeMetaCommitsInReverseOrder(id);
 
@@ -907,7 +905,7 @@
 
     List<RevCommit> commitsBeforeDelete = getChangeMetaCommitsInReverseOrder(id);
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     for (int i = 0; i < 3; i++) {
       DeleteCommentInput input = new DeleteCommentInput("delete comment 2, iteration: " + i);
       gApi.changes().id(changeId).revision(ps1).comment(uuid).delete(input);
diff --git a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index ac0a499..b581977 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -26,13 +26,21 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.gerrit.server.restapi.change.GetRelated;
@@ -44,6 +52,9 @@
 import com.google.gerrit.testing.TestTimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.junit.TestRepository;
@@ -65,6 +76,10 @@
     return cfg;
   }
 
+  @Inject private AccountOperations accountOperations;
+  @Inject private GroupOperations groupOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   private String systemTimeZone;
 
   @Before
@@ -574,6 +589,35 @@
     assertRelated(cd.change().currentPatchSetId());
   }
 
+  @Test
+  public void getRelatedManyChanges() throws Exception {
+    List<ObjectId> commitIds = new ArrayList<>();
+    for (int i = 1; i <= 5; i++) {
+      commitIds.add(commitBuilder().add(i + ".txt", "i").message("subject: " + i).create().copy());
+    }
+    pushHead(testRepo, "refs/for/master", false);
+
+    List<RelatedChangeAndCommitInfo> expected = new ArrayList<>(commitIds.size());
+    for (ObjectId commitId : commitIds) {
+      expected.add(changeAndCommit(getPatchSetId(commitId), commitId, 1));
+    }
+    Collections.reverse(expected);
+
+    PatchSet.Id lastPsId = getPatchSetId(Iterables.getLast(commitIds));
+    assertRelated(lastPsId, expected);
+
+    Account.Id accountId = accountOperations.newAccount().create();
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().addMember(accountId).create();
+    try (ProjectConfigUpdate u = updateProject(allProjects)) {
+      PermissionRule rule = Util.allow(u.getConfig(), GlobalCapability.QUERY_LIMIT, groupUuid);
+      rule.setRange(0, 2);
+      u.save();
+    }
+    requestScopeOperations.setApiUser(accountId);
+
+    assertRelated(lastPsId, expected);
+  }
+
   private RevCommit parseBody(RevCommit c) throws Exception {
     testRepo.getRevWalk().parseBody(c);
     return c;
@@ -618,13 +662,18 @@
 
   private void assertRelated(PatchSet.Id psId, RelatedChangeAndCommitInfo... expected)
       throws Exception {
+    assertRelated(psId, Arrays.asList(expected));
+  }
+
+  private void assertRelated(PatchSet.Id psId, List<RelatedChangeAndCommitInfo> expected)
+      throws Exception {
     List<RelatedChangeAndCommitInfo> actual =
         gApi.changes().id(psId.getParentKey().get()).revision(psId.get()).related().changes;
-    assertThat(actual).named("related to " + psId).hasSize(expected.length);
+    assertThat(actual).named("related to " + psId).hasSize(expected.size());
     for (int i = 0; i < actual.size(); i++) {
       String name = "index " + i + " related to " + psId;
       RelatedChangeAndCommitInfo a = actual.get(i);
-      RelatedChangeAndCommitInfo e = expected[i];
+      RelatedChangeAndCommitInfo e = expected.get(i);
       assertThat(a.project).named("project of " + name).isEqualTo(e.project);
       assertThat(a._changeNumber).named("change ID of " + name).isEqualTo(e._changeNumber);
       // Don't bother checking changeId; assume _changeNumber is sufficient.
diff --git a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
index ddc3905..389859c 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -44,6 +45,7 @@
   }
 
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void doesNotIncludeCurrentFiles() throws Exception {
@@ -104,7 +106,7 @@
     RevCommit b = commitBuilder().add("b", "1").message("change 2").create();
     pushHead(testRepo, "refs/for/master", false);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     assertSubmittedTogether(getChangeId(a));
     assertSubmittedTogether(getChangeId(b), getChangeId(b), getChangeId(a));
   }
@@ -143,7 +145,7 @@
     pushHead(testRepo, "refs/for/master/" + name("topic"), false);
     String id2 = getChangeId(b);
 
-    setApiUserAnonymous();
+    requestScopeOperations.setApiUserAnonymous();
     if (isSubmitWholeTopicEnabled()) {
       assertSubmittedTogether(id1, id2, id1);
       assertSubmittedTogether(id2, id2, id1);
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java b/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
index 218fd30..79cb097 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
@@ -18,17 +18,20 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.mail.MailMessage;
+import com.google.inject.Inject;
 import java.time.Instant;
 import java.util.HashMap;
 import org.junit.Ignore;
 
 @Ignore
 public class AbstractMailIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   protected MailMessage.Builder messageBuilderWithDefaultFields() {
     MailMessage.Builder b = MailMessage.builder();
@@ -54,7 +57,7 @@
     String changeId = r.getChangeId();
 
     // Review it
-    setApiUser(reviewer);
+    requestScopeOperations.setApiUser(reviewer.getId());
     ReviewInput input = new ReviewInput();
     input.message = "I have two comments";
     input.comments = new HashMap<>();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 58a1771..4b6e8c6 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -31,6 +31,7 @@
 import com.google.common.truth.Truth;
 import com.google.gerrit.acceptance.AbstractNotificationTest;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
@@ -52,10 +53,13 @@
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.restapi.change.PostReview;
+import com.google.inject.Inject;
 import org.junit.Before;
 import org.junit.Test;
 
 public class ChangeNotificationsIT extends AbstractNotificationTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   /*
    * Set up for extra standard test accounts and permissions.
    */
@@ -242,7 +246,7 @@
       String changeId, TestAccount by, EmailStrategy emailStrategy, @Nullable NotifyHandling notify)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.getId());
     AbandonInput in = new AbandonInput();
     if (notify != null) {
       in.notify = notify;
@@ -585,7 +589,7 @@
       @Nullable NotifyHandling notify)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.getId());
     adder.addReviewer(changeId, reviewer, notify);
   }
 
@@ -1017,7 +1021,7 @@
   @Test
   public void deleteReviewerFromReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     removeReviewer(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1047,7 +1051,7 @@
   @Test
   public void deleteReviewerFromReviewableChangeByAdmin() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     removeReviewer(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1063,7 +1067,7 @@
   public void deleteReviewerFromReviewableChangeByAdminCcingSelf() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     setEmailStrategy(admin, EmailStrategy.CC_ON_OWN_COMMENTS);
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     removeReviewer(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1078,7 +1082,7 @@
   @Test
   public void deleteCcerFromReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     removeReviewer(sc, extraCcer);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1093,7 +1097,7 @@
   @Test
   public void deleteReviewerFromReviewableChangeNotifyOwnerReviewers() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     removeReviewer(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1150,7 +1154,7 @@
   @Test
   public void deleteReviewerFromWipChangeNotifyAll() throws Exception {
     StagedChange sc = stageWipChangeWithExtraReviewer();
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     removeReviewer(sc, extraReviewer, NotifyHandling.ALL);
     assertThat(sender)
         .sent("deleteReviewer", sc)
@@ -1166,7 +1170,7 @@
   public void deleteReviewerWithApprovalFromWipChange() throws Exception {
     StagedChange sc = stageWipChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     removeReviewer(sc, extraReviewer);
     assertThat(sender).sent("deleteReviewer", sc).to(extraReviewer).noOneElse();
   }
@@ -1187,7 +1191,7 @@
   }
 
   private void recommend(StagedChange sc, TestAccount by) throws Exception {
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.getId());
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.recommend());
   }
 
@@ -1201,7 +1205,7 @@
         ReviewInput.noScore()
             .reviewer(extraReviewer.email)
             .reviewer(extraCcer.email, ReviewerState.CC, false);
-    setApiUser(extraReviewer);
+    requestScopeOperations.setApiUser(extraReviewer.getId());
     gApi.changes().id(sc.changeId).revision("current").review(in);
     return sc;
   }
@@ -1239,7 +1243,7 @@
   public void deleteVoteFromReviewableChange() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1255,7 +1259,7 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1271,7 +1275,7 @@
   public void deleteVoteFromReviewableChangeByAdmin() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1288,7 +1292,7 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(admin, EmailStrategy.CC_ON_OWN_COMMENTS);
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1304,7 +1308,7 @@
   public void deleteVoteFromReviewableChangeNotifyOwnerReviewers() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     deleteVote(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1318,7 +1322,7 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     deleteVote(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1332,7 +1336,7 @@
   public void deleteVoteFromReviewableChangeNotifyOwner() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     deleteVote(sc, extraReviewer, NotifyHandling.OWNER);
     assertThat(sender).sent("deleteVote", sc).to(sc.owner).noOneElse();
   }
@@ -1341,7 +1345,7 @@
   public void deleteVoteFromReviewableChangeNotifyNone() throws Exception {
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     deleteVote(sc, extraReviewer, NotifyHandling.NONE);
     assertThat(sender).notSent();
   }
@@ -1351,7 +1355,7 @@
     StagedChange sc = stageReviewableChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
     setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     deleteVote(sc, extraReviewer, NotifyHandling.NONE);
     assertThat(sender).notSent();
   }
@@ -1360,7 +1364,7 @@
   public void deleteVoteFromReviewableWipChange() throws Exception {
     StagedChange sc = stageReviewableWipChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1375,7 +1379,7 @@
   public void deleteVoteFromWipChange() throws Exception {
     StagedChange sc = stageWipChangeWithExtraReviewer();
     recommend(sc, extraReviewer);
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     deleteVote(sc, extraReviewer);
     assertThat(sender)
         .sent("deleteVote", sc)
@@ -1508,7 +1512,7 @@
   private void merge(String changeId, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.getId());
     gApi.changes().id(changeId).revision("current").submit();
   }
 
@@ -1520,7 +1524,7 @@
       String changeId, TestAccount by, EmailStrategy emailStrategy, NotifyHandling notify)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.getId());
     SubmitInput in = new SubmitInput();
     in.notify = notify;
     gApi.changes().id(changeId).revision("current").submit(in);
@@ -1528,7 +1532,7 @@
 
   private StagedChange stageChangeReadyForMerge() throws Exception {
     StagedChange sc = stageReviewableChange();
-    setApiUser(sc.reviewer);
+    requestScopeOperations.setApiUser(sc.reviewer.getId());
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.approve());
     sender.clear();
     return sc;
@@ -2004,7 +2008,7 @@
   private void restore(String changeId, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.getId());
     gApi.changes().id(changeId).restore();
   }
 
@@ -2109,7 +2113,7 @@
 
   private StagedChange stageChange() throws Exception {
     StagedChange sc = stageReviewableChange();
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.approve());
     gApi.changes().id(sc.changeId).revision("current").submit();
     sender.clear();
@@ -2123,7 +2127,7 @@
   private void revert(StagedChange sc, TestAccount by, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.getId());
     gApi.changes().id(sc.changeId).revert();
   }
 
@@ -2242,7 +2246,7 @@
   private void assign(StagedChange sc, TestAccount by, TestAccount to, EmailStrategy emailStrategy)
       throws Exception {
     setEmailStrategy(by, emailStrategy);
-    setApiUser(by);
+    requestScopeOperations.setApiUser(by.getId());
     AssigneeInput in = new AssigneeInput();
     in.assignee = to.email;
     gApi.changes().id(sc.changeId).setAssignee(in);
@@ -2288,7 +2292,7 @@
   }
 
   private void startReview(StagedChange sc) throws Exception {
-    setApiUser(sc.owner);
+    requestScopeOperations.setApiUser(sc.owner.getId());
     gApi.changes().id(sc.changeId).setReadyForReview();
     // PolyGerrit current immediately follows up with a review.
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.noScore());
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
index a0170d2..8ad9f96 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.mail.EmailHeader;
@@ -27,6 +28,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.Inject;
 import java.sql.Timestamp;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
@@ -41,6 +43,8 @@
 
 /** Tests the presence of required metadata in email headers, text and html. */
 public class MailMetadataIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   private String systemTimeZone;
 
   @Before
@@ -92,7 +96,7 @@
     ReviewInput input = new ReviewInput();
     input.message = "Test";
     revision(newChange).review(input);
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     Collection<ChangeMessageInfo> result =
         gApi.changes().id(newChange.getChangeId()).get().messages;
     assertThat(result).isNotEmpty();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java b/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
index c8292ba..43a3642 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
@@ -18,13 +18,16 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
 import com.google.gerrit.testing.FakeEmailSender;
+import com.google.inject.Inject;
 import org.junit.Test;
 
 public class NotificationMailFormatIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void userReceivesPlaintextEmail() throws Exception {
@@ -35,7 +38,7 @@
 
     // Create change as admin and review as user
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.recommend());
 
     // Check that admin has received only plaintext content
@@ -47,7 +50,7 @@
     assertMailReplyTo(m, user.email);
 
     // Reset user preference
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     i.emailFormat = EmailFormat.HTML_PLAINTEXT;
     gApi.accounts().id(admin.getId().toString()).setPreferences(i);
   }
@@ -56,7 +59,7 @@
   public void userReceivesHtmlAndPlaintextEmail() throws Exception {
     // Create change as admin and review as user
     PushOneCommit.Result r = createChange();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.changes().id(r.getChangeId()).current().review(ReviewInput.recommend());
 
     // Check that admin has received both HTML and plaintext content
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 7f0a493..03f580a 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.StarsInput;
@@ -43,6 +44,7 @@
 @NoHttpd
 public class ProjectWatchIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void newPatchSetsNotifyConfig() throws Exception {
@@ -108,7 +110,7 @@
 
     assertThat(sender.getMessages()).isEmpty();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     ReviewInput in = new ReviewInput();
     in.message = "comment";
     gApi.changes().id(r.getChangeId()).current().review(in);
@@ -169,7 +171,7 @@
 
     assertThat(sender.getMessages()).isEmpty();
 
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     ReviewInput in = new ReviewInput();
     in.message = "comment";
     gApi.changes().id(r.getChangeId()).current().review(in);
@@ -210,11 +212,11 @@
   public void watchProject() throws Exception {
     // watch project
     String watchedProject = projectOperations.newProject().create().get();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     watch(watchedProject);
 
     // push a change to watched project -> should trigger email notification
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
@@ -247,7 +249,7 @@
   public void watchFile() throws Exception {
     String watchedProject = projectOperations.newProject().create().get();
     String otherWatchedProject = projectOperations.newProject().create().get();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     // watch file in project as user
     watch(watchedProject, "file:a.txt");
@@ -257,7 +259,7 @@
 
     // push a change to watched file -> should trigger email notification for
     // user
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
@@ -277,7 +279,7 @@
 
     // watch project as user2
     TestAccount user2 = accountCreator.create("user2", "user2@test.com", "User2");
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.getId());
     watch(watchedProject);
 
     // push a change to non-watched file -> should not trigger email
@@ -300,13 +302,13 @@
   @Test
   public void watchKeyword() throws Exception {
     String watchedProject = projectOperations.newProject().create().get();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     // watch keyword in project as user
     watch(watchedProject, "multimaster");
 
     // push a change with keyword -> should trigger email notification
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
@@ -338,13 +340,13 @@
   @Test
   public void watchAllProjects() throws Exception {
     String anyProject = projectOperations.newProject().create().get();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     // watch the All-Projects project to watch all projects
     watch(allProjects.get());
 
     // push a change to any project -> should trigger email notification
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     TestRepository<InMemoryRepository> anyRepo =
         cloneProject(new Project.NameKey(anyProject), admin);
     PushOneCommit.Result r =
@@ -363,7 +365,7 @@
   @Test
   public void watchFileAllProjects() throws Exception {
     String anyProject = projectOperations.newProject().create().get();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     // watch file in All-Projects project as user to watch the file in all
     // projects
@@ -371,7 +373,7 @@
 
     // push a change to watched file in any project -> should trigger email
     // notification for user
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     TestRepository<InMemoryRepository> anyRepo =
         cloneProject(new Project.NameKey(anyProject), admin);
     PushOneCommit.Result r =
@@ -391,7 +393,7 @@
 
     // watch project as user2
     TestAccount user2 = accountCreator.create("user2", "user2@test.com", "User2");
-    setApiUser(user2);
+    requestScopeOperations.setApiUser(user2.getId());
     watch(anyProject);
 
     // push a change to non-watched file in any project -> should not trigger
@@ -414,14 +416,14 @@
   @Test
   public void watchKeywordAllProjects() throws Exception {
     String anyProject = projectOperations.newProject().create().get();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
 
     // watch keyword in project as user
     watch(allProjects.get(), "multimaster");
 
     // push a change with keyword to any project -> should trigger email
     // notification
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     TestRepository<InMemoryRepository> anyRepo =
         cloneProject(new Project.NameKey(anyProject), admin);
     PushOneCommit.Result r =
@@ -455,11 +457,11 @@
   public void watchProjectNoNotificationForIgnoredChange() throws Exception {
     // watch project
     String watchedProject = projectOperations.newProject().create().get();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     watch(watchedProject);
 
     // push a change to watched project
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
@@ -469,13 +471,13 @@
     r.assertOkStatus();
 
     // ignore the change
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
 
     sender.clear();
 
     // post a comment -> should not trigger email notification since user ignored the change
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     ReviewInput in = new ReviewInput();
     in.message = "comment";
     gApi.changes().id(r.getChangeId()).current().review(in);
@@ -488,11 +490,11 @@
   public void watchProjectNoNotificationForPrivateChange() throws Exception {
     // watch project
     String watchedProject = projectOperations.newProject().create().get();
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     watch(watchedProject);
 
     // push a private change to watched project -> should not trigger email notification
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
@@ -520,19 +522,19 @@
         new AccountGroup.UUID(groupThatCanViewPrivateChanges.id));
 
     // watch project as user that can't view private changes
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     watch(watchedProject);
 
     // watch project as user that can view all private change
     TestAccount userThatCanViewPrivateChanges =
         accountCreator.create(
             "user2", "user2@test.com", "User2", groupThatCanViewPrivateChanges.name);
-    setApiUser(userThatCanViewPrivateChanges);
+    requestScopeOperations.setApiUser(userThatCanViewPrivateChanges.getId());
     watch(watchedProject);
 
     // push a private change to watched project -> should trigger email notification for
     // userThatCanViewPrivateChanges, but not for user
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     TestRepository<InMemoryRepository> watchedRepo =
         cloneProject(new Project.NameKey(watchedProject), admin);
     PushOneCommit.Result r =
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
index 04303ea..c61b7ad 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.groups.GroupApi;
@@ -29,6 +30,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.project.testing.Util;
+import com.google.inject.Inject;
 import java.io.File;
 import java.util.List;
 import org.eclipse.jgit.lib.ReflogEntry;
@@ -37,6 +39,8 @@
 
 @UseLocalDisk
 public class ReflogIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
   @Test
   public void guessRestApiInReflog() throws Exception {
     PushOneCommit.Result r = createChange();
@@ -80,7 +84,7 @@
 
   @Test
   public void regularUserIsNotAllowedToGetReflog() throws Exception {
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
     gApi.projects().name(project.get()).branch("master").reflog();
   }
@@ -96,13 +100,13 @@
       u.save();
     }
 
-    setApiUser(user);
+    requestScopeOperations.setApiUser(user.getId());
     gApi.projects().name(project.get()).branch("master").reflog();
   }
 
   @Test
   public void adminUserIsAllowedToGetReflog() throws Exception {
-    setApiUser(admin);
+    requestScopeOperations.setApiUser(admin.getId());
     gApi.projects().name(project.get()).branch("master").reflog();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
new file mode 100644
index 0000000..9b498011
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
@@ -0,0 +1,147 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.request;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth8.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.CurrentUser.PropertyKey;
+import com.google.gerrit.server.Sequences;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.junit.Test;
+
+@UseSsh
+public class RequestScopeOperationsImplTest extends AbstractDaemonTest {
+  private static final AtomicInteger changeCounter = new AtomicInteger();
+
+  @Inject private AccountOperations accountOperations;
+  @Inject private Provider<CurrentUser> userProvider;
+  @Inject private RequestScopeOperationsImpl requestScopeOperations;
+  @Inject private Sequences sequences;
+
+  @Test
+  public void setApiUserToExistingUserById() throws Exception {
+    fastCheckCurrentUser(admin.getId());
+    AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.setApiUser(user.getId());
+    assertThat(oldCtx.getUser().getAccountId()).isEqualTo(admin.getId());
+    checkCurrentUser(user.getId());
+  }
+
+  @Test
+  public void setApiUserToExistingUserByTestAccount() throws Exception {
+    fastCheckCurrentUser(admin.getId());
+    TestAccount testAccount =
+        accountOperations.account(accountOperations.newAccount().username("tester").create()).get();
+    AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.setApiUser(testAccount);
+    assertThat(oldCtx.getUser().getAccountId()).isEqualTo(admin.getId());
+    checkCurrentUser(testAccount.accountId());
+  }
+
+  @Test
+  public void setApiUserToNonExistingUser() throws Exception {
+    fastCheckCurrentUser(admin.getId());
+    try {
+      requestScopeOperations.setApiUser(new Account.Id(sequences.nextAccountId()));
+      assert_().fail("expected RuntimeException");
+    } catch (RuntimeException e) {
+      // Expected.
+    }
+    checkCurrentUser(admin.getId());
+  }
+
+  @Test
+  public void resetCurrentApiUserClearsCachedState() throws Exception {
+    requestScopeOperations.setApiUser(user.getId());
+    PropertyKey<String> key = PropertyKey.create();
+    atrScope.get().getUser().put(key, "foo");
+    assertThat(atrScope.get().getUser().get(key)).hasValue("foo");
+
+    AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.resetCurrentApiUser();
+    checkCurrentUser(user.getId());
+    assertThat(atrScope.get().getUser().get(key)).isEmpty();
+    assertThat(oldCtx.getUser().get(key)).hasValue("foo");
+  }
+
+  @Test
+  public void setApiUserAnonymousSetsAnonymousUser() throws Exception {
+    fastCheckCurrentUser(admin.getId());
+    requestScopeOperations.setApiUserAnonymous();
+    assertThat(userProvider.get()).isInstanceOf(AnonymousUser.class);
+  }
+
+  private void fastCheckCurrentUser(Account.Id expected) {
+    // Check current user quickly, since the full check requires creating changes and is quite slow.
+    assertThat(userProvider.get().isIdentifiedUser())
+        .named("user from provider is an IdentifiedUser")
+        .isTrue();
+    assertThat(userProvider.get().getAccountId()).named("user from provider").isEqualTo(expected);
+  }
+
+  private void checkCurrentUser(Account.Id expected) throws Exception {
+    // Test all supported ways that an acceptance test might query the active user.
+    fastCheckCurrentUser(expected);
+    assertThat(gApi.accounts().self().get()._accountId)
+        .named("user from GerritApi")
+        .isEqualTo(expected.get());
+    AcceptanceTestRequestScope.Context ctx = atrScope.get();
+    assertThat(ctx.getUser().isIdentifiedUser())
+        .named("user from AcceptanceTestRequestScope.Context is an IdentifiedUser")
+        .isTrue();
+    assertThat(ctx.getUser().getAccountId())
+        .named("user from AcceptanceTestRequestScope.Context")
+        .isEqualTo(expected);
+    checkSshUser(expected);
+  }
+
+  private void checkSshUser(Account.Id expected) throws Exception {
+    // No "gerrit whoami" command, so the simplest way to check who the user is over SSH is to query
+    // for owner:self.
+    ChangeInput cin = new ChangeInput();
+    cin.project = project.get();
+    cin.branch = "master";
+    cin.subject = "Test change " + changeCounter.incrementAndGet();
+    String changeId = gApi.changes().create(cin).get().changeId;
+    assertThat(gApi.changes().id(changeId).get().owner._accountId).isEqualTo(expected.get());
+    String queryResults =
+        atrScope.get().getSession().exec("gerrit query owner:self change:" + changeId);
+    assertThat(findDistinct(queryResults, "I[0-9a-f]{40}"))
+        .named("Change-Ids in query results:\n%s", queryResults)
+        .containsExactly(changeId);
+  }
+
+  private static ImmutableSet<String> findDistinct(String input, String pattern) {
+    Matcher m = Pattern.compile(pattern).matcher(input);
+    ImmutableSet.Builder<String> b = ImmutableSet.builder();
+    while (m.find()) {
+      b.add(m.group(0));
+    }
+    return b.build();
+  }
+}
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index 4c576586..33489a2 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -1,12 +1,12 @@
-load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_CENTRAL", "MAVEN_LOCAL", "maven_jar")
+load("//tools/bzl:maven_jar.bzl", "ECLIPSE", "GERRIT", "MAVEN_CENTRAL", "MAVEN_LOCAL", "maven_jar")
 
-_JGIT_VERS = "5.2.0.201812061821-r"
+_JGIT_VERS = "5.2.1.201812262042-r"
 
 _DOC_VERS = _JGIT_VERS  # Set to _JGIT_VERS unless using a snapshot
 
 JGIT_DOC_URL = "http://download.eclipse.org/jgit/site/" + _DOC_VERS + "/apidocs"
 
-_JGIT_REPO = MAVEN_CENTRAL  # Leave here even if set to MAVEN_CENTRAL.
+_JGIT_REPO = ECLIPSE  # Leave here even if set to MAVEN_CENTRAL.
 
 # set this to use a local version.
 # "/home/<user>/projects/jgit"
@@ -40,28 +40,28 @@
         name = "jgit-lib",
         artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "250269f30458084777a480895e390d2a42143da3",
-        src_sha1 = "eb28d59b3ed0c68a8ba54a38dfb7aa8af6ce624b",
+        sha1 = "34914e63e1463e40ba40e2e28b0392993ea3b938",
+        src_sha1 = "b1c9e2ae01dd31ab4957de54756ec11acc99bb30",
         unsign = True,
     )
     maven_jar(
         name = "jgit-servlet",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "5d7fbe1c8528d881e2987c75e512df2cfa408d73",
+        sha1 = "18c8938c4d8966abed84fc9de6c09aaea8cc8d87",
         unsign = True,
     )
     maven_jar(
         name = "jgit-archive",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "6e49b0516b46ca90d394256d40c6069cdd8f2957",
+        sha1 = "08c945bc664e4efe0d0e9a878f96505076da2ca9",
     )
     maven_jar(
         name = "jgit-junit",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "723b9e6c54f8b3012dd7d4fe42b616b8d10ee230",
+        sha1 = "5a5fb36517cb05ca51cbb1f00a520142dc83f793",
         unsign = True,
     )
 
diff --git a/plugins/delete-project b/plugins/delete-project
index bde9bc6..09269fd 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit bde9bc6db227bf799ec35d6c1667fedb49b1c0a7
+Subproject commit 09269fd13109ef6771803fd0560a78e34dd5281e
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 0f798f6..17f5d01 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 0f798f61c0c598c1499cbaacc1c609078c8bf0d5
+Subproject commit 17f5d016b8c9e76cdbf467a004ba22d4c46311fa
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 eb6a708..05b176c 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
@@ -26,7 +26,13 @@
 <dom-module id="gr-group-audit-log">
   <template>
     <style include="shared-styles"></style>
-    <style include="gr-table-styles"></style>
+    <style include="gr-table-styles">
+      /* GenericList style centers the last column, but we don't want that here. */
+      .genericList tr th:last-of-type,
+      .genericList tr td:last-of-type {
+        text-align: left;
+      }
+    </style>
     <table id="list" class="genericList">
       <tr class="headerRow">
         <th class="date topHeader">Date</th>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html
index e793f42..ecbd67e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html
@@ -58,6 +58,7 @@
         font-size: var(--font-size-large);
       }
       #help p {
+        margin-bottom: .6em;
         max-width: 35em;
       }
       @media only screen and (max-width: 50em) {
diff --git a/resources/com/google/gerrit/server/commit-msg_test.sh b/resources/com/google/gerrit/server/commit-msg_test.sh
index 99bf509..625fd61 100755
--- a/resources/com/google/gerrit/server/commit-msg_test.sh
+++ b/resources/com/google/gerrit/server/commit-msg_test.sh
@@ -26,6 +26,18 @@
   fi
 }
 
+function test_empty_with_comments {
+  rm -f input
+  cat << EOF > input
+# comment
+
+# comment2
+EOF
+  if ${hook} input ; then
+    fail "must fail on empty message"
+  fi
+}
+
 # a Change-Id already set is preserved.
 function test_preserve_changeid {
   cat << EOF > input
@@ -113,7 +125,7 @@
 
 
 # Test driver.
-
+git init
 for func in $( declare -F | awk '{print $3;}' | sort); do
   case ${func} in
     test_*)
diff --git a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index 738e660..53f2995 100755
--- a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -27,17 +27,36 @@
   exit 1
 fi
 
-if test ! -s "$1" ; then
-  echo "file is empty: $1"
-  exit 1
-fi
-
 # $RANDOM will be undefined if not using bash, so don't use set -u
 random=$( (whoami ; hostname ; date; cat $1 ; echo $RANDOM) | git hash-object --stdin)
 dest="$1.tmp.${random}"
 
+trap 'rm -f "${dest}"' EXIT
+
+if ! git stripspace --strip-comments < "$1" > "${dest}" ; then
+   echo "cannot strip comments from $1"
+   exit 1
+fi
+
+if test ! -s "${dest}" ; then
+  echo "file is empty: $1"
+  exit 1
+fi
+
+if ! mv "${dest}" "$1" ; then
+  echo "cannot mv ${dest} to $1"
+  exit 1
+fi
+
 # Avoid the --in-place option which only appeared in Git 2.8
 # Avoid the --if-exists option which only appeared in Git 2.15
-cat "$1" \
-  | git -c trailer.ifexists=doNothing interpret-trailers --trailer "Change-Id: I${random}" > "${dest}" \
-  && mv "${dest}" "$1"
+if ! git -c trailer.ifexists=doNothing interpret-trailers \
+      --trailer "Change-Id: I${random}" < "$1" > "${dest}" ; then
+  echo "cannot insert change-id line in $1"
+  exit 1
+fi
+
+if ! mv "${dest}" "$1" ; then
+  echo "cannot mv ${dest} to $1"
+  exit 1
+fi
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
index 2ebb2c2..b284d0c 100644
--- a/tools/bzl/maven_jar.bzl
+++ b/tools/bzl/maven_jar.bzl
@@ -6,6 +6,8 @@
 
 MAVEN_LOCAL = "MAVEN_LOCAL:"
 
+ECLIPSE = "ECLIPSE:"
+
 def _maven_release(ctx, parts):
     """induce jar and url name from maven coordinates."""
     if len(parts) not in [3, 4]:
diff --git a/tools/util.py b/tools/util.py
index 45d0541..3817f75 100644
--- a/tools/util.py
+++ b/tools/util.py
@@ -15,6 +15,7 @@
 from os import path
 
 REPO_ROOTS = {
+    'ECLIPSE': 'https://repo.eclipse.org/content/groups/releases',
     'GERRIT': 'http://gerrit-maven.storage.googleapis.com',
     'GERRIT_API':
         'https://gerrit-api.commondatastorage.googleapis.com/release',