Merge "Use 'Merge list' as display name for /MERGE_LIST"
diff --git a/.mailmap b/.mailmap
index f14afde..c35b2ec 100644
--- a/.mailmap
+++ b/.mailmap
@@ -1,28 +1,40 @@
 Adrian Görler <adrian.goerler@sap.com>                                                      Adrian Goerler <adrian.goerler@sap.com>
 Ahaan Ugale <ahaanugale@gmail.com>                                                          <augale@codeaurora.org>
 Alex Blewitt <alex.blewitt@gmail.com>                                                       <alex.blewitt@gs.com>
+Alex Blewitt <alex.blewitt@gmail.com>                                                       <alex.blewitt@credit-suisse.com>
 Alex Ryazantsev <alex.ryazantsev@gmail.com>                                                 alex <alex.ryazantsev@gmail.com>
 Alex Ryazantsev <alex.ryazantsev@gmail.com>                                                 alex.ryazantsev <alex.ryazantsev@gmail.com>
+Andrew Bonventre <andybons@chromium.org>                                                    <andybons@google.com>
 Becky Siegel <beckysiegel@google.com>                                                       beckysiegel <beckysiegel@google.com>
 Brad Larson <bklarson@gmail.com>                                                            <brad.larson@garmin.com>
-Bruce Zu <bruce.zu@sonymobile.com>                                                          <bruce.zu@sonyericsson.com>
+Bruce Zu <bruce.zu.run10@gmail.com>                                                         <bruce.zu@sonyericsson.com>
+Bruce Zu <bruce.zu.run10@gmail.com>                                                         <bruce.zu@sonymobile.com>
 Carlos Eduardo Baldacin <carloseduardo.baldacin@sonyericsson.com>                           carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
+Dariusz Luksza <dluksza@collab.net>                                                         <dariusz@luksza.org>
 David Ostrovsky <david@ostrovsky.org>                                                       <d.ostrovsky@gmx.de>
+David Ostrovsky <david@ostrovsky.org>                                                       <david.ostrovsky@gmail.com>
+David Pursehouse <dpursehouse@collab.net>                                                   <david.pursehouse@sonymobile.com>
 Deniz Türkoglu <deniz@spotify.com>                                                          Deniz Türkoglu <deniz@spotify.com>
 Deniz Türkoglu <deniz@spotify.com>                                                          Deniz Turkoglu <deniz@spotify.com>
+Doug Kelly <dougk.ff7@gmail.com>                                                            <doug.kelly@garmin.com>
 Edwin Kempin <ekempin@google.com>                                                           Edwin Kempin <edwin.kempin@gmail.com>
 Edwin Kempin <ekempin@google.com>                                                           Edwin Kempin <edwin.kempin@sap.com>
 Edwin Kempin <ekempin@google.com>                                                           ekempin <ekempin@google.com>
 Eryk Szymanski <eryksz@gmail.com>                                                           <eryksz@google.com>
 Fredrik Luthander <fredrik.luthander@sonymobile.com>                                        <fredrik@gandaraj.com>
 Fredrik Luthander <fredrik.luthander@sonymobile.com>                                        <fredrik.luthander@sonyericsson.com>
-Gustaf Lundh <gustaf.lundh@sonymobile.com>                                                  <gustaf.lundh@sonyericsson.com>
+Gustaf Lundh <gustaflh@axis.com>                                                            <gustaf.lundh@axis.com>
+Gustaf Lundh <gustaflh@axis.com>                                                            <gustaf.lundh@sonyericsson.com>
+Gustaf Lundh <gustaflh@axis.com>                                                            <gustaf.lundh@sonymobile.com>
 Hugo Arès <hugo.ares@ericsson.com>                                                          Hugo Ares <hugo.ares@ericsson.com>
+Jacek Centkowski <jcentkowski@collab.net>                                                   <gemincia.programs@gmail.com>
+Jacek Centkowski <jcentkowski@collab.net>                                                   <geminica.programs@gmail.com>
 Jason Huntley <jhuntley@houghtonassociates.com>                                             jhuntley <jhuntley@houghtonassociates.com>
 Jiří Engelthaler <EngyCZ@gmail.com>                                                         <engycz@gmail.com>
 Joe Onorato <onoratoj@gmail.com>                                                            <joeo@android.com>
 Joel Dodge <dodgejoel@gmail.com>                                                            dodgejoel <dodgejoel@gmail.com>
 Johan Björk <jbjoerk@gmail.com>                                                             Johan Bjork <phb@spotify.com>
+JT Olds <hello@jtolds.com>                                                                  <jtolds@gmail.com>
 Lincoln Oliveira Campos Do Nascimento <lincoln.oliveiracamposdonascimento@sonyericsson.com> lincoln <lincoln.oliveiracamposdonascimento@sonyericsson.com>
 Luca Milanesio <luca.milanesio@gmail.com>                                                   <luca@gitent-scm.com>
 Magnus Bäck <baeck@google.com>                                                              <magnus.back@sonyericsson.com>
@@ -36,12 +48,15 @@
 Peter Jönsson <peter.joensson@gmail.com>                                                    Peter Jönsson <peter.joensson@gmail.com>
 Rafael Rabelo Silva <rafael.rabelosilva@sonyericsson.com>                                   rafael.rabelosilva <rafael.rabelosilva@sonyericsson.com>
 Richard Möhn <richard.moehn@posteo.de>                                                      <richard.moehn@fu-berlin.de>
+Sam Saccone <samccone@google.com>                                                           <samccone@gmail.com>
 Saša Živkov <sasa.zivkov@sap.com>                                                           Sasa Zivkov <sasa.zivkov@sap.com>
 Saša Živkov <sasa.zivkov@sap.com>                                                           Saša Živkov <zivkov@gmail.com>
 Saša Živkov <sasa.zivkov@sap.com>                                                           Sasa Zivkov <zivkov@gmail.com>
+Scott Dial <scott@scottdial.com>                                                            <geekmug@gmail.com>
 Shawn Pearce <sop@google.com>                                                               Shawn O. Pearce <sop@google.com>
 Sixin Li <sixin210@gmail.com>                                                               sixin li <sixin210@gmail.com>
-Sven Selberg <svense@axis.com>                                                              sven <sven.selberg@sonymobile.com>
+Sven Selberg <svense@axis.com>                                                              <sven.selberg@axis.com>
+Sven Selberg <svense@axis.com>                                                              <sven.selberg@sonymobile.com>
 Tom Wang <twang10@gmail.com>                                                                Tom <twang10@gmail.com>
 Tomas Westling <thomas.westling@sonyericsson.com>                                           thomas.westling <thomas.westling@sonyericsson.com>
 Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>                                                <ulrik.sjolin@gmail.com>
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 58389fd..da30264 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -4636,6 +4636,11 @@
 
 Gets the content of a file from a certain revision.
 
+The optional, integer-valued `parent` parameter can be specified to request
+the named file from a parent commit of the specified revision. The value is
+the 1-based index of the parent's position in the commit object. If the
+parameter is omitted or the value is non-positive, the patch set is referenced.
+
 .Request
 ----
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/content HTTP/1.0
@@ -5536,6 +5541,10 @@
 |`topic`              |optional|The topic to which this change belongs.
 |`status`             |optional, default to `NEW`|
 The status of the change (only `NEW` and `DRAFT` accepted here).
+|`is_private`         |optional, default to `false`|
+Whether the new change should be marked as private.
+|`work_in_progress`   |optional, default to `false`|
+Whether the new change should be set to work in progress.
 |`base_change`        |optional|
 A link:#change-id[\{change-id\}] that identifies the base change for a create
 change operation.
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 6345c67..6ee2b1c 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
 import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
@@ -98,6 +99,7 @@
 import com.google.gerrit.server.project.Util;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.FakeEmailSender;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
@@ -109,6 +111,7 @@
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -152,6 +155,7 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.rules.ExpectedException;
+import org.junit.rules.RuleChain;
 import org.junit.rules.TemporaryFolder;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
@@ -163,93 +167,13 @@
   private static GerritServer commonServer;
 
   @ConfigSuite.Parameter public Config baseConfig;
-
   @ConfigSuite.Name private String configName;
 
-  @Inject protected AllProjectsName allProjects;
-
-  @Inject protected AccountCreator accounts;
-
-  @Inject private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject protected GerritApi gApi;
-
-  @Inject protected AcceptanceTestRequestScope atrScope;
-
-  @Inject protected AccountCache accountCache;
-
-  @Inject protected IdentifiedUser.GenericFactory identifiedUserFactory;
-
-  @Inject protected PushOneCommit.Factory pushFactory;
-
-  @Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject protected ProjectCache projectCache;
-
-  @Inject protected GroupCache groupCache;
-
-  @Inject protected GitRepositoryManager repoManager;
-
-  @Inject protected ChangeIndexer indexer;
-
-  @Inject protected Provider<InternalChangeQuery> queryProvider;
-
-  @Inject @CanonicalWebUrl protected Provider<String> canonicalWebUrl;
-
-  @Inject @GerritServerConfig protected Config cfg;
-
-  @Inject private InProcessProtocol inProcessProtocol;
-
-  @Inject private Provider<AnonymousUser> anonymousUser;
-
-  @Inject @GerritPersonIdent protected Provider<PersonIdent> serverIdent;
-
-  @Inject protected ChangeData.Factory changeDataFactory;
-
-  @Inject protected PatchSetUtil psUtil;
-
-  @Inject protected ChangeFinder changeFinder;
-
-  @Inject protected Revisions revisions;
-
-  @Inject protected FakeEmailSender sender;
-
-  @Inject protected ChangeNoteUtil changeNoteUtil;
-
-  @Inject protected ChangeResource.Factory changeResourceFactory;
-
-  @Inject protected SystemGroupBackend systemGroupBackend;
-
-  @Inject private EventRecorder.Factory eventRecorderFactory;
-
-  @Inject private ChangeIndexCollection changeIndexes;
-
-  protected TestRepository<InMemoryRepository> testRepo;
-  protected GerritServer server;
-  protected TestAccount admin;
-  protected TestAccount user;
-  protected RestSession adminRestSession;
-  protected RestSession userRestSession;
-  protected SshSession adminSshSession;
-  protected SshSession userSshSession;
-  protected ReviewDb db;
-  protected Project.NameKey project;
-  protected EventRecorder eventRecorder;
-
-  @Inject protected TestNotesMigration notesMigration;
-
-  @Inject protected ChangeNotes.Factory notesFactory;
-
-  @Inject protected Abandon changeAbandoner;
-
   @Rule public ExpectedException exception = ExpectedException.none();
 
-  private String resourcePrefix;
-  private List<Repository> toClose;
-  private boolean useSsh;
+  protected final TemporaryFolder tempSiteDir = new TemporaryFolder();
 
-  @Rule
-  public TestRule testRunner =
+  private final TestRule testRunner =
       new TestRule() {
         @Override
         public Statement apply(final Statement base, final Description description) {
@@ -267,7 +191,58 @@
         }
       };
 
-  @Rule public TemporaryFolder tempSiteDir = new TemporaryFolder();
+  @Rule public RuleChain ruleChain = RuleChain.outerRule(tempSiteDir).around(testRunner);
+
+  @Inject @CanonicalWebUrl protected Provider<String> canonicalWebUrl;
+  @Inject @GerritPersonIdent protected Provider<PersonIdent> serverIdent;
+  @Inject @GerritServerConfig protected Config cfg;
+  @Inject protected AcceptanceTestRequestScope atrScope;
+  @Inject protected AccountCache accountCache;
+  @Inject protected AccountCreator accounts;
+  @Inject protected AllProjectsName allProjects;
+  @Inject protected BatchUpdate.Factory batchUpdateFactory;
+  @Inject protected ChangeData.Factory changeDataFactory;
+  @Inject protected ChangeFinder changeFinder;
+  @Inject protected ChangeIndexer indexer;
+  @Inject protected ChangeNoteUtil changeNoteUtil;
+  @Inject protected ChangeResource.Factory changeResourceFactory;
+  @Inject protected FakeEmailSender sender;
+  @Inject protected GerritApi gApi;
+  @Inject protected GitRepositoryManager repoManager;
+  @Inject protected GroupCache groupCache;
+  @Inject protected IdentifiedUser.GenericFactory identifiedUserFactory;
+  @Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
+  @Inject protected PatchSetUtil psUtil;
+  @Inject protected ProjectCache projectCache;
+  @Inject protected Provider<InternalChangeQuery> queryProvider;
+  @Inject protected PushOneCommit.Factory pushFactory;
+  @Inject protected Revisions revisions;
+  @Inject protected SystemGroupBackend systemGroupBackend;
+  @Inject protected TestNotesMigration notesMigration;
+  @Inject protected ChangeNotes.Factory notesFactory;
+  @Inject protected Abandon changeAbandoner;
+
+  protected EventRecorder eventRecorder;
+  protected GerritServer server;
+  protected Project.NameKey project;
+  protected RestSession adminRestSession;
+  protected RestSession userRestSession;
+  protected ReviewDb db;
+  protected SshSession adminSshSession;
+  protected SshSession userSshSession;
+  protected TestAccount admin;
+  protected TestAccount user;
+  protected TestRepository<InMemoryRepository> testRepo;
+
+  @Inject private ChangeIndexCollection changeIndexes;
+  @Inject private EventRecorder.Factory eventRecorderFactory;
+  @Inject private InProcessProtocol inProcessProtocol;
+  @Inject private Provider<AnonymousUser> anonymousUser;
+  @Inject private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  private List<Repository> toClose;
+  private String resourcePrefix;
+  private boolean useSsh;
 
   @Before
   public void clearSender() {
@@ -1264,4 +1239,18 @@
     projectsToWatch.add(pwi);
     gApi.accounts().self().setWatchedProjects(projectsToWatch);
   }
+
+  protected void assertContent(PushOneCommit.Result pushResult, String path, String expectedContent)
+      throws Exception {
+    BinaryResult bin =
+        gApi.changes()
+            .id(pushResult.getChangeId())
+            .revision(pushResult.getCommit().name())
+            .file(path)
+            .content();
+    ByteArrayOutputStream os = new ByteArrayOutputStream();
+    bin.writeTo(os);
+    String res = new String(os.toByteArray(), UTF_8);
+    assertThat(res).isEqualTo(expectedContent);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index b32349b..a3ca832 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -82,6 +82,7 @@
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
+import com.google.gerrit.testutil.SshMode;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.ByteArrayOutputStream;
@@ -147,7 +148,9 @@
 
   @After
   public void removeAccountIndexEventCounter() {
-    accountIndexEventCounterHandle.remove();
+    if (accountIndexEventCounterHandle != null) {
+      accountIndexEventCounterHandle.remove();
+    }
   }
 
   @Before
@@ -209,7 +212,11 @@
     TestAccount foo = accounts.create("foo");
     AccountInfo info = gApi.accounts().id(foo.id.get()).get();
     assertThat(info.username).isEqualTo("foo");
-    accountIndexedCounter.assertReindexOf(foo, 2); // account creation + adding SSH keys
+    if (SshMode.useSsh()) {
+      accountIndexedCounter.assertReindexOf(foo, 2); // account creation + adding SSH keys
+    } else {
+      accountIndexedCounter.assertReindexOf(foo, 1); // account creation
+    }
 
     // check user branch
     try (Repository repo = repoManager.openRepository(allUsers);
@@ -376,7 +383,6 @@
     gApi.accounts()
         .self()
         .setStars(triplet, new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "blue", IGNORE_LABEL)));
-    accountIndexedCounter.assertNoReindex();
   }
 
   @Test
@@ -653,12 +659,13 @@
     assertThat(userSelfRef).isNotNull();
     assertThat(userSelfRef.getObjectId()).isEqualTo(userRef.getObjectId());
 
+    accountIndexedCounter.assertNoReindex();
+
     // fetching user branch of another user fails
     String otherUserRefName = RefNames.refsUsers(admin.id);
     exception.expect(TransportException.class);
     exception.expectMessage("Remote does not have " + otherUserRefName + " available for fetch.");
     fetch(allUsersRepo, otherUserRefName + ":otherUserRef");
-    accountIndexedCounter.assertNoReindex();
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 0456aa1..a4a3c04 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -145,8 +145,6 @@
 public class ChangeIT extends AbstractDaemonTest {
   private String systemTimeZone;
 
-  @Inject private BatchUpdate.Factory updateFactory;
-
   @Inject private DynamicSet<ChangeMessageModifier> changeMessageModifiers;
 
   @Before
@@ -191,7 +189,7 @@
 
     setApiUser(user);
     String changeId = result.getChangeId();
-    assertThat(gApi.changes().id(changeId).get().isPrivate).isFalse();
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
 
     gApi.changes().id(changeId).setPrivate(true);
     ChangeInfo info = gApi.changes().id(changeId).get();
@@ -201,7 +199,7 @@
 
     gApi.changes().id(changeId).setPrivate(false);
     info = gApi.changes().id(changeId).get();
-    assertThat(info.isPrivate).isFalse();
+    assertThat(info.isPrivate).isNull();
     assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private");
     assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE);
   }
@@ -212,7 +210,7 @@
     PushOneCommit.Result result =
         pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master");
 
-    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isFalse();
+    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isNull();
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to mark private");
     gApi.changes().id(result.getChangeId()).setPrivate(true);
@@ -295,7 +293,7 @@
     gApi.changes().id(changeId).setReadyForReview("PTAL");
 
     info = gApi.changes().id(changeId).get();
-    assertThat(info.workInProgress).isFalse();
+    assertThat(info.workInProgress).isNull();
     assertThat(Iterables.getLast(info.messages).message).contains("PTAL");
     assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY);
 
@@ -311,7 +309,7 @@
     gApi.changes().id(changeId).setReadyForReview();
 
     info = gApi.changes().id(changeId).get();
-    assertThat(info.workInProgress).isFalse();
+    assertThat(info.workInProgress).isNull();
     assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set Ready For Review");
     assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY);
   }
@@ -368,7 +366,8 @@
     List<ChangeControl> controlB = changeFinder.find(b.getChangeId(), user);
     assertThat(controlB).hasSize(1);
     List<ChangeControl> list = ImmutableList.of(controlA.get(0), controlB.get(0));
-    changeAbandoner.batchAbandon(controlA.get(0).getProject().getNameKey(), user, list, "deadbeef");
+    changeAbandoner.batchAbandon(
+        batchUpdateFactory, controlA.get(0).getProject().getNameKey(), user, list, "deadbeef");
 
     ChangeInfo info = get(a.getChangeId());
     assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
@@ -401,7 +400,7 @@
     exception.expect(ResourceConflictException.class);
     exception.expectMessage(
         String.format("Project name \"%s\" doesn't match \"%s\"", project2Name, project1Name));
-    changeAbandoner.batchAbandon(new Project.NameKey(project1Name), user, list);
+    changeAbandoner.batchAbandon(batchUpdateFactory, new Project.NameKey(project1Name), user, list);
   }
 
   @Test
@@ -2784,7 +2783,7 @@
 
   private void setChangeStatus(Change.Id id, Change.Status newStatus) throws Exception {
     try (BatchUpdate batchUpdate =
-        updateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) {
+        batchUpdateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) {
       batchUpdate.addOp(id, new ChangeStatusUpdateOp(newStatus));
       batchUpdate.execute();
     }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 3a535ba..8c0662b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -1062,20 +1062,6 @@
     return eTag;
   }
 
-  private void assertContent(PushOneCommit.Result pushResult, String path, String expectedContent)
-      throws Exception {
-    BinaryResult bin =
-        gApi.changes()
-            .id(pushResult.getChangeId())
-            .revision(pushResult.getCommit().name())
-            .file(path)
-            .content();
-    ByteArrayOutputStream os = new ByteArrayOutputStream();
-    bin.writeTo(os);
-    String res = new String(os.toByteArray(), UTF_8);
-    assertThat(res).isEqualTo(expectedContent);
-  }
-
   private void assertDiffForNewFile(
       PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception {
     DiffInfo diff =
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 0a03125..4f4b6c6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -929,14 +929,9 @@
     PushResult pr =
         GitUtil.pushHead(testRepo, "refs/for/foo%base=" + rBase.getCommit().name(), false, false);
 
-    if (notesMigration.fuseUpdates()) {
-      // InMemoryRepository's atomic BatchRefUpdate implementation doesn't update the progress
-      // monitor. That's fine, we just care that there was at least one new change and no errors.
-      assertThat(pr.getMessages()).contains("changes: new: 1, done");
-    } else {
-      assertThat(pr.getMessages()).contains("changes: new: 1, refs: 1, done");
-    }
-
+    // BatchUpdate implementations differ in how they hook into progress monitors. We mostly just
+    // care that there is a new change.
+    assertThat(pr.getMessages()).containsMatch("changes: new: 1,( refs: 1)? done");
     assertTwoChangesWithSameRevision(r);
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 2a4c188..0abdde6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -264,20 +264,25 @@
   }
 
   @Test
-  public void uploadPackSubsetOfBranchesVisibleWithEditForOtherUser() throws Exception {
+  public void uploadPackSubsetOfBranchesAndEditsVisibleWithViewPrivateChanges() throws Exception {
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
     allow(Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS, "refs/*");
 
-    Change c = notesFactory.createChecked(db, project, c1.getId()).getChange();
-    String changeId = c.getKey().get();
+    Change change1 = notesFactory.createChecked(db, project, c1.getId()).getChange();
+    String changeId1 = change1.getKey().get();
+    Change change2 = notesFactory.createChecked(db, project, c2.getId()).getChange();
+    String changeId2 = change2.getKey().get();
 
-    // Admin's edit is visible.
+    // Admin's edit on change1 is visible.
     setApiUser(admin);
-    gApi.changes().id(changeId).edit().create();
+    gApi.changes().id(changeId1).edit().create();
+
+    // Admin's edit on change2 is not visible since user cannot see the change.
+    gApi.changes().id(changeId2).edit().create();
 
     // User's edit is visible.
     setApiUser(user);
-    gApi.changes().id(changeId).edit().create();
+    gApi.changes().id(changeId1).edit().create();
 
     assertUploadPackRefs(
         "HEAD",
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 359883a..4111a3d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -114,8 +114,6 @@
 
   @Inject private IdentifiedUser.GenericFactory userFactory;
 
-  @Inject private BatchUpdate.Factory updateFactory;
-
   @Inject private DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
   private RegistrationHandle onSubmitValidatorHandle;
 
@@ -807,7 +805,7 @@
   private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Exception {
     for (PushOneCommit.Result change : changes) {
       try (BatchUpdate bu =
-          updateFactory.create(db, project, userFactory.create(admin.id), TimeUtil.nowTs())) {
+          batchUpdateFactory.create(db, project, userFactory.create(admin.id), TimeUtil.nowTs())) {
         bu.addOp(
             change.getChange().getId(),
             new BatchUpdateOp() {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 146b5ca..153e70b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -156,6 +156,20 @@
   }
 
   @Test
+  public void createNewPrivateChange() throws Exception {
+    ChangeInput input = newChangeInput(ChangeStatus.NEW);
+    input.isPrivate = true;
+    assertCreateSucceeds(input);
+  }
+
+  @Test
+  public void createNewWorkInProgressChange() throws Exception {
+    ChangeInput input = newChangeInput(ChangeStatus.NEW);
+    input.workInProgress = true;
+    assertCreateSucceeds(input);
+  }
+
+  @Test
   public void noteDbCommit() throws Exception {
     assume().that(notesMigration.readChanges()).isTrue();
 
@@ -376,6 +390,8 @@
     assertThat(out.subject).isEqualTo(in.subject);
     assertThat(out.topic).isEqualTo(in.topic);
     assertThat(out.status).isEqualTo(in.status);
+    assertThat(out.isPrivate).isEqualTo(in.isPrivate);
+    assertThat(out.workInProgress).isEqualTo(in.workInProgress);
     assertThat(out.revisions).hasSize(1);
     assertThat(out.submitted).isNull();
     Boolean draft = Iterables.getOnlyElement(out.revisions.values()).draft;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
index 228b478..3eee3a4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
@@ -188,6 +188,26 @@
   }
 
   @Test
+  public void deleteCurrentDraftPatchSetWhenPreviousPatchSetDoesNotExist() throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    String changeId = push.to("refs/for/master").getChangeId();
+    pushFactory
+        .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "foo", changeId)
+        .to("refs/drafts/master");
+    pushFactory
+        .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "bar", changeId)
+        .to("refs/drafts/master");
+
+    deletePatchSet(changeId, 2);
+    deletePatchSet(changeId, 3);
+
+    ChangeData cd = getChange(changeId);
+    assertThat(cd.patchSets()).hasSize(1);
+    assertThat(Iterables.getOnlyElement(cd.patchSets()).getId().get()).isEqualTo(1);
+    assertThat(cd.currentPatchSet().getId().get()).isEqualTo(1);
+  }
+
+  @Test
   public void deleteDraftPatchSetAndPushNewDraftPatchSet() throws Exception {
     String ref = "refs/drafts/master";
 
@@ -263,6 +283,10 @@
   }
 
   private void deletePatchSet(String changeId, PatchSet ps) throws Exception {
-    gApi.changes().id(changeId).revision(ps.getId().get()).delete();
+    deletePatchSet(changeId, ps.getId().get());
+  }
+
+  private void deletePatchSet(String changeId, int ps) throws Exception {
+    gApi.changes().id(changeId).revision(ps).delete();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
index 26e6847..c2822ce 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
@@ -45,7 +45,6 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.testutil.ConfigSuite;
-import com.google.inject.Inject;
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.List;
@@ -58,8 +57,6 @@
     return allowDraftsDisabledConfig();
   }
 
-  @Inject private BatchUpdate.Factory updateFactory;
-
   @Test
   public void deleteDraftChange() throws Exception {
     assume().that(isAllowDrafts()).isTrue();
@@ -244,7 +241,7 @@
 
   private void markChangeAsDraft(Change.Id id) throws Exception {
     try (BatchUpdate batchUpdate =
-        updateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) {
+        batchUpdateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) {
       batchUpdate.addOp(id, new MarkChangeAsDraftUpdateOp());
       batchUpdate.execute();
     }
@@ -256,7 +253,7 @@
   private void setDraftStatusOfPatchSetsOfChange(Change.Id id, boolean draftStatus)
       throws Exception {
     try (BatchUpdate batchUpdate =
-        updateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) {
+        batchUpdateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) {
       batchUpdate.addOp(id, new DraftStatusOfPatchSetsUpdateOp(draftStatus));
       batchUpdate.execute();
     }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/revision/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/revision/BUILD
new file mode 100644
index 0000000..f47ac46
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/revision/BUILD
@@ -0,0 +1,7 @@
+load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "rest_revision",
+    labels = ["rest"],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/revision/RevisionIT.java
new file mode 100644
index 0000000..89fdeff
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/revision/RevisionIT.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.revision;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import org.eclipse.jgit.util.Base64;
+import org.junit.Test;
+
+public class RevisionIT extends AbstractDaemonTest {
+  @Test
+  public void contentOfParent() throws Exception {
+    String parentContent = "parent content";
+    PushOneCommit.Result parent = createChange("Parent change", FILE_NAME, parentContent);
+    parent.assertOkStatus();
+
+    gApi.changes().id(parent.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(parent.getChangeId()).current().submit();
+
+    PushOneCommit.Result child = createChange("Child change", FILE_NAME, FILE_CONTENT);
+    child.assertOkStatus();
+    assertContent(child, FILE_NAME, FILE_CONTENT);
+
+    RestResponse response =
+        adminRestSession.get(
+            "/changes/"
+                + child.getChangeId()
+                + "/revisions/current/files/"
+                + FILE_NAME
+                + "/content?parent=1");
+    response.assertOK();
+    assertThat(new String(Base64.decode(response.getEntityContent()), UTF_8))
+        .isEqualTo(parentContent);
+  }
+
+  @Test
+  public void contentOfInvalidParent() throws Exception {
+    String parentContent = "parent content";
+    PushOneCommit.Result parent = createChange("Parent change", FILE_NAME, parentContent);
+    parent.assertOkStatus();
+
+    gApi.changes().id(parent.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(parent.getChangeId()).current().submit();
+
+    PushOneCommit.Result child = createChange("Child change", FILE_NAME, FILE_CONTENT);
+    child.assertOkStatus();
+    assertContent(child, FILE_NAME, FILE_CONTENT);
+
+    RestResponse response =
+        adminRestSession.get(
+            "/changes/"
+                + child.getChangeId()
+                + "/revisions/current/files/"
+                + FILE_NAME
+                + "/content?parent=10");
+    response.assertBadRequest();
+    assertThat(response.getEntityContent()).isEqualTo("invalid parent");
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 7e95da6..26a49b1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -380,7 +380,7 @@
       ChangeResource changeRsrc =
           changes.get().parse(TopLevelResource.INSTANCE, IdString.fromDecoded(changeId));
       RevisionResource revRsrc = revisions.parse(changeRsrc, IdString.fromDecoded(revId));
-      postReview.get().apply(revRsrc, input, timestamp);
+      postReview.get().apply(batchUpdateFactory, revRsrc, input, timestamp);
       Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
       assertThat(result).isNotEmpty();
       CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index 329716f..8f256be 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -79,8 +79,6 @@
 
   @Inject private IdentifiedUser.GenericFactory userFactory;
 
-  @Inject private BatchUpdate.Factory updateFactory;
-
   @Inject private ChangeInserter.Factory changeInserterFactory;
 
   @Inject private PatchSetInserter.Factory patchSetInserterFactory;
@@ -784,7 +782,7 @@
   }
 
   private BatchUpdate newUpdate(Account.Id owner) {
-    return updateFactory.create(db, project, userFactory.create(owner), TimeUtil.nowTs());
+    return batchUpdateFactory.create(db, project, userFactory.create(owner), TimeUtil.nowTs());
   }
 
   private ChangeControl insertChange() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index fcbad4f..e957c88 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -64,8 +64,6 @@
     System.setProperty("user.timezone", systemTimeZone);
   }
 
-  @Inject private BatchUpdate.Factory updateFactory;
-
   @Inject private ChangesCollection changes;
 
   @Test
@@ -578,7 +576,7 @@
   }
 
   private void clearGroups(final PatchSet.Id psId) throws Exception {
-    try (BatchUpdate bu = updateFactory.create(db, project, user(user), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(db, project, user(user), TimeUtil.nowTs())) {
       bu.addOp(
           psId.getParentKey(),
           new BatchUpdateOp() {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index 15b74bd..aaf01f36 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -139,8 +139,6 @@
 
   @Inject private TestChangeRebuilderWrapper rebuilderWrapper;
 
-  @Inject private BatchUpdate.Factory batchUpdateFactory;
-
   @Inject private Sequences seq;
 
   @Inject private ChangeBundleReader bundleReader;
@@ -754,7 +752,7 @@
     assertThat(ts).isGreaterThan(c.getCreatedOn());
     assertThat(ts).isLessThan(db.patchSets().get(psId).getCreatedOn());
     RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId());
-    postReview.get().apply(revRsrc, rin, ts);
+    postReview.get().apply(batchUpdateFactory, revRsrc, rin, ts);
 
     checker.rebuildAndCheckChanges(id);
   }
@@ -772,7 +770,7 @@
     Timestamp ts = new Timestamp(c.getCreatedOn().getTime() - 10000);
     RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId());
     setApiUser(user);
-    postReview.get().apply(revRsrc, rin, ts);
+    postReview.get().apply(batchUpdateFactory, revRsrc, rin, ts);
 
     checker.rebuildAndCheckChanges(id);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
index 4a5d496..2c61e64 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -20,30 +20,45 @@
 import static com.google.common.truth.TruthJUnit.assume;
 import static java.util.stream.Collectors.toList;
 
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.BatchUpdateListener;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.RepoContext;
+import com.google.gerrit.server.update.RetryHelper;
 import com.google.inject.Inject;
 import java.io.IOException;
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Before;
 import org.junit.Test;
 
 public class NoteDbOnlyIT extends AbstractDaemonTest {
-  @Inject private BatchUpdate.Factory batchUpdateFactory;
+  @Inject private RetryHelper retryHelper;
 
   @Before
   public void setUp() throws Exception {
@@ -81,7 +96,7 @@
           }
         };
 
-    try (BatchUpdate bu = newBatchUpdate()) {
+    try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) {
       bu.addOp(id, backupMasterOp);
       bu.execute();
     }
@@ -97,7 +112,7 @@
     assertThat(master2).isNotEqualTo(master1);
     int msgCount = getMessages(id).size();
 
-    try (BatchUpdate bu = newBatchUpdate()) {
+    try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) {
       // This time, we attempt to back up master, but we fail during updateChange.
       bu.addOp(id, backupMasterOp);
       String msg = "Change is bad";
@@ -122,9 +137,175 @@
     assertThat(getMessages(id)).hasSize(msgCount);
   }
 
-  private BatchUpdate newBatchUpdate() {
-    return batchUpdateFactory.create(
-        db, project, identifiedUserFactory.create(user.getId()), TimeUtil.nowTs());
+  @Test
+  public void retryOnLockFailureWithAtomicUpdates() throws Exception {
+    assume().that(notesMigration.fuseUpdates()).isTrue();
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getChange().getId();
+    String master = "refs/heads/master";
+    ObjectId initial;
+    try (Repository repo = repoManager.openRepository(project)) {
+      ((InMemoryRepository) repo).setPerformsAtomicTransactions(true);
+      initial = repo.exactRef(master).getObjectId();
+    }
+
+    AtomicInteger updateRepoCalledCount = new AtomicInteger();
+    AtomicInteger updateChangeCalledCount = new AtomicInteger();
+    AtomicInteger afterUpdateReposCalledCount = new AtomicInteger();
+
+    String result =
+        retryHelper.execute(
+            batchUpdateFactory -> {
+              try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) {
+                bu.addOp(
+                    id,
+                    new UpdateRefAndAddMessageOp(updateRepoCalledCount, updateChangeCalledCount));
+                bu.execute(new ConcurrentWritingListener(afterUpdateReposCalledCount));
+              }
+              return "Done";
+            });
+
+    assertThat(result).isEqualTo("Done");
+    assertThat(updateRepoCalledCount.get()).isEqualTo(2);
+    assertThat(afterUpdateReposCalledCount.get()).isEqualTo(2);
+    assertThat(updateChangeCalledCount.get()).isEqualTo(2);
+
+    List<String> messages = getMessages(id);
+    assertThat(Iterables.getLast(messages)).isEqualTo(UpdateRefAndAddMessageOp.CHANGE_MESSAGE);
+    assertThat(Collections.frequency(messages, UpdateRefAndAddMessageOp.CHANGE_MESSAGE))
+        .isEqualTo(1);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      // Op lost the race, so the other writer's commit happened first. Then op retried and wrote
+      // its commit with the other writer's commit as parent.
+      assertThat(commitMessages(repo, initial, repo.exactRef(master).getObjectId()))
+          .containsExactly(
+              ConcurrentWritingListener.MSG_PREFIX + "1", UpdateRefAndAddMessageOp.COMMIT_MESSAGE)
+          .inOrder();
+    }
+  }
+
+  @Test
+  public void noRetryOnLockFailureWithoutAtomicUpdates() throws Exception {
+    assume().that(notesMigration.fuseUpdates()).isFalse();
+
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getChange().getId();
+    String master = "refs/heads/master";
+    ObjectId initial;
+    try (Repository repo = repoManager.openRepository(project)) {
+      initial = repo.exactRef(master).getObjectId();
+    }
+
+    AtomicInteger updateRepoCalledCount = new AtomicInteger();
+    AtomicInteger updateChangeCalledCount = new AtomicInteger();
+    AtomicInteger afterUpdateReposCalledCount = new AtomicInteger();
+
+    try {
+      retryHelper.execute(
+          batchUpdateFactory -> {
+            try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) {
+              bu.addOp(
+                  id, new UpdateRefAndAddMessageOp(updateRepoCalledCount, updateChangeCalledCount));
+              bu.execute(new ConcurrentWritingListener(afterUpdateReposCalledCount));
+            }
+            return null;
+          });
+      assert_().fail("expected RestApiException");
+    } catch (RestApiException e) {
+      // Expected.
+    }
+
+    assertThat(updateRepoCalledCount.get()).isEqualTo(1);
+    assertThat(afterUpdateReposCalledCount.get()).isEqualTo(1);
+    assertThat(updateChangeCalledCount.get()).isEqualTo(0);
+
+    // updateChange was never called, so no message was ever added.
+    assertThat(getMessages(id)).doesNotContain(UpdateRefAndAddMessageOp.CHANGE_MESSAGE);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      // Op lost the race, so the other writer's commit happened first. Op didn't retry, because the
+      // ref updates weren't atomic, so it didn't throw LockFailureException on failure.
+      assertThat(commitMessages(repo, initial, repo.exactRef(master).getObjectId()))
+          .containsExactly(ConcurrentWritingListener.MSG_PREFIX + "1");
+    }
+  }
+
+  private class ConcurrentWritingListener implements BatchUpdateListener {
+    static final String MSG_PREFIX = "Other writer ";
+
+    private final AtomicInteger calledCount;
+
+    private ConcurrentWritingListener(AtomicInteger calledCount) {
+      this.calledCount = calledCount;
+    }
+
+    @Override
+    public void afterUpdateRepos() throws Exception {
+      // Reopen repo and update ref, to simulate a concurrent write in another
+      // thread. Only do this the first time the listener is called.
+      if (calledCount.getAndIncrement() > 0) {
+        return;
+      }
+      try (Repository repo = repoManager.openRepository(project);
+          RevWalk rw = new RevWalk(repo);
+          ObjectInserter ins = repo.newObjectInserter()) {
+        String master = "refs/heads/master";
+        ObjectId oldId = repo.exactRef(master).getObjectId();
+        ObjectId newId = newCommit(rw, ins, oldId, MSG_PREFIX + calledCount.get());
+        ins.flush();
+        RefUpdate ru = repo.updateRef(master);
+        ru.setExpectedOldObjectId(oldId);
+        ru.setNewObjectId(newId);
+        assertThat(ru.update(rw)).isEqualTo(RefUpdate.Result.FAST_FORWARD);
+      }
+    }
+  }
+
+  private class UpdateRefAndAddMessageOp implements BatchUpdateOp {
+    static final String COMMIT_MESSAGE = "A commit";
+    static final String CHANGE_MESSAGE = "A change message";
+
+    private final AtomicInteger updateRepoCalledCount;
+    private final AtomicInteger updateChangeCalledCount;
+
+    private UpdateRefAndAddMessageOp(
+        AtomicInteger updateRepoCalledCount, AtomicInteger updateChangeCalledCount) {
+      this.updateRepoCalledCount = updateRepoCalledCount;
+      this.updateChangeCalledCount = updateChangeCalledCount;
+    }
+
+    @Override
+    public void updateRepo(RepoContext ctx) throws Exception {
+      String master = "refs/heads/master";
+      ObjectId oldId = ctx.getRepoView().getRef(master).get();
+      ObjectId newId = newCommit(ctx.getRevWalk(), ctx.getInserter(), oldId, COMMIT_MESSAGE);
+      ctx.addRefUpdate(oldId, newId, master);
+      updateRepoCalledCount.incrementAndGet();
+    }
+
+    @Override
+    public boolean updateChange(ChangeContext ctx) throws Exception {
+      ctx.getUpdate(ctx.getChange().currentPatchSetId()).setChangeMessage(CHANGE_MESSAGE);
+      updateChangeCalledCount.incrementAndGet();
+      return true;
+    }
+  }
+
+  private ObjectId newCommit(RevWalk rw, ObjectInserter ins, ObjectId parent, String msg)
+      throws IOException {
+    PersonIdent ident = serverIdent.get();
+    CommitBuilder cb = new CommitBuilder();
+    cb.setParentId(parent);
+    cb.setTreeId(rw.parseCommit(parent).getTree());
+    cb.setMessage(msg);
+    cb.setAuthor(ident);
+    cb.setCommitter(ident);
+    return ins.insert(Constants.OBJ_COMMIT, cb.build());
+  }
+
+  private BatchUpdate newBatchUpdate(BatchUpdate.Factory buf) {
+    return buf.create(db, project, identifiedUserFactory.create(user.getId()), TimeUtil.nowTs());
   }
 
   private Optional<ObjectId> getRef(String name) throws Exception {
@@ -142,4 +323,15 @@
         .map(m -> m.message)
         .collect(toList());
   }
+
+  private static List<String> commitMessages(
+      Repository repo, ObjectId fromExclusive, ObjectId toInclusive) throws Exception {
+    try (RevWalk rw = new RevWalk(repo)) {
+      rw.markStart(rw.parseCommit(toInclusive));
+      rw.markUninteresting(rw.parseCommit(fromExclusive));
+      rw.sort(RevSort.REVERSE);
+      rw.setRetainBody(true);
+      return Streams.stream(rw).map(c -> c.getShortMessage()).collect(toList());
+    }
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
index dd2e8af..d628268 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
@@ -62,7 +62,6 @@
 import com.google.gerrit.server.notedb.PrimaryStorageMigrator;
 import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.NoteDbMode;
 import com.google.gerrit.testutil.TestTimeUtil;
@@ -95,7 +94,6 @@
   }
 
   @Inject private AllUsersName allUsers;
-  @Inject private BatchUpdate.Factory batchUpdateFactory;
   @Inject private ChangeBundleReader bundleReader;
   @Inject private CommentsUtil commentsUtil;
   @Inject private TestChangeRebuilderWrapper rebuilderWrapper;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java
index b50bcf3..1552554 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java
@@ -27,6 +27,8 @@
 
   public String topic;
   public ChangeStatus status;
+  public Boolean isPrivate;
+  public Boolean workInProgress;
   public String baseChange;
   public Boolean newBranch;
   public MergeInput merge;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index abcfe31..9149230 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -142,6 +142,9 @@
 
   public final native boolean isPrivate() /*-{ return this.is_private ? true : false; }-*/;
 
+  public final native boolean
+      isWorkInProgress() /*-{ return this.work_in_progress ? true : false; }-*/;
+
   public final native NativeMap<LabelInfo> allLabels() /*-{ return this.labels; }-*/;
 
   public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index 1e29be8..6e088c4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
@@ -199,6 +199,7 @@
 
   @UiField Element statusText;
   @UiField Element privateText;
+  @UiField Element wipText;
   @UiField Image projectSettings;
   @UiField AnchorElement projectSettingsLink;
   @UiField InlineHyperlink projectDashboard;
@@ -1392,6 +1393,10 @@
       privateText.setInnerText(Util.C.isPrivate());
     }
 
+    if (info.isWorkInProgress()) {
+      wipText.setInnerText(Util.C.isWorkInProgress());
+    }
+
     if (Gerrit.isSignedIn()) {
       replyAction =
           new ReplyAction(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
index e2297cb..09bdc24 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
@@ -103,6 +103,10 @@
       font-weight: bold;
     }
 
+    .wipText {
+      font-weight: bold;
+    }
+
     div.popdown {
       display: inline-block;
       margin-top: 2px;
@@ -380,7 +384,8 @@
             <ui:msg>Change <g:Anchor ui:field='permalink' title='Reload the change (Shortcut: R)'>
               <ui:attribute name='title'/>
             </g:Anchor> - <span ui:field='statusText' class='{style.statusText}'/>
-              <span ui:field='privateText' class='{style.privateText}'/></ui:msg>
+              <span ui:field='privateText' class='{style.privateText}'/>
+              <span ui:field='wipText' class='{style.wipText}'/></ui:msg>
           </span>
           <g:SimplePanel ui:field='headerExtension' styleName='{style.headerExtension}'/>
         </div>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 4543217..80049df 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -35,6 +35,8 @@
 
   String isPrivate();
 
+  String isWorkInProgress();
+
   String changeEdit();
 
   String myDashboardTitle();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 3545a2f..8a9f323 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -8,6 +8,7 @@
 notCurrent = Not Current
 changeEdit = Change Edit
 isPrivate = (Private)
+isWorkInProgress = (WorkInProgress)
 
 myDashboardTitle = My Reviews
 unknownDashboardTitle = Code Review Dashboard
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 3385bf2..05e1698 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.restapi;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
 import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS;
 import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS;
@@ -115,6 +116,7 @@
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
 import com.google.inject.util.Providers;
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
@@ -638,40 +640,21 @@
   }
 
   private static Type inputType(RestModifyView<RestResource, Object> m) {
-    Type inputType = extractInputType(m.getClass());
-    if (inputType == null) {
-      throw new IllegalStateException(
-          String.format(
-              "View %s does not correctly implement %s",
-              m.getClass(), RestModifyView.class.getSimpleName()));
-    }
-    return inputType;
-  }
+    // MyModifyView implements RestModifyView<SomeResource, MyInput>
+    TypeLiteral<?> typeLiteral = TypeLiteral.get(m.getClass());
 
-  @SuppressWarnings("rawtypes")
-  private static Type extractInputType(Class clazz) {
-    for (Type t : clazz.getGenericInterfaces()) {
-      if (t instanceof ParameterizedType
-          && ((ParameterizedType) t).getRawType() == RestModifyView.class) {
-        return ((ParameterizedType) t).getActualTypeArguments()[1];
-      }
-    }
+    // RestModifyView<SomeResource, MyInput>
+    // This is smart enough to resolve even when there are intervening subclasses, even if they have
+    // reordered type arguments.
+    TypeLiteral<?> supertypeLiteral = typeLiteral.getSupertype(RestModifyView.class);
 
-    if (clazz.getSuperclass() != null) {
-      Type i = extractInputType(clazz.getSuperclass());
-      if (i != null) {
-        return i;
-      }
-    }
-
-    for (Class t : clazz.getInterfaces()) {
-      Type i = extractInputType(t);
-      if (i != null) {
-        return i;
-      }
-    }
-
-    return null;
+    Type supertype = supertypeLiteral.getType();
+    checkState(
+        supertype instanceof ParameterizedType,
+        "supertype of %s is not parameterized: %s",
+        typeLiteral,
+        supertypeLiteral);
+    return ((ParameterizedType) supertype).getActualTypeArguments()[1];
   }
 
   private Object parseRequest(HttpServletRequest req, Type type)
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index 8ba0978..1a79d57 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.common.base.Throwables;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.AccessSection;
@@ -38,7 +39,6 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.RefControl;
@@ -115,6 +115,9 @@
     this.updateFactory = updateFactory;
   }
 
+  // TODO(dborowitz): Hack MetaDataUpdate so it can be created within a BatchUpdate and we can avoid
+  // calling setUpdateRef(false).
+  @SuppressWarnings("deprecation")
   @Override
   protected Change.Id updateProjectConfig(
       ProjectControl projectControl,
@@ -175,13 +178,10 @@
       AddReviewerInput input = new AddReviewerInput();
       input.reviewer = projectOwners;
       reviewersProvider.get().apply(rsrc, input);
-    } catch (IOException
-        | OrmException
-        | RestApiException
-        | UpdateException
-        | PermissionBackendException e) {
+    } catch (Exception e) {
       // one of the owner groups is not visible to the user and this it why it
       // can't be added as reviewer
+      Throwables.throwIfUnchecked(e);
     }
   }
 
@@ -198,12 +198,9 @@
         AddReviewerInput input = new AddReviewerInput();
         input.reviewer = r.getGroup().getUUID().get();
         reviewersProvider.get().apply(rsrc, input);
-      } catch (IOException
-          | OrmException
-          | RestApiException
-          | UpdateException
-          | PermissionBackendException e) {
+      } catch (Exception e) {
         // ignore
+        Throwables.throwIfUnchecked(e);
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
index 7b4777e..08f879f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -53,7 +53,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -65,6 +64,7 @@
     final Timer0 recommendAccountsLatency;
     final Timer0 loadAccountsLatency;
     final Timer0 queryGroupsLatency;
+    final Timer0 filterVisibility;
 
     @Inject
     Metrics(MetricMaker metricMaker) {
@@ -92,12 +92,18 @@
               new Description("Latency for querying groups for reviewer suggestion")
                   .setCumulative()
                   .setUnit(Units.MILLISECONDS));
+      filterVisibility =
+          metricMaker.newTimer(
+              "reviewer_suggestion/filter_visibility",
+              new Description("Latency for removing users that can't see the change")
+                  .setCumulative()
+                  .setUnit(Units.MILLISECONDS));
     }
   }
 
   // Generate a candidate list at 3x the size of what the user wants to see to
   // give the ranking algorithm a good set of candidates it can work with
-  private static final int CANDIDATE_LIST_MULTIPLIER = 3;
+  private static final int CANDIDATE_LIST_MULTIPLIER = 2;
 
   private final AccountLoader accountLoader;
   private final AccountQueryBuilder accountQueryBuilder;
@@ -150,13 +156,26 @@
 
     List<Account.Id> candidateList = new ArrayList<>();
     if (!Strings.isNullOrEmpty(query)) {
-      candidateList = suggestAccounts(suggestReviewers, visibilityControl);
+      candidateList = suggestAccounts(suggestReviewers);
     }
 
     List<Account.Id> sortedRecommendations =
         recommendAccounts(changeNotes, suggestReviewers, projectControl, candidateList);
-    List<SuggestedReviewerInfo> suggestedReviewer = loadAccounts(sortedRecommendations);
 
+    // Filter accounts by visibility and enforce limit
+    List<Account.Id> filteredRecommendations = new ArrayList<>();
+    try (Timer0.Context ctx = metrics.filterVisibility.start()) {
+      for (Account.Id reviewer : sortedRecommendations) {
+        if (filteredRecommendations.size() >= limit) {
+          break;
+        }
+        if (visibilityControl.isVisibleTo(reviewer)) {
+          filteredRecommendations.add(reviewer);
+        }
+      }
+    }
+
+    List<SuggestedReviewerInfo> suggestedReviewer = loadAccounts(filteredRecommendations);
     if (!excludeGroups && suggestedReviewer.size() < limit && !Strings.isNullOrEmpty(query)) {
       // Add groups at the end as individual accounts are usually more
       // important.
@@ -174,22 +193,14 @@
     return suggestedReviewer.subList(0, limit);
   }
 
-  private List<Account.Id> suggestAccounts(
-      SuggestReviewers suggestReviewers, VisibilityControl visibilityControl) throws OrmException {
+  private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers) throws OrmException {
     try (Timer0.Context ctx = metrics.queryAccountsLatency.start()) {
       try {
-        Set<Account.Id> matches = new HashSet<>();
         QueryResult<AccountState> result =
             accountQueryProcessor
                 .setLimit(suggestReviewers.getLimit() * CANDIDATE_LIST_MULTIPLIER)
                 .query(accountQueryBuilder.defaultQuery(suggestReviewers.getQuery()));
-        for (AccountState accountState : result.entities()) {
-          Account.Id id = accountState.getAccount().getId();
-          if (visibilityControl.isVisibleTo(id)) {
-            matches.add(id);
-          }
-        }
-        return new ArrayList<>(matches);
+        return result.entities().stream().map(a -> a.getAccount().getId()).collect(toList());
       } catch (QueryParseException e) {
         return ImmutableList.of();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/ApiUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/ApiUtil.java
new file mode 100644
index 0000000..c5b8b12
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/ApiUtil.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api;
+
+import com.google.common.base.Throwables;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+/** Static utilities for API implementations. */
+public class ApiUtil {
+  /**
+   * Convert an exception encountered during API execution to a {@link RestApiException}.
+   *
+   * @param msg message to be used in the case where a new {@code RestApiException} is wrapped
+   *     around {@code e}.
+   * @param e exception being handled.
+   * @return {@code e} if it is already a {@code RestApiException}, otherwise a new {@code
+   *     RestApiException} wrapped around {@code e}.
+   * @throws RuntimeException if {@code e} is a runtime exception, it is rethrown as-is.
+   */
+  public static RestApiException asRestApiException(String msg, Exception e)
+      throws RuntimeException {
+    Throwables.throwIfUnchecked(e);
+    return e instanceof RestApiException ? (RestApiException) e : new RestApiException(msg, e);
+  }
+
+  private ApiUtil() {}
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 6604496..64760a65 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.api.accounts;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 
 import com.google.gerrit.common.RawInputUtil;
-import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.accounts.AccountApi;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
@@ -38,7 +38,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AddSshKey;
@@ -71,15 +70,11 @@
 import com.google.gerrit.server.account.Stars;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 import java.util.SortedSet;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 
 public class AccountApiImpl implements AccountApi {
   interface Factory {
@@ -203,8 +198,8 @@
       AccountInfo ai = accountLoader.get(account.getUser().getAccountId());
       accountLoader.fill();
       return ai;
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot parse change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot parse change", e);
     }
   }
 
@@ -222,8 +217,8 @@
       } else {
         deleteActive.apply(account, new DeleteActive.Input());
       }
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot set active", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set active", e);
     }
   }
 
@@ -237,8 +232,8 @@
   public GeneralPreferencesInfo getPreferences() throws RestApiException {
     try {
       return getPreferences.apply(account);
-    } catch (PermissionBackendException e) {
-      throw new RestApiException("Cannot get preferences", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get preferences", e);
     }
   }
 
@@ -246,8 +241,8 @@
   public GeneralPreferencesInfo setPreferences(GeneralPreferencesInfo in) throws RestApiException {
     try {
       return setPreferences.apply(account, in);
-    } catch (IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot set preferences", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set preferences", e);
     }
   }
 
@@ -255,8 +250,8 @@
   public DiffPreferencesInfo getDiffPreferences() throws RestApiException {
     try {
       return getDiffPreferences.apply(account);
-    } catch (IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot query diff preferences", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot query diff preferences", e);
     }
   }
 
@@ -264,8 +259,8 @@
   public DiffPreferencesInfo setDiffPreferences(DiffPreferencesInfo in) throws RestApiException {
     try {
       return setDiffPreferences.apply(account, in);
-    } catch (IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot set diff preferences", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set diff preferences", e);
     }
   }
 
@@ -273,8 +268,8 @@
   public EditPreferencesInfo getEditPreferences() throws RestApiException {
     try {
       return getEditPreferences.apply(account);
-    } catch (IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot query edit preferences", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot query edit preferences", e);
     }
   }
 
@@ -282,8 +277,8 @@
   public EditPreferencesInfo setEditPreferences(EditPreferencesInfo in) throws RestApiException {
     try {
       return setEditPreferences.apply(account, in);
-    } catch (IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot set edit preferences", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set edit preferences", e);
     }
   }
 
@@ -291,8 +286,8 @@
   public List<ProjectWatchInfo> getWatchedProjects() throws RestApiException {
     try {
       return getWatchedProjects.apply(account);
-    } catch (OrmException | IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot get watched projects", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get watched projects", e);
     }
   }
 
@@ -301,8 +296,8 @@
       throws RestApiException {
     try {
       return postWatchedProjects.apply(account, in);
-    } catch (OrmException | IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot update watched projects", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot update watched projects", e);
     }
   }
 
@@ -310,8 +305,8 @@
   public void deleteWatchedProjects(List<ProjectWatchInfo> in) throws RestApiException {
     try {
       deleteWatchedProjects.apply(account, in);
-    } catch (OrmException | IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot delete watched projects", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete watched projects", e);
     }
   }
 
@@ -321,8 +316,8 @@
       ChangeResource rsrc = changes.parse(TopLevelResource.INSTANCE, IdString.fromUrl(changeId));
       starredChangesCreate.setChange(rsrc);
       starredChangesCreate.apply(account, new StarredChanges.EmptyInput());
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot star change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot star change", e);
     }
   }
 
@@ -333,8 +328,8 @@
       AccountResource.StarredChange starredChange =
           new AccountResource.StarredChange(account.getUser(), rsrc);
       starredChangesDelete.apply(starredChange, new StarredChanges.EmptyInput());
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot unstar change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot unstar change", e);
     }
   }
 
@@ -343,8 +338,8 @@
     try {
       AccountResource.Star rsrc = stars.parse(account, IdString.fromUrl(changeId));
       starsPost.apply(rsrc, input);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot post stars", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot post stars", e);
     }
   }
 
@@ -353,8 +348,8 @@
     try {
       AccountResource.Star rsrc = stars.parse(account, IdString.fromUrl(changeId));
       return starsGet.apply(rsrc);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot get stars", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get stars", e);
     }
   }
 
@@ -362,8 +357,8 @@
   public List<ChangeInfo> getStarredChanges() throws RestApiException {
     try {
       return stars.list().apply(account);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot get starred changes", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get starred changes", e);
     }
   }
 
@@ -377,12 +372,8 @@
     AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), input.email);
     try {
       createEmailFactory.create(input.email).apply(rsrc, input);
-    } catch (EmailException
-        | OrmException
-        | IOException
-        | ConfigInvalidException
-        | PermissionBackendException e) {
-      throw new RestApiException("Cannot add email", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot add email", e);
     }
   }
 
@@ -391,8 +382,8 @@
     AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), email);
     try {
       deleteEmail.apply(rsrc, null);
-    } catch (OrmException | IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot delete email", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete email", e);
     }
   }
 
@@ -401,8 +392,8 @@
     PutStatus.Input in = new PutStatus.Input(status);
     try {
       putStatus.apply(account, in);
-    } catch (OrmException | IOException | PermissionBackendException e) {
-      throw new RestApiException("Cannot set status", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set status", e);
     }
   }
 
@@ -410,8 +401,8 @@
   public List<SshKeyInfo> listSshKeys() throws RestApiException {
     try {
       return getSshKeys.apply(account);
-    } catch (OrmException | IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot list SSH keys", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list SSH keys", e);
     }
   }
 
@@ -421,8 +412,8 @@
     in.raw = RawInputUtil.create(key);
     try {
       return addSshKey.apply(account, in).value();
-    } catch (OrmException | IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot add SSH key", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot add SSH key", e);
     }
   }
 
@@ -432,8 +423,8 @@
       AccountResource.SshKey sshKeyRes =
           sshKeys.parse(account, IdString.fromDecoded(Integer.toString(seq)));
       deleteSshKey.apply(sshKeyRes, null);
-    } catch (OrmException | IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot delete SSH key", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete SSH key", e);
     }
   }
 
@@ -441,8 +432,8 @@
   public Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException {
     try {
       return gpgApiAdapter.listGpgKeys(account);
-    } catch (GpgException e) {
-      throw new RestApiException("Cannot list GPG keys", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list GPG keys", e);
     }
   }
 
@@ -451,8 +442,8 @@
       throws RestApiException {
     try {
       return gpgApiAdapter.putGpgKeys(account, add, delete);
-    } catch (GpgException e) {
-      throw new RestApiException("Cannot add GPG key", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot add GPG key", e);
     }
   }
 
@@ -460,8 +451,8 @@
   public GpgKeyApi gpgKey(String id) throws RestApiException {
     try {
       return gpgApiAdapter.gpgKey(account, IdString.fromDecoded(id));
-    } catch (GpgException e) {
-      throw new RestApiException("Cannot get PGP key", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get PGP key", e);
     }
   }
 
@@ -476,8 +467,8 @@
       AgreementInput input = new AgreementInput();
       input.name = agreementName;
       putAgreement.apply(account, input);
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot sign agreement", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot sign agreement", e);
     }
   }
 
@@ -485,8 +476,8 @@
   public void index() throws RestApiException {
     try {
       index.apply(account, new Index.Input());
-    } catch (IOException | PermissionBackendException e) {
-      throw new RestApiException("Cannot index account", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot index account", e);
     }
   }
 
@@ -494,8 +485,8 @@
   public List<AccountExternalIdInfo> getExternalIds() throws RestApiException {
     try {
       return getExternalIds.apply(account);
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot get external IDs", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get external IDs", e);
     }
   }
 
@@ -503,8 +494,8 @@
   public void deleteExternalIds(List<String> externalIds) throws RestApiException {
     try {
       deleteExternalIds.apply(account, externalIds);
-    } catch (IOException | OrmException | ConfigInvalidException e) {
-      throw new RestApiException("Cannot delete external IDs", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete external IDs", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
index bade8ce..5257aec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.api.accounts;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
 import com.google.gerrit.extensions.api.accounts.AccountApi;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
@@ -33,14 +34,10 @@
 import com.google.gerrit.server.account.QueryAccounts;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.IOException;
 import java.util.List;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class AccountsImpl implements Accounts {
@@ -71,8 +68,8 @@
   public AccountApi id(String id) throws RestApiException {
     try {
       return api.create(accounts.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(id)));
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot parse change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot parse change", e);
     }
   }
 
@@ -106,8 +103,8 @@
       permissionBackend.user(self).checkAny(GlobalPermission.fromAnnotation(impl.getClass()));
       AccountInfo info = impl.apply(TopLevelResource.INSTANCE, in).value();
       return id(info._accountId);
-    } catch (OrmException | IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot create account " + in.username, e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot create account " + in.username, e);
     }
   }
 
@@ -133,8 +130,8 @@
       myQueryAccounts.setQuery(r.getQuery());
       myQueryAccounts.setLimit(r.getLimit());
       return myQueryAccounts.apply(TopLevelResource.INSTANCE);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve suggested accounts", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve suggested accounts", e);
     }
   }
 
@@ -163,8 +160,8 @@
         myQueryAccounts.addOption(option);
       }
       return myQueryAccounts.apply(TopLevelResource.INSTANCE);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve suggested accounts", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve suggested accounts", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index f75adbc..5338e89 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.server.api.changes;
 
-import com.google.gerrit.common.errors.EmailException;
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
@@ -82,14 +83,9 @@
 import com.google.gerrit.server.change.Unignore;
 import com.google.gerrit.server.change.Unmute;
 import com.google.gerrit.server.change.WorkInProgressOp;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
@@ -247,8 +243,8 @@
   public RevisionApi revision(String id) throws RestApiException {
     try {
       return revisionApi.create(revisions.parse(change, IdString.fromDecoded(id)));
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot parse revision", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot parse revision", e);
     }
   }
 
@@ -256,8 +252,8 @@
   public ReviewerApi reviewer(String id) throws RestApiException {
     try {
       return reviewerApi.create(reviewers.parse(change, IdString.fromDecoded(id)));
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot parse reviewer", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot parse reviewer", e);
     }
   }
 
@@ -270,8 +266,8 @@
   public void abandon(AbandonInput in) throws RestApiException {
     try {
       abandon.apply(change, in);
-    } catch (OrmException | UpdateException | PermissionBackendException e) {
-      throw new RestApiException("Cannot abandon change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot abandon change", e);
     }
   }
 
@@ -284,8 +280,8 @@
   public void restore(RestoreInput in) throws RestApiException {
     try {
       restore.apply(change, in);
-    } catch (OrmException | UpdateException | PermissionBackendException e) {
-      throw new RestApiException("Cannot restore change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot restore change", e);
     }
   }
 
@@ -300,8 +296,8 @@
   public void move(MoveInput in) throws RestApiException {
     try {
       move.apply(change, in);
-    } catch (OrmException | UpdateException e) {
-      throw new RestApiException("Cannot move change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot move change", e);
     }
   }
 
@@ -313,8 +309,8 @@
       } else {
         deletePrivate.apply(change, null);
       }
-    } catch (UpdateException e) {
-      throw new RestApiException("Cannot change private status", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot change private status", e);
     }
   }
 
@@ -322,8 +318,8 @@
   public void setWorkInProgress(String message) throws RestApiException {
     try {
       setWip.apply(change, new WorkInProgressOp.Input(message));
-    } catch (UpdateException e) {
-      throw new RestApiException("Cannot set work in progress state", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set work in progress state", e);
     }
   }
 
@@ -331,8 +327,8 @@
   public void setReadyForReview(String message) throws RestApiException {
     try {
       setReady.apply(change, new WorkInProgressOp.Input(message));
-    } catch (UpdateException e) {
-      throw new RestApiException("Cannot set ready for review state", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set ready for review state", e);
     }
   }
 
@@ -345,8 +341,8 @@
   public ChangeApi revert(RevertInput in) throws RestApiException {
     try {
       return changeApi.id(revert.apply(change, in)._number);
-    } catch (OrmException | IOException | UpdateException e) {
-      throw new RestApiException("Cannot revert change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot revert change", e);
     }
   }
 
@@ -354,8 +350,8 @@
   public ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException {
     try {
       return updateByMerge.apply(change, in).value();
-    } catch (IOException | UpdateException | InvalidChangeOperationException | OrmException e) {
-      throw new RestApiException("Cannot update change by merge", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot update change by merge", e);
     }
   }
 
@@ -383,8 +379,8 @@
           .addListChangesOption(listOptions)
           .addSubmittedTogetherOption(submitOptions)
           .applyInfo(change);
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot query submittedTogether", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot query submittedTogether", e);
     }
   }
 
@@ -392,8 +388,8 @@
   public void publish() throws RestApiException {
     try {
       publishDraftChange.apply(change, null);
-    } catch (UpdateException e) {
-      throw new RestApiException("Cannot publish change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot publish change", e);
     }
   }
 
@@ -406,12 +402,8 @@
   public void rebase(RebaseInput in) throws RestApiException {
     try {
       rebase.apply(change, in);
-    } catch (EmailException
-        | OrmException
-        | UpdateException
-        | IOException
-        | PermissionBackendException e) {
-      throw new RestApiException("Cannot rebase change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot rebase change", e);
     }
   }
 
@@ -419,8 +411,8 @@
   public void delete() throws RestApiException {
     try {
       deleteChange.apply(change, null);
-    } catch (UpdateException | PermissionBackendException e) {
-      throw new RestApiException("Cannot delete change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete change", e);
     }
   }
 
@@ -435,8 +427,8 @@
     in.topic = topic;
     try {
       putTopic.apply(change, in);
-    } catch (UpdateException | PermissionBackendException e) {
-      throw new RestApiException("Cannot set topic", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set topic", e);
     }
   }
 
@@ -444,8 +436,8 @@
   public IncludedInInfo includedIn() throws RestApiException {
     try {
       return includedIn.apply(change);
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Could not extract IncludedIn data", e);
+    } catch (Exception e) {
+      throw asRestApiException("Could not extract IncludedIn data", e);
     }
   }
 
@@ -460,8 +452,8 @@
   public AddReviewerResult addReviewer(AddReviewerInput in) throws RestApiException {
     try {
       return postReviewers.apply(change, in);
-    } catch (OrmException | IOException | UpdateException | PermissionBackendException e) {
-      throw new RestApiException("Cannot add change reviewer", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot add change reviewer", e);
     }
   }
 
@@ -486,8 +478,8 @@
       suggestReviewers.setQuery(r.getQuery());
       suggestReviewers.setLimit(r.getLimit());
       return suggestReviewers.apply(change);
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot retrieve suggested reviewers", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve suggested reviewers", e);
     }
   }
 
@@ -495,8 +487,8 @@
   public ChangeInfo get(EnumSet<ListChangesOption> s) throws RestApiException {
     try {
       return changeJson.create(s).format(change);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve change", e);
     }
   }
 
@@ -524,8 +516,8 @@
   public void setHashtags(HashtagsInput input) throws RestApiException {
     try {
       postHashtags.apply(change, input);
-    } catch (UpdateException | PermissionBackendException e) {
-      throw new RestApiException("Cannot post hashtags", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot post hashtags", e);
     }
   }
 
@@ -533,8 +525,8 @@
   public Set<String> getHashtags() throws RestApiException {
     try {
       return getHashtags.apply(change).value();
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot get hashtags", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get hashtags", e);
     }
   }
 
@@ -542,8 +534,8 @@
   public AccountInfo setAssignee(AssigneeInput input) throws RestApiException {
     try {
       return putAssignee.apply(change, input);
-    } catch (UpdateException | IOException | OrmException | PermissionBackendException e) {
-      throw new RestApiException("Cannot set assignee", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set assignee", e);
     }
   }
 
@@ -552,8 +544,8 @@
     try {
       Response<AccountInfo> r = getAssignee.apply(change);
       return r.isNone() ? null : r.value();
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot get assignee", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get assignee", e);
     }
   }
 
@@ -561,8 +553,8 @@
   public List<AccountInfo> getPastAssignees() throws RestApiException {
     try {
       return getPastAssignees.apply(change).value();
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot get past assignees", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get past assignees", e);
     }
   }
 
@@ -571,8 +563,8 @@
     try {
       Response<AccountInfo> r = deleteAssignee.apply(change, null);
       return r.isNone() ? null : r.value();
-    } catch (UpdateException | OrmException | PermissionBackendException e) {
-      throw new RestApiException("Cannot delete assignee", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete assignee", e);
     }
   }
 
@@ -580,8 +572,8 @@
   public Map<String, List<CommentInfo>> comments() throws RestApiException {
     try {
       return listComments.apply(change);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot get comments", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get comments", e);
     }
   }
 
@@ -589,8 +581,8 @@
   public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException {
     try {
       return listChangeRobotComments.apply(change);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot get robot comments", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get robot comments", e);
     }
   }
 
@@ -598,8 +590,8 @@
   public Map<String, List<CommentInfo>> drafts() throws RestApiException {
     try {
       return listDrafts.apply(change);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot get drafts", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get drafts", e);
     }
   }
 
@@ -607,17 +599,19 @@
   public ChangeInfo check() throws RestApiException {
     try {
       return check.apply(change).value();
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot check change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot check change", e);
     }
   }
 
   @Override
   public ChangeInfo check(FixInput fix) throws RestApiException {
     try {
+      // TODO(dborowitz): Convert to RetryingRestModifyView. Needs to plumb BatchUpdate.Factory into
+      // ConsistencyChecker.
       return check.apply(change, fix).value();
-    } catch (OrmException | PermissionBackendException e) {
-      throw new RestApiException("Cannot check change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot check change", e);
     }
   }
 
@@ -625,13 +619,15 @@
   public void index() throws RestApiException {
     try {
       index.apply(change, new Index.Input());
-    } catch (IOException | OrmException | PermissionBackendException e) {
-      throw new RestApiException("Cannot index change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot index change", e);
     }
   }
 
   @Override
   public void ignore(boolean ignore) throws RestApiException {
+    // TODO(dborowitz): Convert to RetryingRestModifyView. Needs to plumb BatchUpdate.Factory into
+    // StarredChangesUtil.
     if (ignore) {
       this.ignore.apply(change, new Ignore.Input());
     } else {
@@ -641,6 +637,8 @@
 
   @Override
   public void mute(boolean mute) throws RestApiException {
+    // TODO(dborowitz): Convert to RetryingRestModifyView. Needs to plumb BatchUpdate.Factory into
+    // StarredChangesUtil.
     if (mute) {
       this.mute.apply(change, new Mute.Input());
     } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
index 80d5071..5184e89 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.changes;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.changes.ChangeEditApi;
 import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
 import com.google.gerrit.extensions.common.EditInfo;
@@ -30,7 +32,6 @@
 import com.google.gerrit.server.change.DeleteChangeEdit;
 import com.google.gerrit.server.change.PublishChangeEdit;
 import com.google.gerrit.server.change.RebaseChangeEdit;
-import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -88,8 +89,8 @@
     try {
       Response<EditInfo> edit = editDetail.apply(changeResource);
       return edit.isNone() ? Optional.empty() : Optional.of(edit.value());
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot retrieve change edit", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve change edit", e);
     }
   }
 
@@ -97,8 +98,8 @@
   public void create() throws RestApiException {
     try {
       changeEditsPost.apply(changeResource, null);
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot create change edit", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot create change edit", e);
     }
   }
 
@@ -106,8 +107,8 @@
   public void delete() throws RestApiException {
     try {
       deleteChangeEdit.apply(changeResource, new DeleteChangeEdit.Input());
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot delete change edit", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete change edit", e);
     }
   }
 
@@ -115,8 +116,8 @@
   public void rebase() throws RestApiException {
     try {
       rebaseChangeEdit.apply(changeResource, null);
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot rebase change edit", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot rebase change edit", e);
     }
   }
 
@@ -129,8 +130,8 @@
   public void publish(PublishChangeEditInput publishChangeEditInput) throws RestApiException {
     try {
       publishChangeEdit.apply(changeResource, publishChangeEditInput);
-    } catch (IOException | OrmException | UpdateException e) {
-      throw new RestApiException("Cannot publish change edit", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot publish change edit", e);
     }
   }
 
@@ -140,8 +141,8 @@
       ChangeEditResource changeEditResource = getChangeEditResource(filePath);
       Response<BinaryResult> fileResponse = changeEditsGet.apply(changeEditResource);
       return fileResponse.isNone() ? Optional.empty() : Optional.of(fileResponse.value());
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot retrieve file of change edit", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve file of change edit", e);
     }
   }
 
@@ -152,8 +153,8 @@
       renameInput.oldPath = oldFilePath;
       renameInput.newPath = newFilePath;
       changeEditsPost.apply(changeResource, renameInput);
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot rename file of change edit", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot rename file of change edit", e);
     }
   }
 
@@ -163,8 +164,8 @@
       ChangeEdits.Post.Input restoreInput = new ChangeEdits.Post.Input();
       restoreInput.restorePath = filePath;
       changeEditsPost.apply(changeResource, restoreInput);
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot restore file of change edit", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot restore file of change edit", e);
     }
   }
 
@@ -172,8 +173,8 @@
   public void modifyFile(String filePath, RawInput newContent) throws RestApiException {
     try {
       changeEditsPut.apply(changeResource.getControl(), filePath, newContent);
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot modify file of change edit", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot modify file of change edit", e);
     }
   }
 
@@ -181,8 +182,8 @@
   public void deleteFile(String filePath) throws RestApiException {
     try {
       changeEditDeleteContent.apply(changeResource.getControl(), filePath);
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot delete file of change edit", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete file of change edit", e);
     }
   }
 
@@ -192,8 +193,8 @@
       try (BinaryResult binaryResult = getChangeEditCommitMessage.apply(changeResource)) {
         return binaryResult.asString();
       }
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot get commit message of change edit", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get commit message of change edit", e);
     }
   }
 
@@ -203,8 +204,8 @@
     input.message = newCommitMessage;
     try {
       modifyChangeEditCommitMessage.apply(changeResource, input);
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot modify commit message of change edit", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot modify commit message of change edit", e);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
index 9a89d48..b800655 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
@@ -24,7 +25,6 @@
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -32,15 +32,10 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.CreateChange;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.query.change.QueryChanges;
-import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.IOException;
 import java.util.List;
 
 @Singleton
@@ -78,8 +73,8 @@
   public ChangeApi id(String id) throws RestApiException {
     try {
       return api.create(changes.parse(TopLevelResource.INSTANCE, IdString.fromUrl(id)));
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot parse change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot parse change", e);
     }
   }
 
@@ -88,12 +83,8 @@
     try {
       ChangeInfo out = createChange.apply(TopLevelResource.INSTANCE, in).value();
       return api.create(changes.parse(new Change.Id(out._number)));
-    } catch (OrmException
-        | IOException
-        | InvalidChangeOperationException
-        | UpdateException
-        | PermissionBackendException e) {
-      throw new RestApiException("Cannot create change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot create change", e);
     }
   }
 
@@ -137,8 +128,8 @@
       List<ChangeInfo> infos = (List<ChangeInfo>) result;
 
       return ImmutableList.copyOf(infos);
-    } catch (AuthException | OrmException e) {
-      throw new RestApiException("Cannot query changes", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot query changes", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java
index 5c61e23..243833a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.server.api.changes;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.changes.CommentApi;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.change.CommentResource;
 import com.google.gerrit.server.change.GetComment;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -41,8 +42,8 @@
   public CommentInfo get() throws RestApiException {
     try {
       return getComment.apply(comment);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve comment", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve comment", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
index 1bd9216..2daf1dc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.changes;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.changes.DraftApi;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -22,8 +24,6 @@
 import com.google.gerrit.server.change.DraftCommentResource;
 import com.google.gerrit.server.change.GetDraftComment;
 import com.google.gerrit.server.change.PutDraftComment;
-import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -53,8 +53,8 @@
   public CommentInfo get() throws RestApiException {
     try {
       return getDraft.apply(draft);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve draft", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve draft", e);
     }
   }
 
@@ -62,8 +62,8 @@
   public CommentInfo update(DraftInput in) throws RestApiException {
     try {
       return putDraft.apply(draft, in).value();
-    } catch (UpdateException | OrmException e) {
-      throw new RestApiException("Cannot update draft", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot update draft", e);
     }
   }
 
@@ -71,8 +71,8 @@
   public void delete() throws RestApiException {
     try {
       deleteDraft.apply(draft, null);
-    } catch (UpdateException e) {
-      throw new RestApiException("Cannot delete draft", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete draft", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
index aa66e7b..f0a934f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.changes;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.changes.FileApi;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -21,11 +23,8 @@
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.change.GetContent;
 import com.google.gerrit.server.change.GetDiff;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 
 class FileApiImpl implements FileApi {
   interface Factory {
@@ -47,8 +46,8 @@
   public BinaryResult content() throws RestApiException {
     try {
       return getContent.apply(file);
-    } catch (IOException | OrmException e) {
-      throw new RestApiException("Cannot retrieve file content", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve file content", e);
     }
   }
 
@@ -56,8 +55,8 @@
   public DiffInfo diff() throws RestApiException {
     try {
       return getDiff.apply(file).value();
-    } catch (IOException | InvalidChangeOperationException | OrmException e) {
-      throw new RestApiException("Cannot retrieve diff", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve diff", e);
     }
   }
 
@@ -65,8 +64,8 @@
   public DiffInfo diff(String base) throws RestApiException {
     try {
       return getDiff.setBase(base).apply(file).value();
-    } catch (IOException | InvalidChangeOperationException | OrmException e) {
-      throw new RestApiException("Cannot retrieve diff", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve diff", e);
     }
   }
 
@@ -74,8 +73,8 @@
   public DiffInfo diff(int parent) throws RestApiException {
     try {
       return getDiff.setParent(parent).apply(file).value();
-    } catch (OrmException | InvalidChangeOperationException | IOException e) {
-      throw new RestApiException("Cannot retrieve diff", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve diff", e);
     }
   }
 
@@ -104,8 +103,8 @@
     }
     try {
       return getDiff.apply(file).value();
-    } catch (IOException | InvalidChangeOperationException | OrmException e) {
-      throw new RestApiException("Cannot retrieve diff", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve diff", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
index 8ac874a..2f8b7d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.changes;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
 import com.google.gerrit.extensions.api.changes.ReviewerApi;
@@ -23,8 +25,6 @@
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.VoteResource;
 import com.google.gerrit.server.change.Votes;
-import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.Map;
@@ -55,8 +55,8 @@
   public Map<String, Short> votes() throws RestApiException {
     try {
       return listVotes.apply(reviewer);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot list votes", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list votes", e);
     }
   }
 
@@ -64,8 +64,8 @@
   public void deleteVote(String label) throws RestApiException {
     try {
       deleteVote.apply(new VoteResource(reviewer, label), null);
-    } catch (UpdateException e) {
-      throw new RestApiException("Cannot delete vote", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete vote", e);
     }
   }
 
@@ -73,8 +73,8 @@
   public void deleteVote(DeleteVoteInput input) throws RestApiException {
     try {
       deleteVote.apply(new VoteResource(reviewer, input.label), input);
-    } catch (UpdateException e) {
-      throw new RestApiException("Cannot delete vote", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete vote", e);
     }
   }
 
@@ -87,8 +87,8 @@
   public void remove(DeleteReviewerInput input) throws RestApiException {
     try {
       deleteReviewer.apply(reviewer, input);
-    } catch (UpdateException e) {
-      throw new RestApiException("Cannot remove reviewer", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot remove reviewer", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 6440509..21fb578 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.server.api.changes;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
@@ -72,14 +73,9 @@
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.change.TestSubmitType;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -214,8 +210,8 @@
   public void review(ReviewInput in) throws RestApiException {
     try {
       review.apply(revision, in);
-    } catch (OrmException | UpdateException | IOException | PermissionBackendException e) {
-      throw new RestApiException("Cannot post review", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot post review", e);
     }
   }
 
@@ -228,9 +224,11 @@
   @Override
   public void submit(SubmitInput in) throws RestApiException {
     try {
+      // TODO(dborowitz): Convert to RetryingRestModifyHandler. Requires converting MergeOp to a
+      // Factory that takes BatchUpdate.Factory. (Enough Factories yet?)
       submit.apply(revision, in);
-    } catch (OrmException | IOException | PermissionBackendException e) {
-      throw new RestApiException("Cannot submit change", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot submit change", e);
     }
   }
 
@@ -242,10 +240,12 @@
   @Override
   public BinaryResult submitPreview(String format) throws RestApiException {
     try {
+      // TODO(dborowitz): Convert to RetryingRestModifyHandler. Requires converting MergeOp to a
+      // Factory that takes BatchUpdate.Factory.
       submitPreview.setFormat(format);
       return submitPreview.apply(revision);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot get submit preview", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get submit preview", e);
     }
   }
 
@@ -253,8 +253,8 @@
   public void publish() throws RestApiException {
     try {
       publish.apply(revision, new PublishDraftPatchSet.Input());
-    } catch (UpdateException e) {
-      throw new RestApiException("Cannot publish draft patch set", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot publish draft patch set", e);
     }
   }
 
@@ -262,8 +262,8 @@
   public void delete() throws RestApiException {
     try {
       deleteDraft.apply(revision, null);
-    } catch (UpdateException | OrmException | PermissionBackendException e) {
-      throw new RestApiException("Cannot delete draft ps", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete draft ps", e);
     }
   }
 
@@ -277,12 +277,8 @@
   public ChangeApi rebase(RebaseInput in) throws RestApiException {
     try {
       return changes.id(rebase.apply(revision, in)._number);
-    } catch (OrmException
-        | EmailException
-        | UpdateException
-        | IOException
-        | PermissionBackendException e) {
-      throw new RestApiException("Cannot rebase ps", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot rebase ps", e);
     }
   }
 
@@ -291,8 +287,8 @@
     try (Repository repo = repoManager.openRepository(revision.getProject());
         RevWalk rw = new RevWalk(repo)) {
       return rebaseUtil.canRebase(revision.getPatchSet(), revision.getChange().getDest(), repo, rw);
-    } catch (IOException e) {
-      throw new RestApiException("Cannot check if rebase is possible", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot check if rebase is possible", e);
     }
   }
 
@@ -300,8 +296,8 @@
   public ChangeApi cherryPick(CherryPickInput in) throws RestApiException {
     try {
       return changes.id(cherryPick.apply(revision, in)._number);
-    } catch (OrmException | IOException | UpdateException e) {
-      throw new RestApiException("Cannot cherry pick", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot cherry pick", e);
     }
   }
 
@@ -310,8 +306,8 @@
     try {
       return revisionReviewerApi.create(
           revisionReviewers.parse(revision, IdString.fromDecoded(id)));
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot parse reviewer", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot parse reviewer", e);
     }
   }
 
@@ -326,7 +322,7 @@
       }
       view.apply(files.parse(revision, IdString.fromDecoded(path)), new Reviewed.Input());
     } catch (Exception e) {
-      throw new RestApiException("Cannot update reviewed flag", e);
+      throw asRestApiException("Cannot update reviewed flag", e);
     }
   }
 
@@ -336,8 +332,8 @@
     try {
       return ImmutableSet.copyOf(
           (Iterable<String>) listFiles.setReviewed(true).apply(revision).value());
-    } catch (OrmException | IOException | PatchListNotAvailableException e) {
-      throw new RestApiException("Cannot list reviewed files", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list reviewed files", e);
     }
   }
 
@@ -345,8 +341,8 @@
   public MergeableInfo mergeable() throws RestApiException {
     try {
       return mergeable.apply(revision);
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot check mergeability", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot check mergeability", e);
     }
   }
 
@@ -355,8 +351,8 @@
     try {
       mergeable.setOtherBranches(true);
       return mergeable.apply(revision);
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot check mergeability", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot check mergeability", e);
     }
   }
 
@@ -365,8 +361,8 @@
   public Map<String, FileInfo> files() throws RestApiException {
     try {
       return (Map<String, FileInfo>) listFiles.apply(revision).value();
-    } catch (OrmException | IOException | PatchListNotAvailableException e) {
-      throw new RestApiException("Cannot retrieve files", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve files", e);
     }
   }
 
@@ -375,8 +371,8 @@
   public Map<String, FileInfo> files(String base) throws RestApiException {
     try {
       return (Map<String, FileInfo>) listFiles.setBase(base).apply(revision).value();
-    } catch (OrmException | IOException | PatchListNotAvailableException e) {
-      throw new RestApiException("Cannot retrieve files", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve files", e);
     }
   }
 
@@ -385,8 +381,8 @@
   public Map<String, FileInfo> files(int parentNum) throws RestApiException {
     try {
       return (Map<String, FileInfo>) listFiles.setParent(parentNum).apply(revision).value();
-    } catch (OrmException | IOException | PatchListNotAvailableException e) {
-      throw new RestApiException("Cannot retrieve files", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve files", e);
     }
   }
 
@@ -399,8 +395,8 @@
   public Map<String, List<CommentInfo>> comments() throws RestApiException {
     try {
       return listComments.apply(revision);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve comments", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve comments", e);
     }
   }
 
@@ -408,8 +404,8 @@
   public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException {
     try {
       return listRobotComments.apply(revision);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve robot comments", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve robot comments", e);
     }
   }
 
@@ -417,8 +413,8 @@
   public List<CommentInfo> commentsAsList() throws RestApiException {
     try {
       return listComments.getComments(revision);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve comments", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve comments", e);
     }
   }
 
@@ -426,8 +422,8 @@
   public Map<String, List<CommentInfo>> drafts() throws RestApiException {
     try {
       return listDrafts.apply(revision);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve drafts", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve drafts", e);
     }
   }
 
@@ -435,8 +431,8 @@
   public List<RobotCommentInfo> robotCommentsAsList() throws RestApiException {
     try {
       return listRobotComments.getComments(revision);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve robot comments", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve robot comments", e);
     }
   }
 
@@ -444,8 +440,8 @@
   public EditInfo applyFix(String fixId) throws RestApiException {
     try {
       return applyFix.apply(fixes.parse(revision, IdString.fromDecoded(fixId)), null).value();
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot apply fix", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot apply fix", e);
     }
   }
 
@@ -453,8 +449,8 @@
   public List<CommentInfo> draftsAsList() throws RestApiException {
     try {
       return listDrafts.getComments(revision);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve drafts", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve drafts", e);
     }
   }
 
@@ -462,8 +458,8 @@
   public DraftApi draft(String id) throws RestApiException {
     try {
       return draftFactory.create(drafts.parse(revision, IdString.fromDecoded(id)));
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve draft", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve draft", e);
     }
   }
 
@@ -476,8 +472,8 @@
           .id(revision.getChange().getId().get())
           .revision(revision.getPatchSet().getId().get())
           .draft(id);
-    } catch (UpdateException | OrmException e) {
-      throw new RestApiException("Cannot create draft", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot create draft", e);
     }
   }
 
@@ -485,8 +481,8 @@
   public CommentApi comment(String id) throws RestApiException {
     try {
       return commentFactory.create(comments.parse(revision, IdString.fromDecoded(id)));
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve comment", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve comment", e);
     }
   }
 
@@ -494,8 +490,8 @@
   public RobotCommentApi robotComment(String id) throws RestApiException {
     try {
       return robotCommentFactory.create(robotComments.parse(revision, IdString.fromDecoded(id)));
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve robot comment", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve robot comment", e);
     }
   }
 
@@ -503,8 +499,8 @@
   public BinaryResult patch() throws RestApiException {
     try {
       return getPatch.apply(revision);
-    } catch (IOException e) {
-      throw new RestApiException("Cannot get patch", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get patch", e);
     }
   }
 
@@ -512,8 +508,8 @@
   public BinaryResult patch(String path) throws RestApiException {
     try {
       return getPatch.setPath(path).apply(revision);
-    } catch (IOException e) {
-      throw new RestApiException("Cannot get patch", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get patch", e);
     }
   }
 
@@ -521,8 +517,8 @@
   public Map<String, ActionInfo> actions() throws RestApiException {
     try {
       return revisionActions.apply(revision).value();
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot get actions", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get actions", e);
     }
   }
 
@@ -530,8 +526,8 @@
   public SubmitType submitType() throws RestApiException {
     try {
       return getSubmitType.apply(revision);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot get submit type", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get submit type", e);
     }
   }
 
@@ -539,8 +535,8 @@
   public SubmitType testSubmitType(TestSubmitRuleInput in) throws RestApiException {
     try {
       return testSubmitType.apply(revision, in);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot test submit type", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot test submit type", e);
     }
   }
 
@@ -554,8 +550,8 @@
           gml.setUninterestingParent(getUninterestingParent());
           gml.setAddLinks(getAddLinks());
           return gml.apply(revision).value();
-        } catch (IOException e) {
-          throw new RestApiException("Cannot get merge list", e);
+        } catch (Exception e) {
+          throw asRestApiException("Cannot get merge list", e);
         }
       }
     };
@@ -567,8 +563,8 @@
     in.description = description;
     try {
       putDescription.apply(revision, in);
-    } catch (UpdateException | PermissionBackendException e) {
-      throw new RestApiException("Cannot set description", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set description", e);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java
index 5c56321..60dc1d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.changes;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
 import com.google.gerrit.extensions.api.changes.RevisionReviewerApi;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -21,8 +23,6 @@
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.VoteResource;
 import com.google.gerrit.server.change.Votes;
-import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.Map;
@@ -48,8 +48,8 @@
   public Map<String, Short> votes() throws RestApiException {
     try {
       return listVotes.apply(reviewer);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot list votes", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list votes", e);
     }
   }
 
@@ -57,8 +57,8 @@
   public void deleteVote(String label) throws RestApiException {
     try {
       deleteVote.apply(new VoteResource(reviewer, label), null);
-    } catch (UpdateException e) {
-      throw new RestApiException("Cannot delete vote", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete vote", e);
     }
   }
 
@@ -66,8 +66,8 @@
   public void deleteVote(DeleteVoteInput input) throws RestApiException {
     try {
       deleteVote.apply(new VoteResource(reviewer, input.label), input);
-    } catch (UpdateException e) {
-      throw new RestApiException("Cannot delete vote", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete vote", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java
index ded98cb..b19939b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.server.api.changes;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.changes.RobotCommentApi;
 import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.change.GetRobotComment;
 import com.google.gerrit.server.change.RobotCommentResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -41,8 +42,8 @@
   public RobotCommentInfo get() throws RestApiException {
     try {
       return getComment.apply(comment);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve robot comment", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve robot comment", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
index 21b42dd..87118c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.config;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.common.Version;
 import com.google.gerrit.extensions.api.config.AccessCheckInfo;
 import com.google.gerrit.extensions.api.config.AccessCheckInput;
@@ -32,13 +34,9 @@
 import com.google.gerrit.server.config.GetServerInfo;
 import com.google.gerrit.server.config.SetDiffPreferences;
 import com.google.gerrit.server.config.SetPreferences;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.IOException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 public class ServerImpl implements Server {
@@ -77,8 +75,8 @@
   public ServerInfo getInfo() throws RestApiException {
     try {
       return getServerInfo.apply(new ConfigResource());
-    } catch (IOException e) {
-      throw new RestApiException("Cannot get server info", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get server info", e);
     }
   }
 
@@ -86,8 +84,8 @@
   public GeneralPreferencesInfo getDefaultPreferences() throws RestApiException {
     try {
       return getPreferences.apply(new ConfigResource());
-    } catch (IOException | ConfigInvalidException e) {
-      throw new RestApiException("Cannot get default general preferences", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get default general preferences", e);
     }
   }
 
@@ -96,8 +94,8 @@
       throws RestApiException {
     try {
       return setPreferences.apply(new ConfigResource(), in);
-    } catch (IOException | ConfigInvalidException e) {
-      throw new RestApiException("Cannot set default general preferences", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set default general preferences", e);
     }
   }
 
@@ -105,8 +103,8 @@
   public DiffPreferencesInfo getDefaultDiffPreferences() throws RestApiException {
     try {
       return getDiffPreferences.apply(new ConfigResource());
-    } catch (IOException | ConfigInvalidException e) {
-      throw new RestApiException("Cannot get default diff preferences", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get default diff preferences", e);
     }
   }
 
@@ -115,8 +113,8 @@
       throws RestApiException {
     try {
       return setDiffPreferences.apply(new ConfigResource(), in);
-    } catch (IOException | ConfigInvalidException e) {
-      throw new RestApiException("Cannot set default diff preferences", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set default diff preferences", e);
     }
   }
 
@@ -124,8 +122,8 @@
   public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException {
     try {
       return checkConsistency.get().apply(new ConfigResource(), in);
-    } catch (IOException e) {
-      throw new RestApiException("Cannot check consistency", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot check consistency", e);
     }
   }
 
@@ -133,8 +131,8 @@
   public AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException {
     try {
       return checkAccess.get().apply(new ConfigResource(), in);
-    } catch (OrmException | IOException | PermissionBackendException e) {
-      throw new RestApiException("Cannot check access", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot check access", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
index 2c1ee3e..d7f868c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.groups;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -41,10 +43,8 @@
 import com.google.gerrit.server.group.PutName;
 import com.google.gerrit.server.group.PutOptions;
 import com.google.gerrit.server.group.PutOwner;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
 
@@ -119,8 +119,8 @@
   public GroupInfo get() throws RestApiException {
     try {
       return getGroup.apply(rsrc);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve group", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve group", e);
     }
   }
 
@@ -128,8 +128,8 @@
   public GroupInfo detail() throws RestApiException {
     try {
       return getDetail.apply(rsrc);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot retrieve group", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve group", e);
     }
   }
 
@@ -146,8 +146,8 @@
       putName.apply(rsrc, in);
     } catch (NoSuchGroupException e) {
       throw new ResourceNotFoundException(name, e);
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot put group name", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot put group name", e);
     }
   }
 
@@ -155,8 +155,8 @@
   public GroupInfo owner() throws RestApiException {
     try {
       return getOwner.apply(rsrc);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot get group owner", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get group owner", e);
     }
   }
 
@@ -166,8 +166,8 @@
     in.owner = owner;
     try {
       putOwner.apply(rsrc, in);
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot put group owner", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot put group owner", e);
     }
   }
 
@@ -182,8 +182,8 @@
     in.description = description;
     try {
       putDescription.apply(rsrc, in);
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot put group description", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot put group description", e);
     }
   }
 
@@ -196,8 +196,8 @@
   public void options(GroupOptionsInfo options) throws RestApiException {
     try {
       putOptions.apply(rsrc, options);
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot put group options", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot put group options", e);
     }
   }
 
@@ -211,8 +211,8 @@
     listMembers.setRecursive(recursive);
     try {
       return listMembers.apply(rsrc);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot list group members", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list group members", e);
     }
   }
 
@@ -220,8 +220,8 @@
   public void addMembers(String... members) throws RestApiException {
     try {
       addMembers.apply(rsrc, AddMembers.Input.fromMembers(Arrays.asList(members)));
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot add group members", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot add group members", e);
     }
   }
 
@@ -229,8 +229,8 @@
   public void removeMembers(String... members) throws RestApiException {
     try {
       deleteMembers.apply(rsrc, AddMembers.Input.fromMembers(Arrays.asList(members)));
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot remove group members", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot remove group members", e);
     }
   }
 
@@ -238,8 +238,8 @@
   public List<GroupInfo> includedGroups() throws RestApiException {
     try {
       return listGroups.apply(rsrc);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot list included groups", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list included groups", e);
     }
   }
 
@@ -247,8 +247,8 @@
   public void addGroups(String... groups) throws RestApiException {
     try {
       addGroups.apply(rsrc, AddIncludedGroups.Input.fromGroups(Arrays.asList(groups)));
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot add group members", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot add group members", e);
     }
   }
 
@@ -256,8 +256,8 @@
   public void removeGroups(String... groups) throws RestApiException {
     try {
       deleteGroups.apply(rsrc, AddIncludedGroups.Input.fromGroups(Arrays.asList(groups)));
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot remove group members", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot remove group members", e);
     }
   }
 
@@ -265,8 +265,8 @@
   public List<? extends GroupAuditEventInfo> auditLog() throws RestApiException {
     try {
       return getAuditLog.apply(rsrc);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot get audit log", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get audit log", e);
     }
   }
 
@@ -274,8 +274,8 @@
   public void index() throws RestApiException {
     try {
       index.apply(rsrc, new Index.Input());
-    } catch (IOException e) {
-      throw new RestApiException("Cannot index group", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot index group", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
index ecbde59..5f816bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.api.groups;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
 import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -33,13 +34,10 @@
 import com.google.gerrit.server.group.QueryGroups;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectsCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.IOException;
 import java.util.List;
 import java.util.SortedMap;
 
@@ -99,8 +97,8 @@
       permissionBackend.user(user).checkAny(GlobalPermission.fromAnnotation(impl.getClass()));
       GroupInfo info = impl.apply(TopLevelResource.INSTANCE, in);
       return id(info.id);
-    } catch (OrmException | IOException | PermissionBackendException e) {
-      throw new RestApiException("Cannot create group " + in.name, e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot create group " + in.name, e);
     }
   }
 
@@ -122,8 +120,8 @@
     for (String project : req.getProjects()) {
       try {
         list.addProject(projects.parse(tlr, IdString.fromDecoded(project)).getControl());
-      } catch (IOException | PermissionBackendException e) {
-        throw new RestApiException("Error looking up project " + project, e);
+      } catch (Exception e) {
+        throw asRestApiException("Error looking up project " + project, e);
       }
     }
 
@@ -136,8 +134,8 @@
     if (req.getUser() != null) {
       try {
         list.setUser(accounts.parse(req.getUser()).getAccountId());
-      } catch (OrmException e) {
-        throw new RestApiException("Error looking up user " + req.getUser(), e);
+      } catch (Exception e) {
+        throw asRestApiException("Error looking up user " + req.getUser(), e);
       }
     }
 
@@ -148,8 +146,8 @@
     list.setSuggest(req.getSuggest());
     try {
       return list.apply(tlr);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot list groups", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list groups", e);
     }
   }
 
@@ -178,8 +176,8 @@
         myQueryGroups.addOption(option);
       }
       return myQueryGroups.apply(TopLevelResource.INSTANCE);
-    } catch (OrmException e) {
-      throw new RestApiException("Cannot query groups", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot query groups", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
index 2fc7833..4a587a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.projects;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -28,7 +30,6 @@
 import com.google.gerrit.server.project.FilesCollection;
 import com.google.gerrit.server.project.GetContent;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -69,8 +70,8 @@
     try {
       createBranchFactory.create(ref).apply(project, input);
       return this;
-    } catch (IOException e) {
-      throw new RestApiException("Cannot create branch", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot create branch", e);
     }
   }
 
@@ -78,8 +79,8 @@
   public BranchInfo get() throws RestApiException {
     try {
       return resource().getBranchInfo();
-    } catch (IOException e) {
-      throw new RestApiException("Cannot read branch", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot read branch", e);
     }
   }
 
@@ -87,8 +88,8 @@
   public void delete() throws RestApiException {
     try {
       deleteBranch.apply(resource(), new DeleteBranch.Input());
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot delete branch", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete branch", e);
     }
   }
 
@@ -97,8 +98,8 @@
     try {
       FileResource resource = filesCollection.parse(resource(), IdString.fromDecoded(path));
       return getContent.apply(resource);
-    } catch (IOException e) {
-      throw new RestApiException("Cannot retrieve file", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve file", e);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/CommitApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/CommitApiImpl.java
index 9e17498..cbdd03d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/CommitApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/CommitApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.projects;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
@@ -21,11 +23,8 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.change.CherryPickCommit;
 import com.google.gerrit.server.project.CommitResource;
-import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 
 public class CommitApiImpl implements CommitApi {
   public interface Factory {
@@ -48,8 +47,8 @@
   public ChangeApi cherryPick(CherryPickInput input) throws RestApiException {
     try {
       return changes.id(cherryPickCommit.apply(commitResource, input)._number);
-    } catch (OrmException | IOException | UpdateException e) {
-      throw new RestApiException("Cannot cherry pick", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot cherry pick", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 1aa203c..104fb94 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.projects;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.api.projects.BranchApi;
@@ -39,7 +41,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChildProjectsCollection;
 import com.google.gerrit.server.project.CommitsCollection;
 import com.google.gerrit.server.project.CreateProject;
@@ -57,12 +58,9 @@
 import com.google.gerrit.server.project.PutConfig;
 import com.google.gerrit.server.project.PutDescription;
 import com.google.gerrit.server.project.SetAccess;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
-import java.io.IOException;
 import java.util.List;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 
 public class ProjectApiImpl implements ProjectApi {
   interface Factory {
@@ -269,8 +267,8 @@
       permissionBackend.user(user).checkAny(GlobalPermission.fromAnnotation(impl.getClass()));
       impl.apply(TopLevelResource.INSTANCE, in);
       return projectApi.create(projects.parse(name));
-    } catch (IOException | ConfigInvalidException | PermissionBackendException e) {
-      throw new RestApiException("Cannot create project: " + e.getMessage(), e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot create project: " + e.getMessage(), e);
     }
   }
 
@@ -291,8 +289,8 @@
   public ProjectAccessInfo access() throws RestApiException {
     try {
       return getAccess.apply(checkExists());
-    } catch (IOException e) {
-      throw new RestApiException("Cannot get access rights", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get access rights", e);
     }
   }
 
@@ -300,8 +298,8 @@
   public ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException {
     try {
       return setAccess.apply(checkExists(), p);
-    } catch (IOException | PermissionBackendException e) {
-      throw new RestApiException("Cannot put access rights", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot put access rights", e);
     }
   }
 
@@ -309,8 +307,8 @@
   public void description(DescriptionInput in) throws RestApiException {
     try {
       putDescription.apply(checkExists(), in);
-    } catch (IOException e) {
-      throw new RestApiException("Cannot put project description", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot put project description", e);
     }
   }
 
@@ -342,8 +340,8 @@
     listBranches.setMatchRegex(request.getRegex());
     try {
       return listBranches.apply(checkExists());
-    } catch (IOException e) {
-      throw new RestApiException("Cannot list branches", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list branches", e);
     }
   }
 
@@ -364,8 +362,8 @@
     listTags.setMatchRegex(request.getRegex());
     try {
       return listTags.apply(checkExists());
-    } catch (IOException e) {
-      throw new RestApiException("Cannot list tags", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list tags", e);
     }
   }
 
@@ -380,8 +378,8 @@
     list.setRecursive(recursive);
     try {
       return list.apply(checkExists());
-    } catch (PermissionBackendException e) {
-      throw new RestApiException("Cannot list children", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list children", e);
     }
   }
 
@@ -389,8 +387,8 @@
   public ChildProjectApi child(String name) throws RestApiException {
     try {
       return childApi.create(children.parse(checkExists(), IdString.fromDecoded(name)));
-    } catch (IOException | PermissionBackendException e) {
-      throw new RestApiException("Cannot parse child project", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot parse child project", e);
     }
   }
 
@@ -408,8 +406,8 @@
   public void deleteBranches(DeleteBranchesInput in) throws RestApiException {
     try {
       deleteBranches.apply(checkExists(), in);
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot delete branches", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete branches", e);
     }
   }
 
@@ -417,8 +415,8 @@
   public void deleteTags(DeleteTagsInput in) throws RestApiException {
     try {
       deleteTags.apply(checkExists(), in);
-    } catch (OrmException | IOException e) {
-      throw new RestApiException("Cannot delete tags", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete tags", e);
     }
   }
 
@@ -426,8 +424,8 @@
   public CommitApi commit(String commit) throws RestApiException {
     try {
       return commitApi.create(commitsCollection.parse(checkExists(), IdString.fromDecoded(commit)));
-    } catch (IOException e) {
-      throw new RestApiException("Cannot parse commit", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot parse commit", e);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
index dc19f71..702a7e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.projects;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.api.projects.Projects;
@@ -28,7 +30,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.IOException;
 import java.util.SortedMap;
 
 @Singleton
@@ -53,8 +54,8 @@
       return api.create(projects.parse(name));
     } catch (UnprocessableEntityException e) {
       return api.create(name);
-    } catch (IOException | PermissionBackendException e) {
-      throw new RestApiException("Cannot retrieve project", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve project", e);
     }
   }
 
@@ -80,8 +81,8 @@
       public SortedMap<String, ProjectInfo> getAsMap() throws RestApiException {
         try {
           return list(this);
-        } catch (PermissionBackendException e) {
-          throw new RestApiException("project list unavailable", e);
+        } catch (Exception e) {
+          throw asRestApiException("project list unavailable", e);
         }
       }
     };
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java
index 4e81407..283d117 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.projects;
 
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
 import com.google.gerrit.extensions.api.projects.TagApi;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.api.projects.TagInput;
@@ -25,7 +27,6 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.TagResource;
 import com.google.gerrit.server.project.TagsCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -63,8 +64,8 @@
     try {
       createTagFactory.create(ref).apply(project, input);
       return this;
-    } catch (IOException e) {
-      throw new RestApiException("Cannot create tag", e);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot create tag", e);
     }
   }
 
@@ -72,8 +73,8 @@
   public TagInfo get() throws RestApiException {
     try {
       return listTags.get(project, IdString.fromDecoded(ref));
-    } catch (IOException e) {
-      throw new RestApiException(e.getMessage());
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get tag", e);
     }
   }
 
@@ -81,8 +82,8 @@
   public void delete() throws RestApiException {
     try {
       deleteTag.apply(resource(), new DeleteTag.Input());
-    } catch (OrmException | IOException e) {
-      throw new RestApiException(e.getMessage());
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete tag", e);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index 95e1f2f..df22a3a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -35,6 +34,8 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -43,11 +44,10 @@
 import java.util.Collection;
 
 @Singleton
-public class Abandon
-    implements RestModifyView<ChangeResource, AbandonInput>, UiAction<ChangeResource> {
+public class Abandon extends RetryingRestModifyView<ChangeResource, AbandonInput, ChangeInfo>
+    implements UiAction<ChangeResource> {
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson.Factory json;
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final AbandonOp.Factory abandonOpFactory;
   private final NotifyUtil notifyUtil;
 
@@ -55,23 +55,25 @@
   Abandon(
       Provider<ReviewDb> dbProvider,
       ChangeJson.Factory json,
-      BatchUpdate.Factory batchUpdateFactory,
+      RetryHelper retryHelper,
       AbandonOp.Factory abandonOpFactory,
       NotifyUtil notifyUtil) {
+    super(retryHelper);
     this.dbProvider = dbProvider;
     this.json = json;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.abandonOpFactory = abandonOpFactory;
     this.notifyUtil = notifyUtil;
   }
 
   @Override
-  public ChangeInfo apply(ChangeResource req, AbandonInput input)
+  protected ChangeInfo applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource req, AbandonInput input)
       throws RestApiException, UpdateException, OrmException, PermissionBackendException {
     req.permissions().database(dbProvider).check(ChangePermission.ABANDON);
 
     Change change =
         abandon(
+            updateFactory,
             req.getControl(),
             input.message,
             input.notify,
@@ -79,16 +81,18 @@
     return json.noOptions().format(change);
   }
 
-  public Change abandon(ChangeControl control) throws RestApiException, UpdateException {
-    return abandon(control, "", NotifyHandling.ALL, ImmutableListMultimap.of());
+  public Change abandon(BatchUpdate.Factory updateFactory, ChangeControl control)
+      throws RestApiException, UpdateException {
+    return abandon(updateFactory, control, "", NotifyHandling.ALL, ImmutableListMultimap.of());
   }
 
-  public Change abandon(ChangeControl control, String msgTxt)
+  public Change abandon(BatchUpdate.Factory updateFactory, ChangeControl control, String msgTxt)
       throws RestApiException, UpdateException {
-    return abandon(control, msgTxt, NotifyHandling.ALL, ImmutableListMultimap.of());
+    return abandon(updateFactory, control, msgTxt, NotifyHandling.ALL, ImmutableListMultimap.of());
   }
 
   public Change abandon(
+      BatchUpdate.Factory updateFactory,
       ChangeControl control,
       String msgTxt,
       NotifyHandling notifyHandling,
@@ -98,7 +102,7 @@
     Account account = user.isIdentifiedUser() ? user.asIdentifiedUser().getAccount() : null;
     AbandonOp op = abandonOpFactory.create(account, msgTxt, notifyHandling, accountsToNotify);
     try (BatchUpdate u =
-        batchUpdateFactory.create(
+        updateFactory.create(
             dbProvider.get(),
             control.getProject().getNameKey(),
             control.getUser(),
@@ -116,6 +120,7 @@
    * matching project from its ChangeControl. Violations will result in a ResourceConflictException.
    */
   public void batchAbandon(
+      BatchUpdate.Factory updateFactory,
       Project.NameKey project,
       CurrentUser user,
       Collection<ChangeControl> controls,
@@ -127,8 +132,7 @@
       return;
     }
     Account account = user.isIdentifiedUser() ? user.asIdentifiedUser().getAccount() : null;
-    try (BatchUpdate u =
-        batchUpdateFactory.create(dbProvider.get(), project, user, TimeUtil.nowTs())) {
+    try (BatchUpdate u = updateFactory.create(dbProvider.get(), project, user, TimeUtil.nowTs())) {
       for (ChangeControl control : controls) {
         if (!project.equals(control.getProject().getNameKey())) {
           throw new ResourceConflictException(
@@ -145,15 +149,30 @@
   }
 
   public void batchAbandon(
-      Project.NameKey project, CurrentUser user, Collection<ChangeControl> controls, String msgTxt)
+      BatchUpdate.Factory updateFactory,
+      Project.NameKey project,
+      CurrentUser user,
+      Collection<ChangeControl> controls,
+      String msgTxt)
       throws RestApiException, UpdateException {
-    batchAbandon(project, user, controls, msgTxt, NotifyHandling.ALL, ImmutableListMultimap.of());
+    batchAbandon(
+        updateFactory,
+        project,
+        user,
+        controls,
+        msgTxt,
+        NotifyHandling.ALL,
+        ImmutableListMultimap.of());
   }
 
   public void batchAbandon(
-      Project.NameKey project, CurrentUser user, Collection<ChangeControl> controls)
+      BatchUpdate.Factory updateFactory,
+      Project.NameKey project,
+      CurrentUser user,
+      Collection<ChangeControl> controls)
       throws RestApiException, UpdateException {
-    batchAbandon(project, user, controls, "", NotifyHandling.ALL, ImmutableListMultimap.of());
+    batchAbandon(
+        updateFactory, project, user, controls, "", NotifyHandling.ALL, ImmutableListMultimap.of());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
index 7c408c8..7645685 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeQueryProcessor;
+import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -38,6 +39,7 @@
 public class AbandonUtil {
   private static final Logger log = LoggerFactory.getLogger(AbandonUtil.class);
 
+  private final BatchUpdate.Factory updateFactory;
   private final ChangeCleanupConfig cfg;
   private final ChangeQueryProcessor queryProcessor;
   private final ChangeQueryBuilder queryBuilder;
@@ -46,11 +48,13 @@
 
   @Inject
   AbandonUtil(
+      BatchUpdate.Factory updateFactory,
       ChangeCleanupConfig cfg,
       InternalUser.Factory internalUserFactory,
       ChangeQueryProcessor queryProcessor,
       ChangeQueryBuilder queryBuilder,
       Abandon abandon) {
+    this.updateFactory = updateFactory;
     this.cfg = cfg;
     this.queryProcessor = queryProcessor;
     this.queryBuilder = queryBuilder;
@@ -85,7 +89,7 @@
       for (Project.NameKey project : abandons.keySet()) {
         Collection<ChangeControl> changes = getValidChanges(abandons.get(project), query);
         try {
-          abandon.batchAbandon(project, internalUser, changes, message);
+          abandon.batchAbandon(updateFactory, project, internalUser, changes, message);
           count += changes.size();
         } catch (Throwable e) {
           StringBuilder msg = new StringBuilder("Failed to auto-abandon inactive change(s):");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
index 332c3c6..c1db891 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -397,8 +397,9 @@
                 base
                     ? ObjectId.fromString(edit.getBasePatchSet().getRevision().get())
                     : edit.getEditCommit(),
-                rsrc.getPath()));
-      } catch (ResourceNotFoundException rnfe) {
+                rsrc.getPath(),
+                null));
+      } catch (ResourceNotFoundException | BadRequestException e) {
         return Response.none();
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index e2d9eb1..5391635 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -121,12 +121,12 @@
   private Set<Account.Id> extraCC;
   private Map<String, Short> approvals;
   private RequestScopePropagator requestScopePropagator;
-  private ReceiveCommand updateRefCommand;
   private boolean fireRevisionCreated;
   private boolean sendMail;
   private boolean updateRef;
 
   // Fields set during the insertion process.
+  private ReceiveCommand cmd;
   private Change change;
   private ChangeMessage changeMessage;
   private PatchSetInfo patchSetInfo;
@@ -170,7 +170,6 @@
     this.reviewers = Collections.emptySet();
     this.extraCC = Collections.emptySet();
     this.approvals = Collections.emptyMap();
-    this.updateRefCommand = null;
     this.fireRevisionCreated = true;
     this.sendMail = true;
     this.updateRef = true;
@@ -310,10 +309,6 @@
     return this;
   }
 
-  public void setUpdateRefCommand(ReceiveCommand cmd) {
-    updateRefCommand = cmd;
-  }
-
   public void setPushCertificate(String cert) {
     pushCert = cert;
   }
@@ -328,6 +323,18 @@
     return this;
   }
 
+  /**
+   * Set whether to include the new patch set ref update in this update.
+   *
+   * <p>If false, the caller is responsible for creating the patch set ref <strong>before</strong>
+   * executing the containing {@code BatchUpdate}.
+   *
+   * <p>Should not be used in new code, as it doesn't result in a single atomic batch ref update for
+   * code and NoteDb meta refs.
+   *
+   * @param updateRef whether to update the ref during {@code updateRepo}.
+   */
+  @Deprecated
   public ChangeInserter setUpdateRef(boolean updateRef) {
     this.updateRef = updateRef;
     return this;
@@ -341,17 +348,18 @@
     return changeMessage;
   }
 
+  public ReceiveCommand getCommand() {
+    return cmd;
+  }
+
   @Override
   public void updateRepo(RepoContext ctx) throws ResourceConflictException, IOException {
+    cmd = new ReceiveCommand(ObjectId.zeroId(), commitId, psId.toRefName());
     validate(ctx);
     if (!updateRef) {
       return;
     }
-    if (updateRefCommand == null) {
-      ctx.addRefUpdate(ObjectId.zeroId(), commitId, psId.toRefName());
-    } else {
-      ctx.addRefUpdate(updateRefCommand);
-    }
+    ctx.addRefUpdate(cmd);
   }
 
   @Override
@@ -525,10 +533,9 @@
     try {
       RefControl refControl =
           projectControlFactory.controlFor(ctx.getProject(), ctx.getUser()).controlForRef(refName);
-      String refName = psId.toRefName();
       try (CommitReceivedEvent event =
           new CommitReceivedEvent(
-              new ReceiveCommand(ObjectId.zeroId(), commitId, refName),
+              cmd,
               refControl.getProjectControl().getProject(),
               change.getDest().get(),
               ctx.getRevWalk().getObjectReader(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index d197a2c..d690984 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -447,8 +447,8 @@
       info.updated = c.getLastUpdatedOn();
       info._number = c.getId().get();
       info.problems = result.problems();
-      info.isPrivate = c.isPrivate();
-      info.workInProgress = c.isWorkInProgress();
+      info.isPrivate = c.isPrivate() ? true : null;
+      info.workInProgress = c.isWorkInProgress() ? true : null;
       finish(info);
     } else {
       info = new ChangeInfo();
@@ -503,8 +503,8 @@
       out.insertions = changedLines.get().insertions;
       out.deletions = changedLines.get().deletions;
     }
-    out.isPrivate = in.isPrivate();
-    out.workInProgress = in.isWorkInProgress();
+    out.isPrivate = in.isPrivate() ? true : null;
+    out.workInProgress = in.isWorkInProgress() ? true : null;
     out.subject = in.getSubject();
     out.status = in.getStatus().asChangeStatus();
     out.owner = accountLoader.get(in.getOwner());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index 18d2fc1..18ed4de 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -32,6 +31,9 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -41,21 +43,27 @@
 
 @Singleton
 public class CherryPick
-    implements RestModifyView<RevisionResource, CherryPickInput>, UiAction<RevisionResource> {
+    extends RetryingRestModifyView<RevisionResource, CherryPickInput, ChangeInfo>
+    implements UiAction<RevisionResource> {
   private final Provider<ReviewDb> dbProvider;
   private final CherryPickChange cherryPickChange;
   private final ChangeJson.Factory json;
 
   @Inject
   CherryPick(
-      Provider<ReviewDb> dbProvider, CherryPickChange cherryPickChange, ChangeJson.Factory json) {
+      RetryHelper retryHelper,
+      Provider<ReviewDb> dbProvider,
+      CherryPickChange cherryPickChange,
+      ChangeJson.Factory json) {
+    super(retryHelper);
     this.dbProvider = dbProvider;
     this.cherryPickChange = cherryPickChange;
     this.json = json;
   }
 
   @Override
-  public ChangeInfo apply(RevisionResource revision, CherryPickInput input)
+  protected ChangeInfo applyImpl(
+      BatchUpdate.Factory updateFactory, RevisionResource revision, CherryPickInput input)
       throws OrmException, IOException, UpdateException, RestApiException {
     final ChangeControl control = revision.getControl();
     int parent = input.parent == null ? 1 : input.parent;
@@ -88,6 +96,7 @@
 
     try {
       Change.Id cherryPickedChangeId =
+          // TODO(dborowitz): Pass updateFactory here.
           cherryPickChange.cherryPick(
               revision.getChange(),
               revision.getPatchSet(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
index e0cb2e1..3c404d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Change;
@@ -61,6 +60,8 @@
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -86,7 +87,8 @@
 import org.eclipse.jgit.util.ChangeIdUtil;
 
 @Singleton
-public class CreateChange implements RestModifyView<TopLevelResource, ChangeInput> {
+public class CreateChange
+    extends RetryingRestModifyView<TopLevelResource, ChangeInput, Response<ChangeInfo>> {
 
   private final String anonymousCowardName;
   private final Provider<ReviewDb> db;
@@ -99,7 +101,6 @@
   private final ChangeInserter.Factory changeInserterFactory;
   private final ChangeJson.Factory jsonFactory;
   private final ChangeFinder changeFinder;
-  private final BatchUpdate.Factory updateFactory;
   private final PatchSetUtil psUtil;
   private final boolean allowDrafts;
   private final MergeUtil.Factory mergeUtilFactory;
@@ -119,11 +120,12 @@
       ChangeInserter.Factory changeInserterFactory,
       ChangeJson.Factory json,
       ChangeFinder changeFinder,
-      BatchUpdate.Factory updateFactory,
+      RetryHelper retryHelper,
       PatchSetUtil psUtil,
       @GerritServerConfig Config config,
       MergeUtil.Factory mergeUtilFactory,
       NotifyUtil notifyUtil) {
+    super(retryHelper);
     this.anonymousCowardName = anonymousCowardName;
     this.db = db;
     this.gitManager = gitManager;
@@ -135,7 +137,6 @@
     this.changeInserterFactory = changeInserterFactory;
     this.jsonFactory = json;
     this.changeFinder = changeFinder;
-    this.updateFactory = updateFactory;
     this.psUtil = psUtil;
     this.allowDrafts = config.getBoolean("change", "allowDrafts", true);
     this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
@@ -144,7 +145,8 @@
   }
 
   @Override
-  public Response<ChangeInfo> apply(TopLevelResource parent, ChangeInput input)
+  protected Response<ChangeInfo> applyImpl(
+      BatchUpdate.Factory updateFactory, TopLevelResource parent, ChangeInput input)
       throws OrmException, IOException, InvalidChangeOperationException, RestApiException,
           UpdateException, PermissionBackendException {
     if (Strings.isNullOrEmpty(input.project)) {
@@ -260,6 +262,8 @@
       }
       ins.setTopic(topic);
       ins.setDraft(input.status == ChangeStatus.DRAFT);
+      ins.setPrivate(input.isPrivate != null && input.isPrivate);
+      ins.setWorkInProgress(input.workInProgress != null && input.workInProgress);
       ins.setGroups(groups);
       ins.setNotify(input.notify);
       ins.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
index 6536550f..002c8b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Comment;
@@ -37,6 +36,8 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -45,9 +46,9 @@
 import java.util.Collections;
 
 @Singleton
-public class CreateDraftComment implements RestModifyView<RevisionResource, DraftInput> {
+public class CreateDraftComment
+    extends RetryingRestModifyView<RevisionResource, DraftInput, Response<CommentInfo>> {
   private final Provider<ReviewDb> db;
-  private final BatchUpdate.Factory updateFactory;
   private final Provider<CommentJson> commentJson;
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
@@ -56,13 +57,13 @@
   @Inject
   CreateDraftComment(
       Provider<ReviewDb> db,
-      BatchUpdate.Factory updateFactory,
+      RetryHelper retryHelper,
       Provider<CommentJson> commentJson,
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
       PatchListCache patchListCache) {
+    super(retryHelper);
     this.db = db;
-    this.updateFactory = updateFactory;
     this.commentJson = commentJson;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
@@ -70,7 +71,8 @@
   }
 
   @Override
-  public Response<CommentInfo> apply(RevisionResource rsrc, DraftInput in)
+  protected Response<CommentInfo> applyImpl(
+      BatchUpdate.Factory updateFactory, RevisionResource rsrc, DraftInput in)
       throws RestApiException, UpdateException, OrmException {
     if (Strings.isNullOrEmpty(in.path)) {
       throw new BadRequestException("path must be non-empty");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
index 42bfb8a..02c91ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -46,6 +45,8 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -65,7 +66,8 @@
 import org.eclipse.jgit.util.ChangeIdUtil;
 
 @Singleton
-public class CreateMergePatchSet implements RestModifyView<ChangeResource, MergePatchSetInput> {
+public class CreateMergePatchSet
+    extends RetryingRestModifyView<ChangeResource, MergePatchSetInput, Response<ChangeInfo>> {
 
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager gitManager;
@@ -74,7 +76,6 @@
   private final ChangeJson.Factory jsonFactory;
   private final PatchSetUtil psUtil;
   private final MergeUtil.Factory mergeUtilFactory;
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final PatchSetInserter.Factory patchSetInserterFactory;
 
   @Inject
@@ -86,8 +87,9 @@
       ChangeJson.Factory json,
       PatchSetUtil psUtil,
       MergeUtil.Factory mergeUtilFactory,
-      BatchUpdate.Factory batchUpdateFactory,
+      RetryHelper retryHelper,
       PatchSetInserter.Factory patchSetInserterFactory) {
+    super(retryHelper);
     this.db = db;
     this.gitManager = gitManager;
     this.serverTimeZone = myIdent.getTimeZone();
@@ -95,12 +97,12 @@
     this.jsonFactory = json;
     this.psUtil = psUtil;
     this.mergeUtilFactory = mergeUtilFactory;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.patchSetInserterFactory = patchSetInserterFactory;
   }
 
   @Override
-  public Response<ChangeInfo> apply(ChangeResource req, MergePatchSetInput in)
+  protected Response<ChangeInfo> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource req, MergePatchSetInput in)
       throws OrmException, IOException, InvalidChangeOperationException, RestApiException,
           UpdateException {
     if (in.merge == null) {
@@ -157,7 +159,7 @@
 
       PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.getId());
       PatchSetInserter psInserter = patchSetInserterFactory.create(ctl, nextPsId, newCommit);
-      try (BatchUpdate bu = batchUpdateFactory.create(db.get(), project, me, now)) {
+      try (BatchUpdate bu = updateFactory.create(db.get(), project, me, now)) {
         bu.setRepository(git, rw, oi);
         bu.addOp(
             ctl.getId(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java
index 1d5a916..d3feb31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -35,6 +34,8 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -42,10 +43,10 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
+public class DeleteAssignee
+    extends RetryingRestModifyView<ChangeResource, Input, Response<AccountInfo>> {
   public static class Input {}
 
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final ChangeMessagesUtil cmUtil;
   private final Provider<ReviewDb> db;
   private final AssigneeChanged assigneeChanged;
@@ -54,13 +55,13 @@
 
   @Inject
   DeleteAssignee(
-      BatchUpdate.Factory batchUpdateFactory,
+      RetryHelper retryHelper,
       ChangeMessagesUtil cmUtil,
       Provider<ReviewDb> db,
       AssigneeChanged assigneeChanged,
       IdentifiedUser.GenericFactory userFactory,
       AccountLoader.Factory accountLoaderFactory) {
-    this.batchUpdateFactory = batchUpdateFactory;
+    super(retryHelper);
     this.cmUtil = cmUtil;
     this.db = db;
     this.assigneeChanged = assigneeChanged;
@@ -69,12 +70,13 @@
   }
 
   @Override
-  public Response<AccountInfo> apply(ChangeResource rsrc, Input input)
+  protected Response<AccountInfo> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, OrmException, PermissionBackendException {
     rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
 
     try (BatchUpdate bu =
-        batchUpdateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       Op op = new Op();
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java
index 151ffa1..b9b05e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -32,6 +31,8 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.Order;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -39,12 +40,11 @@
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
-public class DeleteChange
-    implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
+public class DeleteChange extends RetryingRestModifyView<ChangeResource, Input, Response<?>>
+    implements UiAction<ChangeResource> {
   public static class Input {}
 
   private final Provider<ReviewDb> db;
-  private final BatchUpdate.Factory updateFactory;
   private final Provider<DeleteChangeOp> opProvider;
   private final Provider<CurrentUser> user;
   private final PermissionBackend permissionBackend;
@@ -53,13 +53,13 @@
   @Inject
   public DeleteChange(
       Provider<ReviewDb> db,
-      BatchUpdate.Factory updateFactory,
+      RetryHelper retryHelper,
       Provider<DeleteChangeOp> opProvider,
       Provider<CurrentUser> user,
       PermissionBackend permissionBackend,
       @GerritServerConfig Config cfg) {
+    super(retryHelper);
     this.db = db;
-    this.updateFactory = updateFactory;
     this.opProvider = opProvider;
     this.user = user;
     this.permissionBackend = permissionBackend;
@@ -67,7 +67,8 @@
   }
 
   @Override
-  public Response<?> apply(ChangeResource rsrc, Input input)
+  protected Response<?> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
     if (rsrc.getChange().getStatus() == Change.Status.MERGED) {
       throw new MethodNotAllowedException("delete not permitted");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
index d1b26ec..021fd45 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -32,6 +31,8 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -41,13 +42,13 @@
 import java.util.Optional;
 
 @Singleton
-public class DeleteDraftComment implements RestModifyView<DraftCommentResource, Input> {
+public class DeleteDraftComment
+    extends RetryingRestModifyView<DraftCommentResource, Input, Response<CommentInfo>> {
   static class Input {}
 
   private final Provider<ReviewDb> db;
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
-  private final BatchUpdate.Factory updateFactory;
   private final PatchListCache patchListCache;
 
   @Inject
@@ -55,17 +56,18 @@
       Provider<ReviewDb> db,
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
-      BatchUpdate.Factory updateFactory,
+      RetryHelper retryHelper,
       PatchListCache patchListCache) {
+    super(retryHelper);
     this.db = db;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
-    this.updateFactory = updateFactory;
     this.patchListCache = patchListCache;
   }
 
   @Override
-  public Response<CommentInfo> apply(DraftCommentResource rsrc, Input input)
+  protected Response<CommentInfo> applyImpl(
+      BatchUpdate.Factory updateFactory, DraftCommentResource rsrc, Input input)
       throws RestApiException, UpdateException {
     try (BatchUpdate bu =
         updateFactory.create(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
index 583bc58..615c32b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -22,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -41,6 +42,8 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Order;
 import com.google.gerrit.server.update.RepoContext;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -48,16 +51,17 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Collection;
+import java.util.Map;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 
 @Singleton
 public class DeleteDraftPatchSet
-    implements RestModifyView<RevisionResource, Input>, UiAction<RevisionResource> {
+    extends RetryingRestModifyView<RevisionResource, Input, Response<?>>
+    implements UiAction<RevisionResource> {
   public static class Input {}
 
   private final Provider<ReviewDb> db;
-  private final BatchUpdate.Factory updateFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final PatchSetUtil psUtil;
   private final Provider<DeleteChangeOp> deleteChangeOpProvider;
@@ -67,14 +71,14 @@
   @Inject
   public DeleteDraftPatchSet(
       Provider<ReviewDb> db,
-      BatchUpdate.Factory updateFactory,
+      RetryHelper retryHelper,
       PatchSetInfoFactory patchSetInfoFactory,
       PatchSetUtil psUtil,
       Provider<DeleteChangeOp> deleteChangeOpProvider,
       DynamicItem<AccountPatchReviewStore> accountPatchReviewStore,
       @GerritServerConfig Config cfg) {
+    super(retryHelper);
     this.db = db;
-    this.updateFactory = updateFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.psUtil = psUtil;
     this.deleteChangeOpProvider = deleteChangeOpProvider;
@@ -83,7 +87,8 @@
   }
 
   @Override
-  public Response<?> apply(RevisionResource rsrc, Input input)
+  protected Response<?> applyImpl(
+      BatchUpdate.Factory updateFactory, RevisionResource rsrc, Input input)
       throws RestApiException, UpdateException, OrmException, PermissionBackendException {
     if (isDeletingOnlyPatchSet(rsrc)) {
       // A change cannot have zero patch sets; the change is deleted instead.
@@ -119,7 +124,8 @@
     @Override
     public boolean updateChange(ChangeContext ctx)
         throws RestApiException, OrmException, IOException, NoSuchChangeException {
-      patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+      Map<PatchSet.Id, PatchSet> patchSets = psUtil.byChangeAsMap(db.get(), ctx.getNotes());
+      patchSet = patchSets.get(psId);
       if (patchSet == null) {
         return false; // Nothing to do.
       }
@@ -133,9 +139,9 @@
         throw new AuthException("Not permitted to delete this draft patch set");
       }
 
-      patchSetsBeforeDeletion = psUtil.byChange(ctx.getDb(), ctx.getNotes());
+      patchSetsBeforeDeletion = patchSets.values();
       deleteDraftPatchSet(patchSet, ctx);
-      deleteOrUpdateDraftChange(ctx);
+      deleteOrUpdateDraftChange(ctx, patchSets);
       return true;
     }
 
@@ -164,7 +170,7 @@
       db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(psId));
     }
 
-    private void deleteOrUpdateDraftChange(ChangeContext ctx)
+    private void deleteOrUpdateDraftChange(ChangeContext ctx, Map<PatchSet.Id, PatchSet> patchSets)
         throws OrmException, RestApiException, IOException, NoSuchChangeException {
       Change c = ctx.getChange();
       if (deletedOnlyPatchSet()) {
@@ -173,7 +179,7 @@
         return;
       }
       if (c.currentPatchSetId().equals(psId)) {
-        c.setCurrentPatchSet(previousPatchSetInfo(ctx));
+        c.setCurrentPatchSet(previousPatchSetInfo(ctx, patchSets));
       }
     }
 
@@ -182,12 +188,22 @@
           && patchSetsBeforeDeletion.iterator().next().getId().equals(psId);
     }
 
-    private PatchSetInfo previousPatchSetInfo(ChangeContext ctx) throws OrmException {
+    private PatchSetInfo previousPatchSetInfo(
+        ChangeContext ctx, Map<PatchSet.Id, PatchSet> patchSets) throws OrmException {
+      PatchSet.Id prevPsId = null;
+      for (PatchSet.Id id : patchSets.keySet()) {
+        if (id.get() < psId.get() && (prevPsId == null || id.get() > prevPsId.get())) {
+          prevPsId = id;
+        }
+      }
+
       try {
         // TODO(dborowitz): Get this in a way that doesn't involve re-opening
         // the repo after the updateRepo phase.
         return patchSetInfoFactory.get(
-            ctx.getDb(), ctx.getNotes(), new PatchSet.Id(psId.getParentKey(), psId.get() - 1));
+            ctx.getDb(),
+            ctx.getNotes(),
+            new PatchSet.Id(psId.getParentKey(), checkNotNull(prevPsId).get()));
       } catch (PatchSetInfoNotAvailableException e) {
         throw new OrmException(e);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
index fc1c3f1..a951d66 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
@@ -19,12 +19,13 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -32,25 +33,23 @@
 
 @Singleton
 public class DeletePrivate
-    implements RestModifyView<ChangeResource, DeletePrivate.Input>, UiAction<ChangeResource> {
+    extends RetryingRestModifyView<ChangeResource, DeletePrivate.Input, Response<String>>
+    implements UiAction<ChangeResource> {
   public static class Input {}
 
   private final ChangeMessagesUtil cmUtil;
   private final Provider<ReviewDb> dbProvider;
-  private final BatchUpdate.Factory batchUpdateFactory;
 
   @Inject
-  DeletePrivate(
-      Provider<ReviewDb> dbProvider,
-      BatchUpdate.Factory batchUpdateFactory,
-      ChangeMessagesUtil cmUtil) {
+  DeletePrivate(Provider<ReviewDb> dbProvider, RetryHelper retryHelper, ChangeMessagesUtil cmUtil) {
+    super(retryHelper);
     this.dbProvider = dbProvider;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.cmUtil = cmUtil;
   }
 
   @Override
-  public Response<String> apply(ChangeResource rsrc, DeletePrivate.Input input)
+  protected Response<String> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, DeletePrivate.Input input)
       throws RestApiException, UpdateException {
     if (!rsrc.isUserOwner()) {
       throw new AuthException("not allowed to unmark private");
@@ -63,7 +62,7 @@
     ChangeControl control = rsrc.getControl();
     SetPrivateOp op = new SetPrivateOp(cmUtil, false);
     try (BatchUpdate u =
-        batchUpdateFactory.create(
+        updateFactory.create(
             dbProvider.get(),
             control.getProject().getNameKey(),
             control.getUser(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
index 4822478..6bee46d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -19,37 +19,39 @@
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
-public class DeleteReviewer implements RestModifyView<ReviewerResource, DeleteReviewerInput> {
+public class DeleteReviewer
+    extends RetryingRestModifyView<ReviewerResource, DeleteReviewerInput, Response<?>> {
 
   private final Provider<ReviewDb> dbProvider;
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final DeleteReviewerOp.Factory deleteReviewerOpFactory;
   private final DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory;
 
   @Inject
   DeleteReviewer(
       Provider<ReviewDb> dbProvider,
-      BatchUpdate.Factory batchUpdateFactory,
+      RetryHelper retryHelper,
       DeleteReviewerOp.Factory deleteReviewerOpFactory,
       DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory) {
+    super(retryHelper);
     this.dbProvider = dbProvider;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.deleteReviewerOpFactory = deleteReviewerOpFactory;
     this.deleteReviewerByEmailOpFactory = deleteReviewerByEmailOpFactory;
   }
 
   @Override
-  public Response<?> apply(ReviewerResource rsrc, DeleteReviewerInput input)
+  protected Response<?> applyImpl(
+      BatchUpdate.Factory updateFactory, ReviewerResource rsrc, DeleteReviewerInput input)
       throws RestApiException, UpdateException {
     if (input == null) {
       input = new DeleteReviewerInput();
@@ -59,7 +61,7 @@
     }
 
     try (BatchUpdate bu =
-        batchUpdateFactory.create(
+        updateFactory.create(
             dbProvider.get(),
             rsrc.getChangeResource().getProject(),
             rsrc.getChangeResource().getUser(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
index 963e7b4..6dd1e2c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -46,6 +45,8 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gwtorm.server.OrmException;
@@ -59,11 +60,10 @@
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class DeleteVote implements RestModifyView<VoteResource, DeleteVoteInput> {
+public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteInput, Response<?>> {
   private static final Logger log = LoggerFactory.getLogger(DeleteVote.class);
 
   private final Provider<ReviewDb> db;
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final ApprovalsUtil approvalsUtil;
   private final PatchSetUtil psUtil;
   private final ChangeMessagesUtil cmUtil;
@@ -75,7 +75,7 @@
   @Inject
   DeleteVote(
       Provider<ReviewDb> db,
-      BatchUpdate.Factory batchUpdateFactory,
+      RetryHelper retryHelper,
       ApprovalsUtil approvalsUtil,
       PatchSetUtil psUtil,
       ChangeMessagesUtil cmUtil,
@@ -83,8 +83,8 @@
       VoteDeleted voteDeleted,
       DeleteVoteSender.Factory deleteVoteSenderFactory,
       NotifyUtil notifyUtil) {
+    super(retryHelper);
     this.db = db;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.approvalsUtil = approvalsUtil;
     this.psUtil = psUtil;
     this.cmUtil = cmUtil;
@@ -95,7 +95,8 @@
   }
 
   @Override
-  public Response<?> apply(VoteResource rsrc, DeleteVoteInput input)
+  protected Response<?> applyImpl(
+      BatchUpdate.Factory updateFactory, VoteResource rsrc, DeleteVoteInput input)
       throws RestApiException, UpdateException {
     if (input == null) {
       input = new DeleteVoteInput();
@@ -114,7 +115,7 @@
     }
 
     try (BatchUpdate bu =
-        batchUpdateFactory.create(
+        updateFactory.create(
             db.get(), change.getProject(), r.getControl().getUser(), TimeUtil.nowTs())) {
       bu.addOp(change.getId(), new Op(r.getReviewerUser().getAccountId(), rsrc.getLabel(), input));
       bu.execute();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
index 166197e..e812002 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.PatchScript.FileMode;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -67,9 +68,33 @@
     this.registry = ftr;
   }
 
-  public BinaryResult getContent(ProjectState project, ObjectId revstr, String path)
-      throws ResourceNotFoundException, IOException {
-    try (Repository repo = openRepository(project)) {
+  /**
+   * Get the content of a file at a specific commit or one of it's parent commits.
+   *
+   * @param project A {@code Project} that this request refers to.
+   * @param revstr An {@code ObjectId} specifying the commit.
+   * @param path A string specifying the filepath.
+   * @param parent A 1-based parent index to get the content from instead. Null if the content
+   *     should be obtained from {@param revstr} instead.
+   * @return Content of the file as {@code BinaryResult}.
+   * @throws ResourceNotFoundException
+   * @throws IOException
+   */
+  public BinaryResult getContent(
+      ProjectState project, ObjectId revstr, String path, @Nullable Integer parent)
+      throws BadRequestException, ResourceNotFoundException, IOException {
+    try (Repository repo = openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      if (parent != null) {
+        RevCommit revCommit = rw.parseCommit(revstr);
+        if (revCommit == null) {
+          throw new ResourceNotFoundException("commit not found");
+        }
+        if (parent > revCommit.getParentCount()) {
+          throw new BadRequestException("invalid parent");
+        }
+        revstr = rw.parseCommit(revstr).getParent(Integer.max(0, parent - 1)).toObjectId();
+      }
       return getContent(repo, project, revstr, path);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
index abb9e66..5433653 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -30,21 +31,23 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
 
-@Singleton
 public class GetContent implements RestReadView<FileResource> {
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager gitManager;
   private final PatchSetUtil psUtil;
   private final FileContentUtil fileContentUtil;
 
+  @Option(name = "--parent")
+  private Integer parent;
+
   @Inject
   GetContent(
       Provider<ReviewDb> db,
@@ -59,7 +62,7 @@
 
   @Override
   public BinaryResult apply(FileResource rsrc)
-      throws ResourceNotFoundException, IOException, NoSuchChangeException, OrmException {
+      throws ResourceNotFoundException, IOException, BadRequestException, OrmException {
     String path = rsrc.getPatchKey().get();
     if (Patch.COMMIT_MSG.equals(path)) {
       String msg = getMessage(rsrc.getRevision().getChangeResource().getNotes());
@@ -75,7 +78,8 @@
     return fileContentUtil.getContent(
         rsrc.getRevision().getControl().getProjectControl().getProjectState(),
         ObjectId.fromString(rsrc.getRevision().getPatchSet().getRevision().get()),
-        path);
+        path,
+        parent);
   }
 
   private String getMessage(ChangeNotes notes) throws OrmException, IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
index 13a94d3..7c4d158 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.Index.Input;
@@ -24,6 +23,9 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -31,7 +33,7 @@
 import java.io.IOException;
 
 @Singleton
-public class Index implements RestModifyView<ChangeResource, Input> {
+public class Index extends RetryingRestModifyView<ChangeResource, Input, Response<?>> {
   public static class Input {}
 
   private final Provider<ReviewDb> db;
@@ -42,9 +44,11 @@
   @Inject
   Index(
       Provider<ReviewDb> db,
+      RetryHelper retryHelper,
       PermissionBackend permissionBackend,
       Provider<CurrentUser> user,
       ChangeIndexer indexer) {
+    super(retryHelper);
     this.db = db;
     this.permissionBackend = permissionBackend;
     this.user = user;
@@ -52,7 +56,8 @@
   }
 
   @Override
-  public Response<?> apply(ChangeResource rsrc, Input input)
+  protected Response<?> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws IOException, AuthException, OrmException, PermissionBackendException {
     permissionBackend.user(user).check(GlobalPermission.MAINTAIN_SERVER);
     indexer.index(db.get(), rsrc.getChange());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
index ec494ce..98b79d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
@@ -42,6 +41,8 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -55,13 +56,12 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 @Singleton
-public class Move implements RestModifyView<ChangeResource, MoveInput> {
+public class Move extends RetryingRestModifyView<ChangeResource, MoveInput, ChangeInfo> {
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson.Factory json;
   private final GitRepositoryManager repoManager;
   private final Provider<InternalChangeQuery> queryProvider;
   private final ChangeMessagesUtil cmUtil;
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final PatchSetUtil psUtil;
 
   @Inject
@@ -71,19 +71,20 @@
       GitRepositoryManager repoManager,
       Provider<InternalChangeQuery> queryProvider,
       ChangeMessagesUtil cmUtil,
-      BatchUpdate.Factory batchUpdateFactory,
+      RetryHelper retryHelper,
       PatchSetUtil psUtil) {
+    super(retryHelper);
     this.dbProvider = dbProvider;
     this.json = json;
     this.repoManager = repoManager;
     this.queryProvider = queryProvider;
     this.cmUtil = cmUtil;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.psUtil = psUtil;
   }
 
   @Override
-  public ChangeInfo apply(ChangeResource req, MoveInput input)
+  protected ChangeInfo applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource req, MoveInput input)
       throws RestApiException, OrmException, UpdateException {
     ChangeControl control = req.getControl();
     input.destinationBranch = RefNames.fullName(input.destinationBranch);
@@ -92,7 +93,7 @@
     }
 
     try (BatchUpdate u =
-        batchUpdateFactory.create(
+        updateFactory.create(
             dbProvider.get(), req.getChange().getProject(), control.getUser(), TimeUtil.nowTs())) {
       u.addOp(req.getChange().getId(), new Op(input));
       u.execute();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
index ebe8f7e..0c8f010 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
@@ -19,12 +19,13 @@
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -32,28 +33,28 @@
 
 @Singleton
 public class PostHashtags
-    implements RestModifyView<ChangeResource, HashtagsInput>, UiAction<ChangeResource> {
+    extends RetryingRestModifyView<
+        ChangeResource, HashtagsInput, Response<ImmutableSortedSet<String>>>
+    implements UiAction<ChangeResource> {
   private final Provider<ReviewDb> db;
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final SetHashtagsOp.Factory hashtagsFactory;
 
   @Inject
   PostHashtags(
-      Provider<ReviewDb> db,
-      BatchUpdate.Factory batchUpdateFactory,
-      SetHashtagsOp.Factory hashtagsFactory) {
+      Provider<ReviewDb> db, RetryHelper retryHelper, SetHashtagsOp.Factory hashtagsFactory) {
+    super(retryHelper);
     this.db = db;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.hashtagsFactory = hashtagsFactory;
   }
 
   @Override
-  public Response<ImmutableSortedSet<String>> apply(ChangeResource req, HashtagsInput input)
+  protected Response<ImmutableSortedSet<String>> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource req, HashtagsInput input)
       throws RestApiException, UpdateException, PermissionBackendException {
     req.permissions().check(ChangePermission.EDIT_HASHTAGS);
 
     try (BatchUpdate bu =
-        batchUpdateFactory.create(
+        updateFactory.create(
             db.get(), req.getChange().getProject(), req.getControl().getUser(), TimeUtil.nowTs())) {
       SetHashtagsOp op = hashtagsFactory.create(input);
       bu.addOp(req.getId(), op);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 69aa19d..0f229f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -61,7 +61,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Account;
@@ -101,10 +100,11 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.BatchUpdate.Factory;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gson.Gson;
@@ -131,13 +131,13 @@
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class PostReview implements RestModifyView<RevisionResource, ReviewInput> {
+public class PostReview
+    extends RetryingRestModifyView<RevisionResource, ReviewInput, Response<ReviewResult>> {
   private static final Logger log = LoggerFactory.getLogger(PostReview.class);
   private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
   private static final int DEFAULT_ROBOT_COMMENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024;
 
   private final Provider<ReviewDb> db;
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final ChangesCollection changes;
   private final ChangeData.Factory changeDataFactory;
   private final ApprovalsUtil approvalsUtil;
@@ -156,7 +156,7 @@
   @Inject
   PostReview(
       Provider<ReviewDb> db,
-      Factory batchUpdateFactory,
+      RetryHelper retryHelper,
       ChangesCollection changes,
       ChangeData.Factory changeDataFactory,
       ApprovalsUtil approvalsUtil,
@@ -171,8 +171,8 @@
       NotesMigration migration,
       NotifyUtil notifyUtil,
       @GerritServerConfig Config gerritConfig) {
+    super(retryHelper);
     this.db = db;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.changes = changes;
     this.changeDataFactory = changeDataFactory;
     this.commentsUtil = commentsUtil;
@@ -190,13 +190,15 @@
   }
 
   @Override
-  public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input)
+  protected Response<ReviewResult> applyImpl(
+      BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input)
       throws RestApiException, UpdateException, OrmException, IOException,
           PermissionBackendException {
-    return apply(revision, input, TimeUtil.nowTs());
+    return apply(updateFactory, revision, input, TimeUtil.nowTs());
   }
 
-  public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input, Timestamp ts)
+  public Response<ReviewResult> apply(
+      BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input, Timestamp ts)
       throws RestApiException, UpdateException, OrmException, IOException,
           PermissionBackendException {
     // Respect timestamp, but truncate at change created-on time.
@@ -264,8 +266,7 @@
     output.labels = input.labels;
 
     try (BatchUpdate bu =
-        batchUpdateFactory.create(
-            db.get(), revision.getChange().getProject(), revision.getUser(), ts)) {
+        updateFactory.create(db.get(), revision.getChange().getProject(), revision.getUser(), ts)) {
       Account.Id id = revision.getUser().getAccountId();
       boolean ccOrReviewer = false;
       if (input.labels != null && !input.labels.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index 13d5271..c7b0031 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -37,7 +37,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -65,6 +64,8 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -78,7 +79,8 @@
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
-public class PostReviewers implements RestModifyView<ChangeResource, AddReviewerInput> {
+public class PostReviewers
+    extends RetryingRestModifyView<ChangeResource, AddReviewerInput, AddReviewerResult> {
 
   public static final int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
   public static final int DEFAULT_MAX_REVIEWERS = 20;
@@ -92,7 +94,6 @@
   private final AccountLoader.Factory accountLoaderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeData.Factory changeDataFactory;
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final Config cfg;
   private final ReviewerJson json;
@@ -113,7 +114,7 @@
       AccountLoader.Factory accountLoaderFactory,
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
-      BatchUpdate.Factory batchUpdateFactory,
+      RetryHelper retryHelper,
       IdentifiedUser.GenericFactory identifiedUserFactory,
       @GerritServerConfig Config cfg,
       ReviewerJson json,
@@ -123,6 +124,7 @@
       Provider<AnonymousUser> anonymousProvider,
       PostReviewersOp.Factory postReviewersOpFactory,
       OutgoingEmailValidator validator) {
+    super(retryHelper);
     this.accounts = accounts;
     this.reviewerFactory = reviewerFactory;
     this.permissionBackend = permissionBackend;
@@ -131,7 +133,6 @@
     this.accountLoaderFactory = accountLoaderFactory;
     this.dbProvider = db;
     this.changeDataFactory = changeDataFactory;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.identifiedUserFactory = identifiedUserFactory;
     this.cfg = cfg;
     this.json = json;
@@ -144,7 +145,8 @@
   }
 
   @Override
-  public AddReviewerResult apply(ChangeResource rsrc, AddReviewerInput input)
+  protected AddReviewerResult applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, AddReviewerInput input)
       throws IOException, OrmException, RestApiException, UpdateException,
           PermissionBackendException {
     if (input.reviewer == null) {
@@ -156,7 +158,7 @@
       return addition.result;
     }
     try (BatchUpdate bu =
-        batchUpdateFactory.create(
+        updateFactory.create(
             dbProvider.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       Change.Id id = rsrc.getChange().getId();
       bu.addOp(id, addition.op);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index 4cbeaf63c..3a614a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -49,6 +48,8 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -65,14 +66,14 @@
 
 @Singleton
 public class PublishDraftPatchSet
-    implements RestModifyView<RevisionResource, Input>, UiAction<RevisionResource> {
+    extends RetryingRestModifyView<RevisionResource, Input, Response<?>>
+    implements UiAction<RevisionResource> {
   private static final Logger log = LoggerFactory.getLogger(PublishDraftPatchSet.class);
 
   public static class Input {}
 
   private final AccountResolver accountResolver;
   private final ApprovalsUtil approvalsUtil;
-  private final BatchUpdate.Factory updateFactory;
   private final CreateChangeSender.Factory createChangeSenderFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final PatchSetUtil psUtil;
@@ -84,16 +85,16 @@
   public PublishDraftPatchSet(
       AccountResolver accountResolver,
       ApprovalsUtil approvalsUtil,
-      BatchUpdate.Factory updateFactory,
+      RetryHelper retryHelper,
       CreateChangeSender.Factory createChangeSenderFactory,
       PatchSetInfoFactory patchSetInfoFactory,
       PatchSetUtil psUtil,
       Provider<ReviewDb> dbProvider,
       ReplacePatchSetSender.Factory replacePatchSetFactory,
       DraftPublished draftPublished) {
+    super(retryHelper);
     this.accountResolver = accountResolver;
     this.approvalsUtil = approvalsUtil;
-    this.updateFactory = updateFactory;
     this.createChangeSenderFactory = createChangeSenderFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.psUtil = psUtil;
@@ -103,12 +104,19 @@
   }
 
   @Override
-  public Response<?> apply(RevisionResource rsrc, Input input)
+  protected Response<?> applyImpl(
+      BatchUpdate.Factory updateFactory, RevisionResource rsrc, Input input)
       throws RestApiException, UpdateException {
-    return apply(rsrc.getUser(), rsrc.getChange(), rsrc.getPatchSet().getId(), rsrc.getPatchSet());
+    return apply(
+        updateFactory,
+        rsrc.getUser(),
+        rsrc.getChange(),
+        rsrc.getPatchSet().getId(),
+        rsrc.getPatchSet());
   }
 
-  private Response<?> apply(CurrentUser u, Change c, PatchSet.Id psId, PatchSet ps)
+  private Response<?> apply(
+      BatchUpdate.Factory updateFactory, CurrentUser u, Change c, PatchSet.Id psId, PatchSet ps)
       throws RestApiException, UpdateException {
     try (BatchUpdate bu =
         updateFactory.create(dbProvider.get(), c.getProject(), u, TimeUtil.nowTs())) {
@@ -131,18 +139,22 @@
     }
   }
 
-  public static class CurrentRevision implements RestModifyView<ChangeResource, Input> {
+  public static class CurrentRevision
+      extends RetryingRestModifyView<ChangeResource, Input, Response<?>> {
     private final PublishDraftPatchSet publish;
 
     @Inject
-    CurrentRevision(PublishDraftPatchSet publish) {
+    CurrentRevision(RetryHelper retryHelper, PublishDraftPatchSet publish) {
+      super(retryHelper);
       this.publish = publish;
     }
 
     @Override
-    public Response<?> apply(ChangeResource rsrc, Input input)
+    protected Response<?> applyImpl(
+        BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
         throws RestApiException, UpdateException {
       return publish.apply(
+          updateFactory,
           rsrc.getControl().getUser(),
           rsrc.getChange(),
           rsrc.getChange().currentPatchSetId(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
index 735107f..b07d24b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -35,6 +34,8 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -43,12 +44,11 @@
 import java.io.IOException;
 
 @Singleton
-public class PutAssignee
-    implements RestModifyView<ChangeResource, AssigneeInput>, UiAction<ChangeResource> {
+public class PutAssignee extends RetryingRestModifyView<ChangeResource, AssigneeInput, AccountInfo>
+    implements UiAction<ChangeResource> {
 
   private final AccountsCollection accounts;
   private final SetAssigneeOp.Factory assigneeFactory;
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final Provider<ReviewDb> db;
   private final PostReviewers postReviewers;
   private final AccountLoader.Factory accountLoaderFactory;
@@ -57,20 +57,21 @@
   PutAssignee(
       AccountsCollection accounts,
       SetAssigneeOp.Factory assigneeFactory,
-      BatchUpdate.Factory batchUpdateFactory,
+      RetryHelper retryHelper,
       Provider<ReviewDb> db,
       PostReviewers postReviewers,
       AccountLoader.Factory accountLoaderFactory) {
+    super(retryHelper);
     this.accounts = accounts;
     this.assigneeFactory = assigneeFactory;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.db = db;
     this.postReviewers = postReviewers;
     this.accountLoaderFactory = accountLoaderFactory;
   }
 
   @Override
-  public AccountInfo apply(ChangeResource rsrc, AssigneeInput input)
+  protected AccountInfo applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, AssigneeInput input)
       throws RestApiException, UpdateException, OrmException, IOException,
           PermissionBackendException {
     rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
@@ -91,7 +92,7 @@
     }
 
     try (BatchUpdate bu =
-        batchUpdateFactory.create(
+        updateFactory.create(
             db.get(),
             rsrc.getChange().getProject(),
             rsrc.getControl().getUser(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java
index e872206..f2614c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -33,6 +32,8 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -42,10 +43,10 @@
 
 @Singleton
 public class PutDescription
-    implements RestModifyView<RevisionResource, PutDescription.Input>, UiAction<RevisionResource> {
+    extends RetryingRestModifyView<RevisionResource, PutDescription.Input, Response<String>>
+    implements UiAction<RevisionResource> {
   private final Provider<ReviewDb> dbProvider;
   private final ChangeMessagesUtil cmUtil;
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final PatchSetUtil psUtil;
 
   public static class Input {
@@ -56,23 +57,24 @@
   PutDescription(
       Provider<ReviewDb> dbProvider,
       ChangeMessagesUtil cmUtil,
-      BatchUpdate.Factory batchUpdateFactory,
+      RetryHelper retryHelper,
       PatchSetUtil psUtil) {
+    super(retryHelper);
     this.dbProvider = dbProvider;
     this.cmUtil = cmUtil;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.psUtil = psUtil;
   }
 
   @Override
-  public Response<String> apply(RevisionResource rsrc, Input input)
+  protected Response<String> applyImpl(
+      BatchUpdate.Factory updateFactory, RevisionResource rsrc, Input input)
       throws UpdateException, RestApiException, PermissionBackendException {
     rsrc.permissions().check(ChangePermission.EDIT_DESCRIPTION);
 
     ChangeControl ctl = rsrc.getControl();
     Op op = new Op(input != null ? input : new Input(), rsrc.getPatchSet().getId());
     try (BatchUpdate u =
-        batchUpdateFactory.create(
+        updateFactory.create(
             dbProvider.get(), rsrc.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
       u.addOp(rsrc.getChange().getId(), op);
       u.execute();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
index ecdb382..90716ce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
@@ -36,6 +35,8 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -46,13 +47,13 @@
 import java.util.Optional;
 
 @Singleton
-public class PutDraftComment implements RestModifyView<DraftCommentResource, DraftInput> {
+public class PutDraftComment
+    extends RetryingRestModifyView<DraftCommentResource, DraftInput, Response<CommentInfo>> {
 
   private final Provider<ReviewDb> db;
   private final DeleteDraftComment delete;
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
-  private final BatchUpdate.Factory updateFactory;
   private final Provider<CommentJson> commentJson;
   private final PatchListCache patchListCache;
 
@@ -62,23 +63,24 @@
       DeleteDraftComment delete,
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
-      BatchUpdate.Factory updateFactory,
+      RetryHelper retryHelper,
       Provider<CommentJson> commentJson,
       PatchListCache patchListCache) {
+    super(retryHelper);
     this.db = db;
     this.delete = delete;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
-    this.updateFactory = updateFactory;
     this.commentJson = commentJson;
     this.patchListCache = patchListCache;
   }
 
   @Override
-  public Response<CommentInfo> apply(DraftCommentResource rsrc, DraftInput in)
+  protected Response<CommentInfo> applyImpl(
+      BatchUpdate.Factory updateFactory, DraftCommentResource rsrc, DraftInput in)
       throws RestApiException, UpdateException, OrmException {
     if (in == null || in.message == null || in.message.trim().isEmpty()) {
-      return delete.apply(rsrc, null);
+      return delete.applyImpl(updateFactory, rsrc, null);
     } else if (in.id != null && !rsrc.getId().equals(in.id)) {
       throw new BadRequestException("id must match URL");
     } else if (in.line != null && in.line < 0) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutPrivate.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutPrivate.java
index d9105f1..bd2bf05 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutPrivate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutPrivate.java
@@ -18,13 +18,14 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -32,25 +33,23 @@
 
 @Singleton
 public class PutPrivate
-    implements RestModifyView<ChangeResource, PutPrivate.Input>, UiAction<ChangeResource> {
+    extends RetryingRestModifyView<ChangeResource, PutPrivate.Input, Response<String>>
+    implements UiAction<ChangeResource> {
   public static class Input {}
 
   private final ChangeMessagesUtil cmUtil;
   private final Provider<ReviewDb> dbProvider;
-  private final BatchUpdate.Factory batchUpdateFactory;
 
   @Inject
-  PutPrivate(
-      Provider<ReviewDb> dbProvider,
-      BatchUpdate.Factory batchUpdateFactory,
-      ChangeMessagesUtil cmUtil) {
+  PutPrivate(Provider<ReviewDb> dbProvider, RetryHelper retryHelper, ChangeMessagesUtil cmUtil) {
+    super(retryHelper);
     this.dbProvider = dbProvider;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.cmUtil = cmUtil;
   }
 
   @Override
-  public Response<String> apply(ChangeResource rsrc, Input input)
+  protected Response<String> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException {
     if (!rsrc.isUserOwner()) {
       throw new AuthException("not allowed to mark private");
@@ -63,7 +62,7 @@
     ChangeControl control = rsrc.getControl();
     SetPrivateOp op = new SetPrivateOp(cmUtil, true);
     try (BatchUpdate u =
-        batchUpdateFactory.create(
+        updateFactory.create(
             dbProvider.get(),
             control.getProject().getNameKey(),
             control.getUser(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
index b1e351b..5d9f7b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -34,6 +33,8 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -41,10 +42,10 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class PutTopic implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
+public class PutTopic extends RetryingRestModifyView<ChangeResource, Input, Response<String>>
+    implements UiAction<ChangeResource> {
   private final Provider<ReviewDb> dbProvider;
   private final ChangeMessagesUtil cmUtil;
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final TopicEdited topicEdited;
 
   public static class Input {
@@ -55,29 +56,27 @@
   PutTopic(
       Provider<ReviewDb> dbProvider,
       ChangeMessagesUtil cmUtil,
-      BatchUpdate.Factory batchUpdateFactory,
+      RetryHelper retryHelper,
       TopicEdited topicEdited) {
+    super(retryHelper);
     this.dbProvider = dbProvider;
     this.cmUtil = cmUtil;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.topicEdited = topicEdited;
   }
 
   @Override
-  public Response<String> apply(ChangeResource req, Input input)
+  protected Response<String> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource req, Input input)
       throws UpdateException, RestApiException, PermissionBackendException {
     req.permissions().check(ChangePermission.EDIT_TOPIC_NAME);
-
     Op op = new Op(input != null ? input : new Input());
     try (BatchUpdate u =
-        batchUpdateFactory.create(
+        updateFactory.create(
             dbProvider.get(), req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) {
       u.addOp(req.getId(), op);
       u.execute();
     }
-    return Strings.isNullOrEmpty(op.newTopicName)
-        ? Response.<String>none()
-        : Response.ok(op.newTopicName);
+    return Strings.isNullOrEmpty(op.newTopicName) ? Response.none() : Response.ok(op.newTopicName);
   }
 
   private class Op implements BatchUpdateOp {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index b866489..7131e20 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -40,6 +40,8 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -57,13 +59,12 @@
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class Rebase
+public class Rebase extends RetryingRestModifyView<RevisionResource, RebaseInput, ChangeInfo>
     implements RestModifyView<RevisionResource, RebaseInput>, UiAction<RevisionResource> {
   private static final Logger log = LoggerFactory.getLogger(Rebase.class);
   private static final ImmutableSet<ListChangesOption> OPTIONS =
       Sets.immutableEnumSet(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT);
 
-  private final BatchUpdate.Factory updateFactory;
   private final GitRepositoryManager repoManager;
   private final RebaseChangeOp.Factory rebaseFactory;
   private final RebaseUtil rebaseUtil;
@@ -72,13 +73,13 @@
 
   @Inject
   public Rebase(
-      BatchUpdate.Factory updateFactory,
+      RetryHelper retryHelper,
       GitRepositoryManager repoManager,
       RebaseChangeOp.Factory rebaseFactory,
       RebaseUtil rebaseUtil,
       ChangeJson.Factory json,
       Provider<ReviewDb> dbProvider) {
-    this.updateFactory = updateFactory;
+    super(retryHelper);
     this.repoManager = repoManager;
     this.rebaseFactory = rebaseFactory;
     this.rebaseUtil = rebaseUtil;
@@ -87,7 +88,8 @@
   }
 
   @Override
-  public ChangeInfo apply(RevisionResource rsrc, RebaseInput input)
+  protected ChangeInfo applyImpl(
+      BatchUpdate.Factory updateFactory, RevisionResource rsrc, RebaseInput input)
       throws EmailException, OrmException, UpdateException, RestApiException, IOException,
           NoSuchChangeException, PermissionBackendException {
     rsrc.permissions().database(dbProvider).check(ChangePermission.REBASE);
@@ -210,18 +212,21 @@
         .setEnabled(enabled);
   }
 
-  public static class CurrentRevision implements RestModifyView<ChangeResource, RebaseInput> {
+  public static class CurrentRevision
+      extends RetryingRestModifyView<ChangeResource, RebaseInput, ChangeInfo> {
     private final PatchSetUtil psUtil;
     private final Rebase rebase;
 
     @Inject
-    CurrentRevision(PatchSetUtil psUtil, Rebase rebase) {
+    CurrentRevision(RetryHelper retryHelper, PatchSetUtil psUtil, Rebase rebase) {
+      super(retryHelper);
       this.psUtil = psUtil;
       this.rebase = rebase;
     }
 
     @Override
-    public ChangeInfo apply(ChangeResource rsrc, RebaseInput input)
+    protected ChangeInfo applyImpl(
+        BatchUpdate.Factory updateFactory, ChangeResource rsrc, RebaseInput input)
         throws EmailException, OrmException, UpdateException, RestApiException, IOException,
             PermissionBackendException {
       PatchSet ps = psUtil.current(rebase.dbProvider.get(), rsrc.getNotes());
@@ -230,7 +235,7 @@
       } else if (!rsrc.getControl().isPatchVisible(ps, rebase.dbProvider.get())) {
         throw new AuthException("current revision not accessible");
       }
-      return rebase.apply(new RevisionResource(rsrc, ps), input);
+      return rebase.applyImpl(updateFactory, new RevisionResource(rsrc, ps), input);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index b362472..6f74ddd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
@@ -41,6 +40,8 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -50,8 +51,8 @@
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class Restore
-    implements RestModifyView<ChangeResource, RestoreInput>, UiAction<ChangeResource> {
+public class Restore extends RetryingRestModifyView<ChangeResource, RestoreInput, ChangeInfo>
+    implements UiAction<ChangeResource> {
   private static final Logger log = LoggerFactory.getLogger(Restore.class);
 
   private final RestoredSender.Factory restoredSenderFactory;
@@ -59,7 +60,6 @@
   private final ChangeJson.Factory json;
   private final ChangeMessagesUtil cmUtil;
   private final PatchSetUtil psUtil;
-  private final BatchUpdate.Factory batchUpdateFactory;
   private final ChangeRestored changeRestored;
 
   @Inject
@@ -69,26 +69,27 @@
       ChangeJson.Factory json,
       ChangeMessagesUtil cmUtil,
       PatchSetUtil psUtil,
-      BatchUpdate.Factory batchUpdateFactory,
+      RetryHelper retryHelper,
       ChangeRestored changeRestored) {
+    super(retryHelper);
     this.restoredSenderFactory = restoredSenderFactory;
     this.dbProvider = dbProvider;
     this.json = json;
     this.cmUtil = cmUtil;
     this.psUtil = psUtil;
-    this.batchUpdateFactory = batchUpdateFactory;
     this.changeRestored = changeRestored;
   }
 
   @Override
-  public ChangeInfo apply(ChangeResource req, RestoreInput input)
+  protected ChangeInfo applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource req, RestoreInput input)
       throws RestApiException, UpdateException, OrmException, PermissionBackendException {
     req.permissions().database(dbProvider).check(ChangePermission.RESTORE);
 
     ChangeControl ctl = req.getControl();
     Op op = new Op(input);
     try (BatchUpdate u =
-        batchUpdateFactory.create(
+        updateFactory.create(
             dbProvider.get(), req.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
       u.addOp(req.getId(), op).execute();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index 14e55c5..039aa9e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -50,6 +49,8 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -74,15 +75,14 @@
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class Revert
-    implements RestModifyView<ChangeResource, RevertInput>, UiAction<ChangeResource> {
+public class Revert extends RetryingRestModifyView<ChangeResource, RevertInput, ChangeInfo>
+    implements UiAction<ChangeResource> {
   private static final Logger log = LoggerFactory.getLogger(Revert.class);
 
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager repoManager;
   private final ChangeInserter.Factory changeInserterFactory;
   private final ChangeMessagesUtil cmUtil;
-  private final BatchUpdate.Factory updateFactory;
   private final Sequences seq;
   private final PatchSetUtil psUtil;
   private final RevertedSender.Factory revertedSenderFactory;
@@ -97,7 +97,7 @@
       GitRepositoryManager repoManager,
       ChangeInserter.Factory changeInserterFactory,
       ChangeMessagesUtil cmUtil,
-      BatchUpdate.Factory updateFactory,
+      RetryHelper retryHelper,
       Sequences seq,
       PatchSetUtil psUtil,
       RevertedSender.Factory revertedSenderFactory,
@@ -105,11 +105,11 @@
       @GerritPersonIdent PersonIdent serverIdent,
       ApprovalsUtil approvalsUtil,
       ChangeReverted changeReverted) {
+    super(retryHelper);
     this.db = db;
     this.repoManager = repoManager;
     this.changeInserterFactory = changeInserterFactory;
     this.cmUtil = cmUtil;
-    this.updateFactory = updateFactory;
     this.seq = seq;
     this.psUtil = psUtil;
     this.revertedSenderFactory = revertedSenderFactory;
@@ -120,7 +120,8 @@
   }
 
   @Override
-  public ChangeInfo apply(ChangeResource req, RevertInput input)
+  protected ChangeInfo applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource req, RevertInput input)
       throws IOException, OrmException, RestApiException, UpdateException, NoSuchChangeException {
     RefControl refControl = req.getControl().getRefControl();
     ProjectControl projectControl = req.getControl().getProjectControl();
@@ -137,11 +138,12 @@
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     }
 
-    Change.Id revertedChangeId = revert(req.getControl(), Strings.emptyToNull(input.message));
+    Change.Id revertedChangeId =
+        revert(updateFactory, req.getControl(), Strings.emptyToNull(input.message));
     return json.noOptions().format(req.getProject(), revertedChangeId);
   }
 
-  private Change.Id revert(ChangeControl ctl, String message)
+  private Change.Id revert(BatchUpdate.Factory updateFactory, ChangeControl ctl, String message)
       throws OrmException, IOException, RestApiException, UpdateException {
     Change.Id changeIdToRevert = ctl.getChange().getId();
     PatchSet.Id patchSetId = ctl.getChange().currentPatchSetId();
@@ -196,7 +198,6 @@
 
       Change.Id changeId = new Change.Id(seq.nextChangeId());
       ObjectId id = oi.insert(revertCommitBuilder);
-      oi.flush();
       RevCommit revertCommit = revWalk.parseCommit(id);
 
       ChangeInserter ins =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetReadyForReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetReadyForReview.java
index dbc82b7..ef174ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetReadyForReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetReadyForReview.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
@@ -28,28 +27,29 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.WorkInProgressOp.Input;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
-public class SetReadyForReview
-    implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
-  private final BatchUpdate.Factory batchUpdateFactory;
+public class SetReadyForReview extends RetryingRestModifyView<ChangeResource, Input, Response<?>>
+    implements UiAction<ChangeResource> {
   private final ChangeMessagesUtil cmUtil;
   private final Provider<ReviewDb> db;
 
   @Inject
-  SetReadyForReview(
-      BatchUpdate.Factory batchUpdateFactory, ChangeMessagesUtil cmUtil, Provider<ReviewDb> db) {
-    this.batchUpdateFactory = batchUpdateFactory;
+  SetReadyForReview(RetryHelper retryHelper, ChangeMessagesUtil cmUtil, Provider<ReviewDb> db) {
+    super(retryHelper);
     this.cmUtil = cmUtil;
     this.db = db;
   }
 
   @Override
-  public Response<?> apply(ChangeResource rsrc, Input input)
+  protected Response<?> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException {
     Change change = rsrc.getChange();
     if (!rsrc.isUserOwner()) {
@@ -65,7 +65,7 @@
     }
 
     try (BatchUpdate bu =
-        batchUpdateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       bu.addOp(rsrc.getChange().getId(), new WorkInProgressOp(cmUtil, false, input));
       bu.execute();
       return Response.ok("");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetWorkInProgress.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetWorkInProgress.java
index aa93fc2..2b481a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetWorkInProgress.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetWorkInProgress.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
@@ -28,28 +27,29 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.WorkInProgressOp.Input;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
-public class SetWorkInProgress
-    implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
-  private final BatchUpdate.Factory batchUpdateFactory;
+public class SetWorkInProgress extends RetryingRestModifyView<ChangeResource, Input, Response<?>>
+    implements UiAction<ChangeResource> {
   private final ChangeMessagesUtil cmUtil;
   private final Provider<ReviewDb> db;
 
   @Inject
-  SetWorkInProgress(
-      BatchUpdate.Factory batchUpdateFactory, ChangeMessagesUtil cmUtil, Provider<ReviewDb> db) {
-    this.batchUpdateFactory = batchUpdateFactory;
+  SetWorkInProgress(RetryHelper retryHelper, ChangeMessagesUtil cmUtil, Provider<ReviewDb> db) {
+    super(retryHelper);
     this.cmUtil = cmUtil;
     this.db = db;
   }
 
   @Override
-  public Response<?> apply(ChangeResource rsrc, Input input)
+  protected Response<?> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException {
     Change change = rsrc.getChange();
     if (!rsrc.isUserOwner()) {
@@ -65,7 +65,7 @@
     }
 
     try (BatchUpdate bu =
-        batchUpdateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       bu.addOp(rsrc.getChange().getId(), new WorkInProgressOp(cmUtil, true, input));
       bu.execute();
       return Response.ok("");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 3e489da..b1b980f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -209,6 +209,11 @@
       submitter = rsrc.getUser().asIdentifiedUser();
     }
 
+    return new Output(mergeChange(rsrc, submitter, input));
+  }
+
+  public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
+      throws OrmException, RestApiException, IOException {
     Change change = rsrc.getChange();
     if (!change.getStatus().isOpen()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
@@ -235,7 +240,7 @@
 
     switch (change.getStatus()) {
       case MERGED:
-        return new Output(change);
+        return change;
       case NEW:
         ChangeMessage msg = getConflictMessage(rsrc);
         if (msg != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 11871e7..bb07884 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -92,7 +92,6 @@
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
 import com.google.gerrit.server.events.CommitReceivedEvent;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
@@ -122,6 +121,9 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RepoContext;
+import com.google.gerrit.server.update.RepoOnlyOp;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.MagicBranch;
@@ -152,7 +154,6 @@
 import java.util.regex.Pattern;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -300,7 +301,6 @@
   private final PermissionBackend permissionBackend;
   private final PermissionBackend.ForProject permissions;
   private final CmdLineParser.Factory optionParserFactory;
-  private final GitReferenceUpdated gitRefUpdated;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final PatchSetUtil psUtil;
   private final ProjectCache projectCache;
@@ -348,6 +348,15 @@
   private final ChangeEditUtil editUtil;
   private final ChangeIndexer indexer;
 
+  /**
+   * Actual commands to be executed, as opposed to the mix of actual and magic commands that were
+   * provided over the wire.
+   *
+   * <p>Excludes commands executed implicitly as part of other {@link BatchUpdateOp}s, such as
+   * creating patch set refs.
+   */
+  private final List<ReceiveCommand> actualCommands = new ArrayList<>();
+
   private final List<ValidationMessage> messages = new ArrayList<>();
   private ListMultimap<Error, String> errors = LinkedListMultimap.create();
   private Task newProgress;
@@ -355,7 +364,6 @@
   private Task closeProgress;
   private Task commandProgress;
   private MessageSender messageSender;
-  private BatchRefUpdate batch;
 
   @Inject
   ReceiveCommits(
@@ -366,7 +374,6 @@
       AccountResolver accountResolver,
       PermissionBackend permissionBackend,
       CmdLineParser.Factory optionParserFactory,
-      GitReferenceUpdated gitRefUpdated,
       PatchSetInfoFactory patchSetInfoFactory,
       PatchSetUtil psUtil,
       ProjectCache projectCache,
@@ -406,7 +413,6 @@
     this.accountResolver = accountResolver;
     this.permissionBackend = permissionBackend;
     this.optionParserFactory = optionParserFactory;
-    this.gitRefUpdated = gitRefUpdated;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.psUtil = psUtil;
     this.projectCache = projectCache;
@@ -589,15 +595,10 @@
     closeProgress = progress.beginSubTask("closed", UNKNOWN);
     commandProgress = progress.beginSubTask("refs", UNKNOWN);
 
-    batch = repo.getRefDatabase().newBatchUpdate();
-    batch.setPushCertificate(rp.getPushCertificate());
-    batch.setRefLogIdent(rp.getRefLogIdent());
-    batch.setRefLogMessage("push", true);
-
     try {
       parseCommands(commands);
     } catch (PermissionBackendException err) {
-      for (ReceiveCommand cmd : batch.getCommands()) {
+      for (ReceiveCommand cmd : actualCommands) {
         if (cmd.getResult() == NOT_ATTEMPTED) {
           cmd.setResult(REJECTED_OTHER_REASON, "internal server error");
         }
@@ -608,27 +609,6 @@
       selectNewAndReplacedChangesFromMagicBranch();
     }
     preparePatchSetsForReplace();
-
-    logDebug("Executing batch with {} commands", batch.getCommands().size());
-    if (!batch.getCommands().isEmpty()) {
-      try {
-        if (!batch.isAllowNonFastForwards() && magicBranch != null && magicBranch.edit) {
-          logDebug("Allowing non-fast-forward for edit ref");
-          batch.setAllowNonFastForwards(true);
-        }
-        batch.execute(rp.getRevWalk(), commandProgress);
-      } catch (IOException err) {
-        int cnt = 0;
-        for (ReceiveCommand cmd : batch.getCommands()) {
-          if (cmd.getResult() == NOT_ATTEMPTED) {
-            cmd.setResult(REJECTED_OTHER_REASON, "internal server error");
-            cnt++;
-          }
-        }
-        logError(String.format("Failed to store %d refs in %s", cnt, project.getName()), err);
-      }
-    }
-
     insertChangesAndPatchSets();
     newProgress.end();
     replaceProgress.end();
@@ -643,44 +623,24 @@
     }
 
     Set<Branch.NameKey> branches = new HashSet<>();
-    for (ReceiveCommand c : batch.getCommands()) {
-      if (c.getResult() == OK) {
-        String refName = c.getRefName();
-        if (c.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward
-          logDebug("Updating tag cache on fast-forward of {}", c.getRefName());
-          tagCache.updateFastForward(project.getNameKey(), refName, c.getOldId(), c.getNewId());
-        }
+    for (ReceiveCommand c : actualCommands) {
+      // Most post-update steps should happen in UpdateOneRefOp#postUpdate. The only steps that
+      // should happen in this loop are things that can't happen within one BatchUpdate because they
+      // involve kicking off an additional BatchUpdate.
+      if (c.getResult() != OK) {
+        continue;
+      }
+      if (isHead(c) || isConfig(c)) {
+        switch (c.getType()) {
+          case CREATE:
+          case UPDATE:
+          case UPDATE_NONFASTFORWARD:
+            autoCloseChanges(c);
+            branches.add(new Branch.NameKey(project.getNameKey(), c.getRefName()));
+            break;
 
-        if (isHead(c) || isConfig(c)) {
-          switch (c.getType()) {
-            case CREATE:
-            case UPDATE:
-            case UPDATE_NONFASTFORWARD:
-              autoCloseChanges(c);
-              branches.add(new Branch.NameKey(project.getNameKey(), refName));
-              break;
-
-            case DELETE:
-              break;
-          }
-        }
-
-        if (isConfig(c)) {
-          logDebug("Reloading project in cache");
-          projectCache.evict(project);
-          ProjectState ps = projectCache.get(project.getNameKey());
-          try {
-            repo.setGitwebDescription(ps.getProject().getDescription());
-          } catch (IOException e) {
-            log.warn("cannot update description of " + project.getName(), e);
-          }
-        }
-
-        if (!MagicBranch.isMagicBranch(refName)) {
-          logDebug("Firing ref update for {}", c.getRefName());
-          gitRefUpdated.fire(project.getNameKey(), c, user.getAccount());
-        } else {
-          logDebug("Assuming ref update event for {} has fired", c.getRefName());
+          case DELETE:
+            break;
         }
       }
     }
@@ -775,117 +735,42 @@
   }
 
   private void insertChangesAndPatchSets() {
-    int replaceCount = 0;
-    int okToInsert = 0;
-
-    for (Map.Entry<Change.Id, ReplaceRequest> e : replaceByChange.entrySet()) {
-      ReplaceRequest replace = e.getValue();
-      if (magicBranch != null && replace.inputCommand == magicBranch.cmd) {
-        replaceCount++;
-
-        if (replace.cmd != null && replace.cmd.getResult() == OK) {
-          okToInsert++;
-        }
-      } else if (replace.cmd != null && replace.cmd.getResult() == OK) {
-        String refName = replace.inputCommand.getRefName();
-        checkState(
-            NEW_PATCHSET.matcher(refName).matches(),
-            "expected a new patch set command as input when creating %s; got %s",
-            replace.cmd.getRefName(),
-            refName);
-        try {
-          logDebug("One-off insertion of patch set for {}", refName);
-          replace.insertPatchSetWithoutBatchUpdate();
-          replace.inputCommand.setResult(OK);
-        } catch (IOException | UpdateException | RestApiException err) {
-          reject(replace.inputCommand, "internal server error");
-          logError(
-              String.format(
-                  "Cannot add patch set to change %d in project %s",
-                  e.getKey().get(), project.getName()),
-              err);
-        }
-      } else if (replace.inputCommand.getResult() == NOT_ATTEMPTED) {
-        reject(replace.inputCommand, "internal server error");
-        logError(String.format("Replacement for project %s was not attempted", project.getName()));
-      }
-    }
-
-    // refs/for/ or refs/drafts/ not used, or it already failed earlier.
-    // No need to continue.
-    if (magicBranch == null) {
-      logDebug("No magic branch, nothing more to do");
-      return;
-    } else if (magicBranch.cmd.getResult() != NOT_ATTEMPTED) {
+    ReceiveCommand magicBranchCmd = magicBranch != null ? magicBranch.cmd : null;
+    if (magicBranchCmd != null && magicBranchCmd.getResult() != NOT_ATTEMPTED) {
       logWarn(
           String.format(
               "Skipping change updates on %s because ref update failed: %s %s",
               project.getName(),
-              magicBranch.cmd.getResult(),
-              Strings.nullToEmpty(magicBranch.cmd.getMessage())));
-      return;
-    }
-
-    List<String> lastCreateChangeErrors = new ArrayList<>();
-    for (CreateRequest create : newChanges) {
-      if (create.cmd.getResult() == OK) {
-        okToInsert++;
-      } else {
-        String createChangeResult =
-            String.format(
-                    "%s %s", create.cmd.getResult(), Strings.nullToEmpty(create.cmd.getMessage()))
-                .trim();
-        lastCreateChangeErrors.add(createChangeResult);
-        logError(
-            String.format(
-                "Command %s on %s:%s not completed: %s",
-                create.cmd.getType(),
-                project.getName(),
-                create.cmd.getRefName(),
-                createChangeResult));
-      }
-    }
-
-    logDebug(
-        "Counted {} ok to insert, out of {} to replace and {} new",
-        okToInsert,
-        replaceCount,
-        newChanges.size());
-
-    if (okToInsert != replaceCount + newChanges.size()) {
-      // One or more new references failed to create. Assume the
-      // system isn't working correctly anymore and abort.
-      reject(
-          magicBranch.cmd,
-          "Unable to create changes: " + lastCreateChangeErrors.stream().collect(joining(" ")));
-      logError(
-          String.format(
-              "Only %d of %d new change refs created in %s; aborting",
-              okToInsert, replaceCount + newChanges.size(), project.getName()));
+              magicBranchCmd.getResult(),
+              Strings.nullToEmpty(magicBranchCmd.getMessage())));
       return;
     }
 
     try (BatchUpdate bu =
             batchUpdateFactory.create(
-                db, magicBranch.dest.getParentKey(), user.materializedCopy(), TimeUtil.nowTs());
+                db, project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
         ObjectInserter ins = repo.newObjectInserter();
         ObjectReader reader = ins.newReader();
         RevWalk rw = new RevWalk(reader)) {
       bu.setRepository(repo, rw, ins).updateChangesInParallel();
       bu.setRequestId(receiveId);
+      bu.setRefLogMessage("push");
+
+      logDebug("Adding {} replace requests", newChanges.size());
       for (ReplaceRequest replace : replaceByChange.values()) {
-        if (replace.inputCommand == magicBranch.cmd) {
-          replace.addOps(bu, replaceProgress);
-        }
+        replace.addOps(bu, replaceProgress);
       }
 
+      logDebug("Adding {} create requests", newChanges.size());
       for (CreateRequest create : newChanges) {
         create.addOps(bu);
       }
 
-      for (UpdateGroupsRequest update : updateGroups) {
-        update.addOps(bu);
-      }
+      logDebug("Adding {} group update requests", newChanges.size());
+      updateGroups.forEach(r -> r.addOps(bu));
+
+      logDebug("Adding {} additional ref updates", actualCommands.size());
+      actualCommands.forEach(c -> bu.addRepoOnlyOp(new UpdateOneRefOp(c)));
 
       logDebug("Executing batch");
       try {
@@ -893,10 +778,17 @@
       } catch (UpdateException e) {
         throw INSERT_EXCEPTION.apply(e);
       }
-      magicBranch.cmd.setResult(OK);
+      if (magicBranchCmd != null) {
+        magicBranchCmd.setResult(OK);
+      }
       for (ReplaceRequest replace : replaceByChange.values()) {
         String rejectMessage = replace.getRejectMessage();
-        if (rejectMessage != null) {
+        if (rejectMessage == null) {
+          if (replace.inputCommand.getResult() == NOT_ATTEMPTED) {
+            // Not necessarily the magic branch, so need to set OK on the original value.
+            replace.inputCommand.setResult(OK);
+          }
+        } else {
           logDebug("Rejecting due to message from ReplaceOp");
           reject(replace.inputCommand, rejectMessage);
         }
@@ -904,10 +796,10 @@
 
     } catch (ResourceConflictException e) {
       addMessage(e.getMessage());
-      reject(magicBranch.cmd, "conflict");
+      reject(magicBranchCmd, "conflict");
     } catch (RestApiException | IOException err) {
       logError("Can't insert change/patch set for " + project.getName(), err);
-      reject(magicBranch.cmd, "internal server error: " + err.getMessage());
+      reject(magicBranchCmd, "internal server error: " + err.getMessage());
     }
 
     if (magicBranch != null && magicBranch.submit) {
@@ -915,10 +807,10 @@
         submit(newChanges, replaceByChange.values());
       } catch (ResourceConflictException e) {
         addMessage(e.getMessage());
-        reject(magicBranch.cmd, "conflict");
+        reject(magicBranchCmd, "conflict");
       } catch (RestApiException | OrmException e) {
         logError("Error submitting changes to " + project.getName(), e);
-        reject(magicBranch.cmd, "error during submit");
+        reject(magicBranchCmd, "error during submit");
       }
     }
   }
@@ -1176,7 +1068,7 @@
         return;
       }
       validateNewCommits(ctl, cmd);
-      batch.addCommand(cmd);
+      actualCommands.add(cmd);
     } else {
       reject(cmd, "prohibited by Gerrit: create access denied for " + cmd.getRefName());
     }
@@ -1199,7 +1091,7 @@
         return;
       }
       validateNewCommits(projectControl.controlForRef(cmd.getRefName()), cmd);
-      batch.addCommand(cmd);
+      actualCommands.add(cmd);
     } else {
       if (RefNames.REFS_CONFIG.equals(cmd.getRefName())) {
         errors.put(Error.CONFIG_UPDATE, RefNames.REFS_CONFIG);
@@ -1237,7 +1129,7 @@
       if (!validRefOperation(cmd)) {
         return;
       }
-      batch.addCommand(cmd);
+      actualCommands.add(cmd);
     } else {
       if (RefNames.REFS_CONFIG.equals(ctl.getRefName())) {
         reject(cmd, "cannot delete project configuration");
@@ -1282,7 +1174,7 @@
       if (!validRefOperation(cmd)) {
         return;
       }
-      batch.setAllowNonFastForwards(true).addCommand(cmd);
+      actualCommands.add(cmd);
     } else {
       cmd.setResult(
           REJECTED_NONFASTFORWARD, " need '" + PermissionRule.FORCE_PUSH + "' privilege.");
@@ -2039,7 +1931,6 @@
       for (int i = 0; i < newChanges.size(); i++) {
         CreateRequest create = newChanges.get(i);
         create.setChangeId(newIds.get(i));
-        batch.addCommand(create.cmd);
         create.groups = ImmutableList.copyOf(groups.get(create.commit));
       }
       for (ReplaceRequest replace : replaceByChange.values()) {
@@ -2252,7 +2143,6 @@
                 .setAccountsToNotify(magicBranch.getAccountsToNotify())
                 .setRequestScopePropagator(requestScopePropagator)
                 .setSendMail(true)
-                .setUpdateRef(false)
                 .setPatchSetDescription(magicBranch.message));
         if (!magicBranch.hashtags.isEmpty()) {
           // Any change owner is allowed to add hashtags when creating a change.
@@ -2342,15 +2232,6 @@
     }
     logDebug("Read {} changes to replace", replaceByChange.size());
 
-    for (ReplaceRequest req : replaceByChange.values()) {
-      if (req.inputCommand.getResult() == NOT_ATTEMPTED && req.cmd != null) {
-        if (req.prev != null) {
-          batch.addCommand(req.prev);
-        }
-        batch.addCommand(req.cmd);
-      }
-    }
-
     if (magicBranch != null && magicBranch.cmd.getResult() != NOT_ATTEMPTED) {
       // Cancel creations tied to refs/for/ or refs/drafts/ command.
       for (ReplaceRequest req : replaceByChange.values()) {
@@ -2394,7 +2275,7 @@
         Change.Id toChange, RevCommit newCommit, ReceiveCommand cmd, boolean checkMergedInto) {
       this.ontoChange = toChange;
       this.newCommitId = newCommit.copy();
-      this.inputCommand = cmd;
+      this.inputCommand = checkNotNull(cmd);
       this.checkMergedInto = checkMergedInto;
 
       revisions = HashBiMap.create();
@@ -2572,21 +2453,12 @@
     }
 
     void addOps(BatchUpdate bu, @Nullable Task progress) throws IOException {
-      if (cmd.getResult() == NOT_ATTEMPTED) {
-        // TODO(dborowitz): When does this happen? Only when an edit ref is
-        // involved?
-        cmd.execute(rp);
-      }
       if (magicBranch != null && magicBranch.edit) {
-        bu.addOp(
-            notes.getChangeId(),
-            new BatchUpdateOp() {
-              @Override
-              public boolean updateChange(ChangeContext ctx) throws Exception {
-                // return pseudo dirty state to trigger reindexing
-                return true;
-              }
-            });
+        bu.addOp(notes.getChangeId(), new ReindexOnlyOp());
+        if (prev != null) {
+          bu.addRepoOnlyOp(new UpdateOneRefOp(prev));
+        }
+        bu.addRepoOnlyOp(new UpdateOneRefOp(cmd));
         return;
       }
       RevWalk rw = rp.getRevWalk();
@@ -2609,28 +2481,13 @@
                   groups,
                   magicBranch,
                   rp.getPushCertificate())
-              .setRequestScopePropagator(requestScopePropagator)
-              .setUpdateRef(false);
+              .setRequestScopePropagator(requestScopePropagator);
       bu.addOp(notes.getChangeId(), replaceOp);
       if (progress != null) {
         bu.addOp(notes.getChangeId(), new ChangeProgressOp(progress));
       }
     }
 
-    void insertPatchSetWithoutBatchUpdate() throws IOException, UpdateException, RestApiException {
-      try (BatchUpdate bu =
-              batchUpdateFactory.create(
-                  db, projectControl.getProject().getNameKey(), user, TimeUtil.nowTs());
-          ObjectInserter ins = repo.newObjectInserter();
-          ObjectReader reader = ins.newReader();
-          RevWalk rw = new RevWalk(reader)) {
-        bu.setRepository(repo, rw, ins);
-        bu.setRequestId(receiveId);
-        addOps(bu, replaceProgress);
-        bu.execute();
-      }
-    }
-
     String getRejectMessage() {
       return replaceOp != null ? replaceOp.getRejectMessage() : null;
     }
@@ -2672,6 +2529,47 @@
     }
   }
 
+  private class UpdateOneRefOp implements RepoOnlyOp {
+    private final ReceiveCommand cmd;
+
+    private UpdateOneRefOp(ReceiveCommand cmd) {
+      this.cmd = checkNotNull(cmd);
+    }
+
+    @Override
+    public void updateRepo(RepoContext ctx) throws IOException {
+      ctx.addRefUpdate(cmd);
+    }
+
+    @Override
+    public void postUpdate(Context ctx) {
+      String refName = cmd.getRefName();
+      if (cmd.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward
+        logDebug("Updating tag cache on fast-forward of {}", cmd.getRefName());
+        tagCache.updateFastForward(project.getNameKey(), refName, cmd.getOldId(), cmd.getNewId());
+      }
+      if (isConfig(cmd)) {
+        logDebug("Reloading project in cache");
+        projectCache.evict(project);
+        ProjectState ps = projectCache.get(project.getNameKey());
+        try {
+          logDebug("Updating project description");
+          repo.setGitwebDescription(ps.getProject().getDescription());
+        } catch (IOException e) {
+          log.warn("cannot update description of " + project.getName(), e);
+        }
+      }
+    }
+  }
+
+  private static class ReindexOnlyOp implements BatchUpdateOp {
+    @Override
+    public boolean updateChange(ChangeContext ctx) {
+      // Trigger reindexing even though change isn't actually updated.
+      return true;
+    }
+  }
+
   private List<Ref> refs(Change.Id changeId) {
     return refsByChange().get(changeId);
   }
@@ -2953,9 +2851,11 @@
     return r;
   }
 
-  private void reject(ReceiveCommand cmd, String why) {
-    cmd.setResult(REJECTED_OTHER_REASON, why);
-    commandProgress.update(1);
+  private void reject(@Nullable ReceiveCommand cmd, String why) {
+    if (cmd != null) {
+      cmd.setResult(REJECTED_OTHER_REASON, why);
+      commandProgress.update(1);
+    }
   }
 
   private static boolean isHead(ReceiveCommand cmd) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
index 5322b2e..0cee090 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -41,7 +40,6 @@
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.extensions.events.CommentAdded;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.extensions.events.RevisionCreated;
 import com.google.gerrit.server.git.ReceiveCommits.MagicBranchInput;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
@@ -71,6 +69,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -102,7 +101,6 @@
   private final ChangeKindCache changeKindCache;
   private final ChangeMessagesUtil cmUtil;
   private final ExecutorService sendEmailExecutor;
-  private final GitReferenceUpdated gitRefUpdated;
   private final RevisionCreated revisionCreated;
   private final CommentAdded commentAdded;
   private final MergedByPushOp.Factory mergedByPushOpFactory;
@@ -124,6 +122,7 @@
   private final Map<String, Short> approvals = new HashMap<>();
   private final MailRecipients recipients = new MailRecipients();
   private RevCommit commit;
+  private ReceiveCommand cmd;
   private Change change;
   private PatchSet newPatchSet;
   private ChangeKind changeKind;
@@ -131,7 +130,6 @@
   private String rejectMessage;
   private MergedByPushOp mergedByPushOp;
   private RequestScopePropagator requestScopePropagator;
-  private boolean updateRef;
 
   @Inject
   ReplaceOp(
@@ -142,7 +140,6 @@
       ChangeData.Factory changeDataFactory,
       ChangeKindCache changeKindCache,
       ChangeMessagesUtil cmUtil,
-      GitReferenceUpdated gitRefUpdated,
       RevisionCreated revisionCreated,
       CommentAdded commentAdded,
       MergedByPushOp.Factory mergedByPushOpFactory,
@@ -167,7 +164,6 @@
     this.changeDataFactory = changeDataFactory;
     this.changeKindCache = changeKindCache;
     this.cmUtil = cmUtil;
-    this.gitRefUpdated = gitRefUpdated;
     this.revisionCreated = revisionCreated;
     this.commentAdded = commentAdded;
     this.mergedByPushOpFactory = mergedByPushOpFactory;
@@ -186,7 +182,6 @@
     this.groups = groups;
     this.magicBranch = magicBranch;
     this.pushCertificate = pushCertificate;
-    this.updateRef = true;
   }
 
   @Override
@@ -209,9 +204,8 @@
       }
     }
 
-    if (updateRef) {
-      ctx.addRefUpdate(ObjectId.zeroId(), commitId, patchSetId.toRefName());
-    }
+    cmd = new ReceiveCommand(ObjectId.zeroId(), commitId, patchSetId.toRefName());
+    ctx.addRefUpdate(cmd);
   }
 
   @Override
@@ -403,17 +397,7 @@
   }
 
   @Override
-  public void postUpdate(final Context ctx) throws Exception {
-    // Normally the ref updated hook is fired by BatchUpdate, but ReplaceOp is
-    // special because its ref is actually updated by ReceiveCommits, so from
-    // BatchUpdate's perspective there is no ref update. Thus we have to fire it
-    // manually.
-    final Account account = ctx.getAccount();
-    if (!updateRef) {
-      gitRefUpdated.fire(
-          ctx.getProject(), newPatchSet.getRefName(), ObjectId.zeroId(), commitId, account);
-    }
-
+  public void postUpdate(Context ctx) throws Exception {
     if (changeKind != ChangeKind.TRIVIAL_REBASE) {
       Runnable sender =
           new Runnable() {
@@ -423,7 +407,7 @@
                 ReplacePatchSetSender cm =
                     replacePatchSetFactory.create(
                         projectControl.getProject().getNameKey(), change.getId());
-                cm.setFrom(account.getId());
+                cm.setFrom(ctx.getAccountId());
                 cm.setPatchSet(newPatchSet, info);
                 cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
                 if (magicBranch != null) {
@@ -508,9 +492,8 @@
     return rejectMessage;
   }
 
-  public ReplaceOp setUpdateRef(boolean updateRef) {
-    this.updateRef = updateRef;
-    return this;
+  public ReceiveCommand getCommand() {
+    return cmd;
   }
 
   public ReplaceOp setRequestScopePropagator(RequestScopePropagator requestScopePropagator) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index b58fb55..3756d73 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -231,7 +231,8 @@
     }
     if (id != null) {
       return (userEditPrefix != null && name.startsWith(userEditPrefix) && visible(id))
-          || projectCtl.controlForRef(visibleChanges.get(id)).isEditVisible();
+          || (visibleChanges.containsKey(id)
+              && projectCtl.controlForRef(visibleChanges.get(id)).isEditVisible());
     }
     return false;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 83193d5..3aa2748 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -64,6 +64,7 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
 /**
@@ -212,6 +213,7 @@
   private boolean checkExpectedState = true;
   private String refLogMessage;
   private PersonIdent refLogIdent;
+  private PushCertificate pushCert;
 
   @Inject
   NoteDbUpdateManager(
@@ -279,6 +281,25 @@
     return this;
   }
 
+  /**
+   * Set a push certificate for the push that originally triggered this NoteDb update.
+   *
+   * <p>The pusher will not necessarily have specified any of the NoteDb refs explicitly, such as
+   * when processing a push to {@code refs/for/master}. That's fine; this is just passed to the
+   * underlying {@link BatchRefUpdate}, and the implementation decides what to do with it.
+   *
+   * <p>The cert should be associated with the main repo. There is currently no way of associating a
+   * push cert with the {@code All-Users} repo, since it is not currently possible to update draft
+   * changes via push.
+   *
+   * @param pushCert push certificate; may be null.
+   * @return this
+   */
+  public NoteDbUpdateManager setPushCertificate(PushCertificate pushCert) {
+    this.pushCert = pushCert;
+    return this;
+  }
+
   public OpenRepo getChangeRepo() throws IOException {
     initChangeRepo();
     return changeRepo;
@@ -490,15 +511,16 @@
       // we may have stale draft comments. Doing it in this order allows stale
       // comments to be filtered out by ChangeNotes, reflecting the fact that
       // comments can only go from DRAFT to PUBLISHED, not vice versa.
-      BatchRefUpdate result = execute(changeRepo, dryrun);
-      execute(allUsersRepo, dryrun);
+      BatchRefUpdate result = execute(changeRepo, dryrun, pushCert);
+      execute(allUsersRepo, dryrun, null);
       return result;
     } finally {
       close();
     }
   }
 
-  private BatchRefUpdate execute(OpenRepo or, boolean dryrun) throws IOException {
+  private BatchRefUpdate execute(OpenRepo or, boolean dryrun, @Nullable PushCertificate pushCert)
+      throws IOException {
     if (or == null || or.cmds.isEmpty()) {
       return null;
     }
@@ -511,6 +533,7 @@
     }
 
     BatchRefUpdate bru = or.repo.getRefDatabase().newBatchUpdate();
+    bru.setPushCertificate(pushCert);
     bru.setRefLogMessage(firstNonNull(refLogMessage, "Update NoteDb refs"), false);
     bru.setRefLogIdent(refLogIdent != null ? refLogIdent : serverIdent.get());
     or.cmds.addTo(bru);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java
index 10da990f..387c966 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -32,8 +33,9 @@
   }
 
   @Override
-  public BinaryResult apply(FileResource rsrc) throws ResourceNotFoundException, IOException {
+  public BinaryResult apply(FileResource rsrc)
+      throws ResourceNotFoundException, BadRequestException, IOException {
     return fileContentUtil.getContent(
-        rsrc.getProject().getProjectState(), rsrc.getRev(), rsrc.getPath());
+        rsrc.getProject().getProjectState(), rsrc.getRev(), rsrc.getPath(), null);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 22e8d58..099a3d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -572,9 +572,12 @@
     }
 
     if ("reviewer".equalsIgnoreCase(value)) {
-      return Predicate.and(
-          Predicate.not(new BooleanPredicate(ChangeField.WIP, args.fillArgs)),
-          ReviewerPredicate.reviewer(args, self()));
+      if (args.getSchema().hasField(ChangeField.WIP)) {
+        return Predicate.and(
+            Predicate.not(new BooleanPredicate(ChangeField.WIP, args.fillArgs)),
+            ReviewerPredicate.reviewer(args, self()));
+      }
+      return ReviewerPredicate.reviewer(args, self());
     }
 
     if ("cc".equalsIgnoreCase(value)) {
@@ -606,7 +609,10 @@
     }
 
     if ("wip".equalsIgnoreCase(value)) {
-      return new BooleanPredicate(ChangeField.WIP, args.fillArgs);
+      if (args.getSchema().hasField(ChangeField.WIP)) {
+        return new BooleanPredicate(ChangeField.WIP, args.fillArgs);
+      }
+      throw new QueryParseException("'is:wip' operator is not supported by change index version");
     }
 
     try {
@@ -962,9 +968,12 @@
 
   @Operator
   public Predicate<ChangeData> reviewer(String who) throws QueryParseException, OrmException {
-    return Predicate.and(
-        Predicate.not(new BooleanPredicate(ChangeField.WIP, args.fillArgs)),
-        reviewerByState(who, ReviewerStateInternal.REVIEWER));
+    if (args.getSchema().hasField(ChangeField.WIP)) {
+      return Predicate.and(
+          Predicate.not(new BooleanPredicate(ChangeField.WIP, args.fillArgs)),
+          reviewerByState(who, ReviewerStateInternal.REVIEWER));
+    }
+    return reviewerByState(who, ReviewerStateInternal.REVIEWER);
   }
 
   @Operator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java
index 22329fd..fdae8e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -60,6 +60,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -107,6 +108,7 @@
     private final FusedNoteDbBatchUpdate.AssistedFactory fusedNoteDbBatchUpdateFactory;
     private final UnfusedNoteDbBatchUpdate.AssistedFactory unfusedNoteDbBatchUpdateFactory;
 
+    // TODO(dborowitz): Make this non-injectable to force all callers to use RetryHelper.
     @Inject
     Factory(
         NotesMigration migration,
@@ -261,6 +263,8 @@
   protected Order order;
   protected OnSubmitValidators onSubmitValidators;
   protected RequestId requestId;
+  protected PushCertificate pushCert;
+  protected String refLogMessage;
 
   private boolean updateChangesInParallel;
 
@@ -305,6 +309,16 @@
     return this;
   }
 
+  public BatchUpdate setPushCertificate(@Nullable PushCertificate pushCert) {
+    this.pushCert = pushCert;
+    return this;
+  }
+
+  public BatchUpdate setRefLogMessage(@Nullable String refLogMessage) {
+    this.refLogMessage = refLogMessage;
+    return this;
+  }
+
   public BatchUpdate setOrder(Order order) {
     this.order = order;
     return this;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdateOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdateOp.java
index 39e25dd..87a43a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdateOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdateOp.java
@@ -22,8 +22,11 @@
  * BatchUpdate#addOp(com.google.gerrit.reviewdb.client.Change.Id, BatchUpdateOp)}.
  *
  * <p>Usually, a single {@code BatchUpdateOp} instance is only associated with a single change, i.e.
- * {@code addOp} is only called once with that instance. This allows an instance to communicate
- * between phases by storing data in private fields.
+ * {@code addOp} is only called once with that instance. Additionally, each method in {@code
+ * BatchUpdateOp} is called at most once per {@link BatchUpdate} execution.
+ *
+ * <p>Taken together, these two properties mean an instance may communicate between phases by
+ * storing data in private fields, and a single instance must not be reused.
  */
 public interface BatchUpdateOp extends RepoOnlyOp {
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java
index ad758484..f8ef5f9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java
@@ -402,6 +402,8 @@
     if (user.isIdentifiedUser()) {
       handle.manager.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, tz));
     }
+    handle.manager.setRefLogMessage(refLogMessage);
+    handle.manager.setPushCertificate(pushCert);
     for (Map.Entry<Change.Id, Collection<BatchUpdateOp>> e : ops.asMap().entrySet()) {
       Change.Id id = e.getKey();
       ChangeContextImpl ctx = newChangeContext(id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java
new file mode 100644
index 0000000..dbbce2b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.update;
+
+import com.github.rholder.retry.RetryException;
+import com.github.rholder.retry.RetryerBuilder;
+import com.github.rholder.retry.StopStrategies;
+import com.github.rholder.retry.WaitStrategies;
+import com.google.common.base.Throwables;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.git.LockFailureException;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+public class RetryHelper {
+  public interface Action<T> {
+    T call(BatchUpdate.Factory updateFactory) throws Exception;
+  }
+
+  private final BatchUpdate.Factory updateFactory;
+
+  @Inject
+  RetryHelper(
+      NotesMigration migration,
+      ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory,
+      FusedNoteDbBatchUpdate.AssistedFactory fusedNoteDbBatchUpdateFactory,
+      UnfusedNoteDbBatchUpdate.AssistedFactory unfusedNoteDbBatchUpdateFactory) {
+    this.updateFactory =
+        new BatchUpdate.Factory(
+            migration,
+            reviewDbBatchUpdateFactory,
+            fusedNoteDbBatchUpdateFactory,
+            unfusedNoteDbBatchUpdateFactory);
+  }
+
+  public <T> T execute(Action<T> action) throws RestApiException, UpdateException {
+    try {
+      // TODO(dborowitz): Make configurable.
+      return RetryerBuilder.<T>newBuilder()
+          .withStopStrategy(StopStrategies.stopAfterDelay(20, TimeUnit.SECONDS))
+          .withWaitStrategy(
+              WaitStrategies.join(
+                  WaitStrategies.exponentialWait(5, TimeUnit.SECONDS),
+                  WaitStrategies.randomWait(50, TimeUnit.MILLISECONDS)))
+          .retryIfException(RetryHelper::isLockFailure)
+          .build()
+          .call(() -> action.call(updateFactory));
+    } catch (ExecutionException | RetryException e) {
+      if (e.getCause() != null) {
+        Throwables.throwIfInstanceOf(e.getCause(), UpdateException.class);
+        Throwables.throwIfInstanceOf(e.getCause(), RestApiException.class);
+      }
+      throw new UpdateException(e);
+    }
+  }
+
+  private static boolean isLockFailure(Throwable t) {
+    if (t instanceof UpdateException) {
+      t = t.getCause();
+    }
+    return t instanceof LockFailureException;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryingRestModifyView.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryingRestModifyView.java
new file mode 100644
index 0000000..e2f4a02
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryingRestModifyView.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.update;
+
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestResource;
+
+public abstract class RetryingRestModifyView<R extends RestResource, I, O>
+    implements RestModifyView<R, I> {
+  private final RetryHelper retryHelper;
+
+  protected RetryingRestModifyView(RetryHelper retryHelper) {
+    this.retryHelper = retryHelper;
+  }
+
+  @Override
+  public final O apply(R resource, I input) throws Exception {
+    return retryHelper.execute((updateFactory) -> applyImpl(updateFactory, resource, input));
+  }
+
+  protected abstract O applyImpl(BatchUpdate.Factory updateFactory, R resource, I input)
+      throws Exception;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
index 95ed053..aaf7486 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
@@ -412,6 +412,9 @@
     // TODO(dborowitz): Really?
     initRepository();
     batchRefUpdate = repoView.getRepository().getRefDatabase().newBatchUpdate();
+    batchRefUpdate.setPushCertificate(pushCert);
+    batchRefUpdate.setRefLogMessage(refLogMessage, true);
+    batchRefUpdate.setAllowNonFastForwards(true);
     repoView.getCommands().addTo(batchRefUpdate);
     logDebug("Executing batch of {} ref updates", batchRefUpdate.getCommands().size());
     if (dryrun) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java
index 2f7de46..ab4b701 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java
@@ -332,6 +332,9 @@
     // May not be opened if the caller added ref updates but no new objects.
     initRepository();
     batchRefUpdate = repoView.getRepository().getRefDatabase().newBatchUpdate();
+    batchRefUpdate.setPushCertificate(pushCert);
+    batchRefUpdate.setRefLogMessage(refLogMessage, true);
+    batchRefUpdate.setAllowNonFastForwards(true);
     repoView.getCommands().addTo(batchRefUpdate);
     logDebug("Executing batch of {} ref updates", batchRefUpdate.getCommands().size());
     if (dryrun) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
index 0ee1c28..86209fe 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -14,17 +14,14 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.Index;
-import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import java.io.IOException;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import org.kohsuke.args4j.Argument;
@@ -58,7 +55,7 @@
     for (ChangeResource rsrc : changes.values()) {
       try {
         index.apply(rsrc, new Index.Input());
-      } catch (IOException | RestApiException | OrmException | PermissionBackendException e) {
+      } catch (Exception e) {
         ok = false;
         writeError(
             "error", String.format("failed to index change %s: %s", rsrc.getId(), e.getMessage()));
diff --git a/polygerrit-ui/.eslintrc.json b/polygerrit-ui/.eslintrc.json
new file mode 100644
index 0000000..8c4ee99
--- /dev/null
+++ b/polygerrit-ui/.eslintrc.json
@@ -0,0 +1,70 @@
+{
+  "extends": ["eslint:recommended", "google"],
+  "installedESLint": true,
+  "env": {
+    "browser": true,
+    "es6": true
+  },
+  "globals": {
+    "__dirname": false,
+    "app": false,
+    "page": false,
+    "Polymer": false,
+    "process": false,
+    "require": false,
+    "Gerrit": false,
+    "Promise": false,
+    "assert": false,
+    "test": false,
+    "flushAsynchronousOperations": false
+  },
+  "rules": {
+    "arrow-parens": ["error", "as-needed"],
+    "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
+    "camelcase": "off",
+    "comma-dangle": ["error", "always-multiline"],
+    "eol-last": "off",
+    "indent": ["error", 2, {
+      "MemberExpression": 2,
+      "FunctionDeclaration": {"body": 1, "parameters": 2},
+      "FunctionExpression": {"body": 1, "parameters": 2},
+      "CallExpression": {"arguments": 2},
+      "ArrayExpression": 1,
+      "ObjectExpression": 1,
+      "SwitchCase": 1
+    }],
+    "max-len": [
+      "error",
+      80,
+      2,
+      {"ignoreComments": true}
+    ],
+    "new-cap": ["error", { "capIsNewExceptions": ["Polymer"] }],
+    "no-console": "off",
+    "no-restricted-syntax": [
+      "error",
+      {
+        "selector": "BinaryExpression > CallExpression > MemberExpression > Identifier[name = 'indexOf']",
+        "message": "Prefer includes/startsWith to indexOf."
+      },
+      {
+        "selector": "ExpressionStatement > CallExpression > MemberExpression > Identifier[name = 'forEach']",
+        "message": "Prefer for-of to Array.forEach."
+      }
+    ],
+    "no-undef": "off",
+    "no-var": "error",
+    "object-shorthand": ["error", "always"],
+    "prefer-arrow-callback": "error",
+    "prefer-const": "error",
+    "prefer-spread": "error",
+    "quote-props": ["error", "consistent-as-needed"],
+    "require-jsdoc": "off",
+    "semi": [2, "always"],
+    "template-curly-spacing": "error",
+    "valid-jsdoc": "off"
+  },
+  "plugins": [
+    "html"
+  ]
+}
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 77d5781..0fdbd44 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -116,3 +116,22 @@
 
 We follow the [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml)
 with a few exceptions. When in doubt, remain consistent with the code around you.
+
+In addition, we encourage the use of [ESLint](http://eslint.org/).
+It is available as a command line utility, as well as a plugin for most editors
+and IDEs. It, along with a few dependencies, can also be installed through NPM:
+
+```sh
+sudo npm install -g eslint eslint-config-google eslint-plugin-html
+```
+
+`eslint-config-google` is a port of the Google JS Style Guide to an ESLint
+config module, and `eslint-plugin-html` allows ESLint to lint scripts inside
+HTML.
+We have an .eslintrc.json config file in the polygerrit-ui/ directory configured
+to enforce the preferred style of the PolyGerrit project.
+After installing, you can use `eslint` on any new file you create.
+In addition, you can supply the `--fix` flag to apply some suggested fixes for
+simple style issues.
+If you modify JS inside of `<script>` tags, like for test suites, you may have
+to supply the `--ext .html` flag.
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index a51632a..31721a0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -47,8 +47,10 @@
   var ChangeActions = {
     ABANDON: 'abandon',
     DELETE: '/',
+    IGNORE: 'ignore',
     RESTORE: 'restore',
     REVERT: 'revert',
+    UNIGNORE: 'unignore',
   };
 
   // TODO(andybons): Add the rest of the revision actions.
@@ -206,6 +208,14 @@
               type: ActionType.REVISION,
               key: RevisionActions.DOWNLOAD,
             },
+            {
+              type: ActionType.CHANGE,
+              key: ChangeActions.IGNORE,
+            },
+            {
+              type: ActionType.CHANGE,
+              key: ChangeActions.UNIGNORE,
+            },
           ];
           return value;
         },
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index fa56bb5..4721fdf 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -557,6 +557,89 @@
       });
     });
 
+    suite('ignore change', function() {
+      var fireActionStub;
+
+      setup(function(done) {
+        fireActionStub = sandbox.stub(element, '_fireAction');
+
+        var IgnoreAction = {
+          __key: 'ignore',
+          __type: 'change',
+          __primary: false,
+          method: 'PUT',
+          label: 'Ignore',
+          title: 'Working...',
+          enabled: true,
+        };
+
+        element.actions = {
+          ignore: IgnoreAction,
+        };
+
+        element.changeNum = '2';
+        element.patchNum = '2';
+
+        element.reload().then(function() {flush(done)});
+      });
+
+      test('make sure the ignore button is not outside of the overflow menu',
+          function() {
+        assert.isNotOk(element.$$('[data-action-key="ignore"]'));
+      });
+
+      test('ignoring change', function() {
+        assert.isOk(element.$.moreActions.$$('span[data-id="ignore-change"]'));
+        element.setActionOverflow('change', 'ignore', false);
+        flushAsynchronousOperations();
+        assert.isOk(element.$$('[data-action-key="ignore"]'));
+        assert.isNotOk(
+            element.$.moreActions.$$('span[data-id="ignore-change"]'));
+      });
+    });
+
+    suite('unignore change', function() {
+      var fireActionStub;
+
+      setup(function(done) {
+        fireActionStub = sandbox.stub(element, '_fireAction');
+
+        var UnignoreAction = {
+          __key: 'unignore',
+          __type: 'change',
+          __primary: false,
+          method: 'PUT',
+          label: 'Unignore',
+          title: 'Working...',
+          enabled: true,
+        };
+
+        element.actions = {
+          unignore: UnignoreAction,
+        };
+
+        element.changeNum = '2';
+        element.patchNum = '2';
+
+        element.reload().then(function() {flush(done)});
+      });
+
+
+      test('unignore button is not outside of the overflow menu', function() {
+        assert.isNotOk(element.$$('[data-action-key="unignore"]'));
+      });
+
+      test('unignoring change', function() {
+        assert.isOk(
+          element.$.moreActions.$$('span[data-id="unignore-change"]'));
+        element.setActionOverflow('change', 'unignore', false);
+        flushAsynchronousOperations();
+        assert.isOk(element.$$('[data-action-key="unignore"]'));
+        assert.isNotOk(
+          element.$.moreActions.$$('span[data-id="unignore-change"]'));
+      });
+    });
+
     suite('quick approve', function() {
       setup(function() {
         element.change = {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
index 32b0a4d..1c5dd59 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -22,40 +22,10 @@
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="../../plugins/gr-plugin-host/gr-plugin-host.html">
 <link rel="import" href="gr-change-metadata.html">
 
-<test-fixture id="plugin">
-  <template>
-    <script>
-      Gerrit.install(function(plugin) {
-        plugin.registerStyleModule('change-metadata', 'some-style');
-      }, '', 'http://x/plugins/foo.js');
-    </script>
-  </template>
-</test-fixture>
-
-<test-fixture id="some-style">
-  <template>
-    <dom-module id="some-style">
-      <style>
-        :root {
-          --change-metadata-assignee: {
-            display: none;
-          }
-          --change-metadata-label-status: {
-            display: none;
-          }
-          --change-metadata-strategy: {
-            display: none;
-          }
-          --change-metadata-topic: {
-            display: none;
-          }
-        }
-      </style>
-    </dom-module>
-  </template>
-</test-fixture>
+<script>void(0);</script>
 
 <test-fixture id="element">
   <template>
@@ -63,10 +33,15 @@
   </template>
 </test-fixture>
 
+<test-fixture id="plugin-host">
+  <template>
+    <gr-plugin-host></gr-plugin-host>
+  </template>
+</test-fixture>
+
 <script>
   suite('gr-change-metadata integration tests', function() {
     var sandbox;
-    var plugin;
     var element;
 
     var sectionSelectors = [
@@ -82,19 +57,20 @@
     };
 
     setup(function() {
-      Gerrit._pluginsPending = 0;
       sandbox = sinon.sandbox.create();
       stub('gr-change-metadata', {
         _computeShowLabelStatus: function() { return true; },
         _computeShowReviewersByState: function() { return true; },
         ready: function() {
-          this.change = {labels:[]};
+          this.change = {labels: []};
           this.serverConfig = {};
         },
       });
     });
 
     teardown(function() {
+      Gerrit._pluginsPending = -1;
+      Gerrit._allPluginsPromise = undefined;
       sandbox.restore();
     });
 
@@ -106,26 +82,32 @@
 
       sectionSelectors.forEach(function(sectionSelector) {
         test(sectionSelector + ' does not have display: none', function() {
-          assert.notEqual(
-              getStyle(sectionSelector, 'display'), 'none');
-          });
+          assert.notEqual(getStyle(sectionSelector, 'display'), 'none');
+        });
       });
     });
 
     suite('with plugin style', function() {
-      var styleEl;
-
       setup(function(done) {
-        styleEl = fixture('some-style');
+        var pluginHost = fixture('plugin-host');
+        pluginHost.config = {
+          js_resource_paths: [],
+          html_resource_paths: [
+            new URL('test/plugin.html', window.location.href).toString(),
+          ]
+        };
         element = fixture('element');
-        plugin = fixture('plugin');
-        flush(done);
+        var importSpy = sandbox.spy(element.$.externalStyle, '_import');
+        Gerrit.awaitPluginsLoaded().then(function() {
+          Promise.all(importSpy.returnValues).then(function() {
+            flush(done);
+          });
+        });
       });
 
       sectionSelectors.forEach(function(sectionSelector) {
         test(sectionSelector + ' may have display: none', function() {
-          assert.equal(
-              getStyle(sectionSelector, 'display'), 'none');
+          assert.equal(getStyle(sectionSelector, 'display'), 'none');
         });
       });
     });
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 2feab27..778b09c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -78,6 +78,11 @@
       .webLink {
         display: block;
       }
+      #missingLabels {
+        padding-left: 1.5em;
+      }
+
+      /* CSS Mixins should be applied last. */
       section.assignee {
         @apply(--change-metadata-assignee);
       }
@@ -90,9 +95,6 @@
       section.topic {
         @apply(--change-metadata-topic);
       }
-      #missingLabels {
-        padding-left: 1.5em;
-      }
       @media screen and (max-width: 50em), screen and (min-width: 75em) {
         :host {
           display: table;
@@ -114,7 +116,7 @@
         }
       }
     </style>
-    <gr-external-style name="change-metadata">
+    <gr-external-style id="externalStyle" name="change-metadata">
       <section>
         <span class="title">Updated</span>
         <span class="value">
@@ -163,7 +165,7 @@
         </section>
       </template>
       <template is="dom-if" if="[[!_showReviewersByState]]">
-        <section>
+        <section class="assignee">
           <span class="title">Assignee</span>
           <span class="value">
             <gr-account-list
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/test/plugin.html b/polygerrit-ui/app/elements/change/gr-change-metadata/test/plugin.html
new file mode 100644
index 0000000..1cd3138
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/test/plugin.html
@@ -0,0 +1,26 @@
+<dom-module id="my-plugin">
+  <script>
+    Gerrit.install(function(plugin) {
+      plugin.registerStyleModule('change-metadata', 'my-plugin-style');
+    });
+  </script>
+</dom-module>
+
+<dom-module id="my-plugin-style">
+  <style>
+    html {
+      --change-metadata-assignee: {
+        display: none;
+      }
+      --change-metadata-label-status: {
+        display: none;
+      }
+      --change-metadata-strategy: {
+        display: none;
+      }
+      --change-metadata-topic: {
+        display: none;
+      }
+    }
+  </style>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 1a7765c..4a50dbb 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -1177,10 +1177,14 @@
       this._updateCheckTimerHandle = this.async(function() {
         this.fetchIsLatestKnown(this._change, this.$.restAPI)
             .then(function(latest) {
-              if (!latest) {
+              if (latest) {
+                this._startUpdateCheckTimer();
+              } else {
                 this._cancelUpdateCheckTimer();
                 this.fire('show-alert', {
                   message: 'A newer patch has been uploaded.',
+                  // Persist this alert.
+                  dismissOnNavigation: true,
                   action: 'Reload',
                   callback: function() {
                     // Load the current change without any patch range.
@@ -1189,7 +1193,6 @@
                   }.bind(this),
                 });
               }
-              this._startUpdateCheckTimer();
             }.bind(this));
       }, this.serverConfig.change.update_delay * 1000);
     },
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 27dcd4a..ab5ecc2 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -96,7 +96,8 @@
     },
 
     _handleShowAlert: function(e) {
-      this._showAlert(e.detail.message, e.detail.action, e.detail.callback);
+      this._showAlert(e.detail.message, e.detail.action, e.detail.callback,
+          e.detail.dismissOnNavigation);
     },
 
     _handleNetworkError: function(e) {
@@ -108,12 +109,18 @@
       return this.$.restAPI.getLoggedIn();
     },
 
-    _showAlert: function(text, opt_actionText, opt_actionCallback) {
+    _showAlert: function(text, opt_actionText, opt_actionCallback,
+        dismissOnNavigation) {
       if (this._alertElement) { return; }
 
       this._clearHideAlertHandle();
-      this._hideAlertHandle =
-        this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS);
+      if (dismissOnNavigation) {
+        // Persist alert until navigation.
+        this.listen(document, 'location-change', '_hideAlert');
+      } else {
+        this._hideAlertHandle =
+          this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS);
+      }
       var el = this._createToastAlert();
       el.show(text, opt_actionText, opt_actionCallback);
       this._alertElement = el;
@@ -124,6 +131,9 @@
 
       this._alertElement.hide();
       this._alertElement = null;
+
+      // Remove listener for page navigation, if it exists.
+      this.unlisten(document, 'location-change', '_hideAlert');
     },
 
     _clearHideAlertHandle: function() {
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index c8743fb..83d4a7b 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -272,5 +272,21 @@
         done();
       });
     });
+
+    test('dismissOnNavigation respected', function() {
+      var asyncStub = sandbox.stub(element, 'async');
+      var hideSpy = sandbox.spy(element, '_hideAlert');
+      // No async call when dismissOnNavigation supplied.
+      element._showAlert('test', null, null, true);
+      assert.isFalse(asyncStub.called);
+
+      // When page nav happens, clear alert.
+      document.dispatchEvent(new CustomEvent('location-change'));
+      assert.isTrue(hideSpy.called);
+
+      // When timeout is not supplied, use HIDE_ALERT_TIMEOUT_MS.
+      element._showAlert('test');
+      assert.isTrue(asyncStub.called);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 1e6596b..265e970 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -51,7 +51,7 @@
         position: relative;
       }
       .linksTitle {
-        color: black;
+        color: var(--primary-text-color);
         display: inline-block;
         position: relative;
       }
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index affa8e5..ab042c4 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -244,7 +244,7 @@
 
       this.$.restAPI.getPreferences().then(function(prefs) {
         this._userLinks =
-            prefs.my.map(this._stripHashPrefix).filter(this._isSupportedLink);
+            prefs.my.map(this._fixMyMenuItem).filter(this._isSupportedLink);
       }.bind(this));
       this._loadAccountCapabilities();
     },
@@ -260,10 +260,20 @@
       }.bind(this));
     },
 
-    _stripHashPrefix: function(linkObj) {
+    _fixMyMenuItem: function(linkObj) {
+      // Normalize all urls to PolyGerrit style.
       if (linkObj.url.indexOf('#') === 0) {
         linkObj.url = linkObj.url.slice(1);
       }
+
+      // Delete target property due to complications of
+      // https://bugs.chromium.org/p/gerrit/issues/detail?id=5888
+      //
+      // The server tries to guess whether URL is a view within the UI.
+      // If not, it sets target='_blank' on the menu item. The server
+      // makes assumptions that work for the GWT UI, but not PolyGerrit,
+      // so we'll just disable it altogether for now.
+      delete linkObj.target;
       return linkObj;
     },
 
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
index 4582b4f..94f1f66 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -53,14 +53,16 @@
       sandbox.restore();
     });
 
-    test('strip hash prefix', function() {
+    test('fix my menu item', function() {
       assert.deepEqual([
         {url: '#/q/owner:self+is:draft'},
         {url: 'https://awesometown.com/#hashyhash'},
-      ].map(element._stripHashPrefix),
+        {url: 'url', target: '_blank'},
+      ].map(element._fixMyMenuItem),
       [
         {url: '/q/owner:self+is:draft'},
         {url: 'https://awesometown.com/#hashyhash'},
+        {url: 'url'},
       ]);
     });
 
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 1e3481f..cc8d478 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -33,16 +33,16 @@
     getReporting().pageLoaded();
   };
 
-  var base = window.Gerrit.BaseUrlBehavior.getBaseUrl();
-  if (base) {
-    page.base(base);
-  }
-
   window.addEventListener('WebComponentsReady', function() {
     getReporting().timeEnd('WebComponentsReady');
   });
 
   function startRouter() {
+    var base = window.Gerrit.BaseUrlBehavior.getBaseUrl();
+    if (base) {
+      page.base(base);
+    }
+
     var restAPI = document.createElement('gr-rest-api-interface');
     var reporting = getReporting();
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index 62e8915..30d01cb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -36,11 +36,11 @@
    * The maximum size for an addition or removal chunk before it is broken down
    * into a series of chunks that are this size at most.
    *
-   * Note: The value of 70 is chosen so that it is larger than the default
+   * Note: The value of 120 is chosen so that it is larger than the default
    * _asyncThreshold of 64, but feel free to tune this constant to your
    * performance needs.
    */
-  var MAX_GROUP_SIZE = 70;
+  var MAX_GROUP_SIZE = 120;
 
   Polymer({
     is: 'gr-diff-processor',
@@ -364,8 +364,11 @@
       if (this.context === -1) {
         var newContent = [];
         content.forEach(function(group) {
-          if (group.ab) {
-            newContent.push.apply(newContent, this._breakdownGroup(group));
+          if (group.ab && group.ab.length > MAX_GROUP_SIZE * 2) {
+            // Split large shared groups in two, where the first is the maximum
+            // group size.
+            newContent.push({ab: group.ab.slice(0, MAX_GROUP_SIZE)});
+            newContent.push({ab: group.ab.slice(MAX_GROUP_SIZE)});
           } else {
             newContent.push(group);
           }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index 687b3dd..1a5ccc5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -337,15 +337,16 @@
       });
 
       test('breaks-down shared chunks w/ whole-file', function() {
+        var size = 120 * 2 + 5;
         var lineNums = {left: 0, right: 0};
         var content = [{
-          ab: _.times(75, function() { return '' + Math.random(); }),
+          ab: _.times(size, function() { return '' + Math.random(); }),
         }];
         element.context = -1;
         var result = element._splitCommonGroupsWithComments(content, lineNums);
         assert.equal(result.length, 2);
-        assert.deepEqual(result[0].ab, content[0].ab.slice(0, 5));
-        assert.deepEqual(result[1].ab, content[0].ab.slice(5));
+        assert.deepEqual(result[0].ab, content[0].ab.slice(0, 120));
+        assert.deepEqual(result[1].ab, content[0].ab.slice(120));
       });
 
       test('does not break-down shared chunks w/ context', function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index c267eb0..5868f77 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -331,7 +331,10 @@
               function() { return Promise.resolve(mockFile1); }));
           stubs.push(sandbox.stub(element.$.restAPI,
               'getChangeFileContents',
-              function() { return Promise.resolve(mockFile2); }));
+              function(changeId, patchNum, path, opt_parentIndex) {
+                return Promise.resolve(opt_parentIndex === 1 ? mockFile1 :
+                    mockFile2);
+              }));
           stubs.push(sandbox.stub(element.$.restAPI, '_getDiffComments',
               function() { return Promise.resolve(mockComments); }));
           stubs.push(sandbox.stub(element.$.restAPI, 'getDiffDrafts',
diff --git a/polygerrit-ui/app/elements/gr-app-it_test.html b/polygerrit-ui/app/elements/gr-app-it_test.html
new file mode 100644
index 0000000..66b0f8f
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app-it_test.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-app-it_test</title>
+
+<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-app.html">
+
+<script>void(0);</script>
+
+<test-fixture id="element">
+  <template>
+    <gr-app id="app"></gr-app>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-app integration tests', function() {
+    var sandbox;
+    var element;
+
+    setup(function(done) {
+      sandbox = sinon.sandbox.create();
+      stub('gr-reporting', {
+        appStarted: sandbox.stub(),
+      });
+      stub('gr-account-dropdown', {
+        _getTopContent: sinon.stub(),
+      });
+      stub('gr-rest-api-interface', {
+        getAccount: function() { return Promise.resolve(null); },
+        getAccountCapabilities: function() { return Promise.resolve({}); },
+        getConfig: function() {
+          return Promise.resolve({
+            gerrit: {web_uis: ['GWT', 'POLYGERRIT']},
+            plugin: {
+              js_resource_paths: [],
+              html_resource_paths: [
+                new URL('test/plugin.html', window.location.href).toString()
+              ]
+            },
+          });
+        },
+        getVersion: function() { return Promise.resolve(42); },
+        getLoggedIn: function() { return Promise.resolve(false); },
+      });
+      element = fixture('element');
+
+      var importSpy = sandbox.spy(element.$.externalStyle, '_import');
+      Gerrit.awaitPluginsLoaded().then(function() {
+        Promise.all(importSpy.returnValues).then(function() {
+          flush(done);
+        });
+      });
+    });
+
+    teardown(function() {
+      sandbox.restore();
+    });
+
+    test('applies --primary-text-color', function() {
+      assert.equal(
+          element.getComputedStyleValue('--primary-text-color'), '#F00BAA');
+    });
+
+    test('applies --header-background-color', function() {
+      assert.equal(element.getComputedStyleValue('--header-background-color'),
+          '#F01BAA');
+    });
+    test('applies --footer-background-color', function() {
+      assert.equal(element.getComputedStyleValue('--footer-background-color'),
+          '#F02BAA');
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index bc88b14..6f09701 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -14,28 +14,25 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../styles/app-theme.html">
-<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
-
 <link rel="import" href="./admin/gr-admin-view/gr-admin-view.html">
-
-<link rel="import" href="./core/gr-error-manager/gr-error-manager.html">
-<link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
-<link rel="import" href="./core/gr-main-header/gr-main-header.html">
-<link rel="import" href="./core/gr-router/gr-router.html">
-<link rel="import" href="./core/gr-reporting/gr-reporting.html">
-
 <link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html">
 <link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html">
 <link rel="import" href="./change/gr-change-view/gr-change-view.html">
+<link rel="import" href="./core/gr-error-manager/gr-error-manager.html">
+<link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
+<link rel="import" href="./core/gr-main-header/gr-main-header.html">
+<link rel="import" href="./core/gr-reporting/gr-reporting.html">
+<link rel="import" href="./core/gr-router/gr-router.html">
 <link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
+<link rel="import" href="./plugins/gr-external-style/gr-external-style.html">
+<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
 <link rel="import" href="./settings/gr-cla-view/gr-cla-view.html">
 <link rel="import" href="./settings/gr-registration-dialog/gr-registration-dialog.html">
 <link rel="import" href="./settings/gr-settings-view/gr-settings-view.html">
-
 <link rel="import" href="./shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
@@ -54,11 +51,11 @@
         color: var(--primary-text-color);
       }
       gr-main-header {
-        background-color: var(--header-background-color, #eee);
+        background-color: var(--header-background-color);
         padding: 0 var(--default-horizontal-margin);
       }
       footer {
-        background-color: var(--footer-background-color, #eee);
+        background-color: var(--footer-background-color);
         display: flex;
         justify-content: space-between;
         padding: .5rem var(--default-horizontal-margin);
@@ -175,6 +172,7 @@
     <gr-plugin-host id="plugins"
         config="[[_serverConfig.plugin]]">
     </gr-plugin-host>
+    <gr-external-style id="externalStyle" name="app-theme"></gr-external-style>
   </template>
   <script src="gr-app.js" crossorigin="anonymous"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
index 94bc534..162921a 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -21,6 +21,12 @@
       name: String,
     },
 
+    _import: function(url) {
+      return new Promise(function(resolve, reject) {
+        this.importHref(url, resolve, reject);
+      }.bind(this));
+    },
+
     _applyStyle: function(name) {
       var s = document.createElement('style', 'custom-style');
       s.setAttribute('include', name);
@@ -31,7 +37,21 @@
       Gerrit.awaitPluginsLoaded().then(function() {
         var sharedStyles = Gerrit._styleModules[this.name];
         if (sharedStyles) {
-          sharedStyles.map(this._applyStyle.bind(this));
+          var pluginUrls = [];
+          var moduleNames = [];
+          sharedStyles.reduce(function(result, item) {
+            if (!result.pluginUrls.includes(item.pluginUrl)) {
+              result.pluginUrls.push(item.pluginUrl);
+            }
+            result.moduleNames.push(item.moduleName);
+            return result;
+          }, {pluginUrls: pluginUrls, moduleNames: moduleNames});
+          Promise.all(pluginUrls.map(this._import.bind(this)))
+            .then(function() {
+              moduleNames.forEach(function(name) {
+                this._applyStyle(name);
+              }.bind(this));
+            }.bind(this));
         }
       }.bind(this));
     },
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
index 9c9ab35..f626f71 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
@@ -35,23 +35,27 @@
     var sandbox;
     var element;
 
-    setup(function() {
+    setup(function(done) {
       sandbox = sinon.sandbox.create();
       sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
+      Gerrit._styleModules = {'foo': [{pluginUrl: 'bar', moduleName: 'baz',}]};
+
       element = fixture('basic');
+      sandbox.stub(element, '_applyStyle');
+      sandbox.stub(element, 'importHref', function(url, resolve) { resolve() });
+      flush(done);
     });
 
     teardown(function() {
       sandbox.restore();
     });
 
-    test('applies plugin-provided styles', function(done) {
-      Gerrit._styleModules = {'foo': ['bar']};
-      sandbox.stub(element, '_applyStyle');
-      flush(function() {
-        assert.isTrue(element._applyStyle.calledWith('bar'));
-        done();
-      });
+    test('imports plugin-provided module', function() {
+      assert.isTrue(element.importHref.calledWith('bar'));
+    });
+
+    test('applies plugin-provided styles', function() {
+      assert.isTrue(element._applyStyle.calledWith('baz'));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index ccfe604..1c78689 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -34,7 +34,11 @@
 
     _importHtmlPlugins: function(plugins) {
       plugins.forEach(function(url) {
-        this.importHref('/' + url, null, Gerrit._pluginInstalled, true);
+        if (url.indexOf('http') !== 0) {
+          url = '/' + url;
+        }
+        this.importHref(
+            url, Gerrit._pluginInstalled, Gerrit._pluginInstalled, true);
       }.bind(this));
     },
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
index b0c7c71..5965bc3 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -59,9 +59,9 @@
         html_resource_paths: ['foo/bar', 'baz'],
       };
       assert.isTrue(element.importHref.calledWith(
-          '/foo/bar', null, Gerrit._pluginInstalled, true));
+          '/foo/bar', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
       assert.isTrue(element.importHref.calledWith(
-          '/baz', null, Gerrit._pluginInstalled, true));
+          '/baz', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 3c8c4a0..402b371 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -59,7 +59,10 @@
     if (!Gerrit._styleModules[stylingEndpointName]) {
       Gerrit._styleModules[stylingEndpointName] = [];
     }
-    Gerrit._styleModules[stylingEndpointName].push(moduleName);
+    Gerrit._styleModules[stylingEndpointName].push({
+      pluginUrl: this._url,
+      moduleName: moduleName,
+    });
   };
 
   Plugin.prototype.getServerInfo = function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index dab22d6..d7e3328 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -963,22 +963,25 @@
     },
 
     _fetchB64File: function(url) {
-      return fetch(this.getBaseUrl() + url, {credentials: 'same-origin'}).then(
-            function(response) {
-        var type = response.headers.get('X-FYI-Content-Type');
-        return response.text()
-          .then(function(text) {
-            return {body: text, type: type};
+      return fetch(this.getBaseUrl() + url, {credentials: 'same-origin'})
+          .then(function(response) {
+            if (!response.ok) { return Promise.reject(response.statusText); }
+            var type = response.headers.get('X-FYI-Content-Type');
+            return response.text()
+              .then(function(text) {
+                return {body: text, type: type};
+              });
           });
-      });
     },
 
-    getChangeFileContents: function(changeId, patchNum, path) {
+    getChangeFileContents: function(changeId, patchNum, path, opt_parentIndex) {
+      var parent = typeof opt_parentIndex === 'number' ?
+          '?parent=' + opt_parentIndex : '';
       return this._fetchB64File(
           '/changes/' + encodeURIComponent(changeId) +
           '/revisions/' + encodeURIComponent(patchNum) +
           '/files/' + encodeURIComponent(path) +
-          '/content');
+          '/content' + parent);
     },
 
     getCommitFileContents: function(projectName, commit, path) {
@@ -995,16 +998,17 @@
 
       if (diff.meta_a && diff.meta_a.content_type.indexOf('image/') === 0) {
         if (patchRange.basePatchNum === 'PARENT') {
-          // Need the commit info know the parent SHA.
-          promiseA = this.getCommitInfo(project, commit).then(function(info) {
-            if (info.parents.length !== 1) {
-              return Promise.reject('Change commit has multiple parents.');
-            }
-            var parent = info.parents[0].commit;
-            return this.getCommitFileContents(project, parent,
-                diff.meta_a.name);
-          }.bind(this));
-
+          // Note: we only attempt to get the image from the first parent.
+          promiseA = this.getChangeFileContents(changeNum, patchRange.patchNum,
+              diff.meta_a.name, 1)
+              .catch(function(result) {
+                // If getting the parent-indexed version of the image fails, it
+                // may be because the API has not been rolled out. Fall back to
+                // getting the file from the commit using the slow API.
+                // NOTE(wyatta): Remove this when the rollout is complete.
+                return this._getImageFromCommit(project, commit,
+                    diff.meta_a.name);
+              }.bind(this));
         } else {
           promiseA = this.getChangeFileContents(changeNum,
               patchRange.basePatchNum, diff.meta_a.name);
@@ -1039,6 +1043,19 @@
         }.bind(this));
     },
 
+    /**
+     * Remove when parent-indexed file requests are completely rolled out.
+     */
+    _getImageFromCommit: function(project, commit, path) {
+      return this.getCommitInfo(project, commit).then(function(info) {
+        if (info.parents.length !== 1) {
+          return Promise.reject('Change commit has multiple parents.');
+        }
+        var parent = info.parents[0].commit;
+        return this.getCommitFileContents(project, parent, path);
+      }.bind(this));
+    },
+
     setChangeTopic: function(changeNum, topic) {
       return this.send('PUT', '/changes/' + encodeURIComponent(changeNum) +
           '/topic', {topic: topic});
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 0ff162d..f2f41b8 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -34,63 +34,63 @@
 </test-fixture>
 
 <script>
-  suite('gr-rest-api-interface tests', function() {
-    var element;
-    var sandbox;
+  suite('gr-rest-api-interface tests', () => {
+    let element;
+    let sandbox;
 
-    setup(function() {
+    setup(() => {
       sandbox = sinon.sandbox.create();
       element = fixture('basic');
-      var testJSON = ')]}\'\n{"hello": "bonjour"}';
+      const testJSON = ')]}\'\n{"hello": "bonjour"}';
       sandbox.stub(window, 'fetch').returns(Promise.resolve({
         ok: true,
-        text: function() {
+        text() {
           return Promise.resolve(testJSON);
         },
       }));
     });
 
-    teardown(function() {
+    teardown(() => {
       sandbox.restore();
     });
 
-    test('JSON prefix is properly removed', function(done) {
-      element.fetchJSON('/dummy/url').then(function(obj) {
+    test('JSON prefix is properly removed', done => {
+      element.fetchJSON('/dummy/url').then(obj => {
         assert.deepEqual(obj, {hello: 'bonjour'});
         done();
       });
     });
 
-    test('cached results', function(done) {
-      var n = 0;
-      sandbox.stub(element, 'fetchJSON', function() {
+    test('cached results', done => {
+      let n = 0;
+      sandbox.stub(element, 'fetchJSON', () => {
         return Promise.resolve(++n);
       });
-      var promises = [];
+      const promises = [];
       promises.push(element._fetchSharedCacheURL('/foo'));
       promises.push(element._fetchSharedCacheURL('/foo'));
       promises.push(element._fetchSharedCacheURL('/foo'));
 
-      Promise.all(promises).then(function(results) {
+      Promise.all(promises).then(results => {
         assert.deepEqual(results, [1, 1, 1]);
-        element._fetchSharedCacheURL('/foo').then(function(foo) {
+        element._fetchSharedCacheURL('/foo').then(foo => {
           assert.equal(foo, 1);
           done();
         });
       });
     });
 
-    test('cached promise', function(done) {
-      var promise = Promise.reject('foo');
+    test('cached promise', done => {
+      const promise = Promise.reject('foo');
       element._cache['/foo'] = promise;
-      element._fetchSharedCacheURL('/foo').catch(function(p) {
+      element._fetchSharedCacheURL('/foo').catch(p => {
         assert.equal(p, 'foo');
         done();
       });
     });
 
-    test('params are properly encoded', function() {
-      var url = element._urlWithParams('/path/', {
+    test('params are properly encoded', () => {
+      let url = element._urlWithParams('/path/', {
         sp: 'hola',
         gr: 'guten tag',
         noval: null,
@@ -110,23 +110,23 @@
       assert.equal(url, '/path/?l=c&l=b&l=a');
     });
 
-    test('request callbacks can be canceled', function(done) {
-      var cancelCalled = false;
+    test('request callbacks can be canceled', done => {
+      let cancelCalled = false;
       window.fetch.returns(Promise.resolve({
         body: {
-          cancel: function() { cancelCalled = true; },
+          cancel() { cancelCalled = true; },
         },
       }));
-      element.fetchJSON('/dummy/url', null, function() { return true; }).then(
-        function(obj) {
-          assert.isUndefined(obj);
-          assert.isTrue(cancelCalled);
-          done();
-        });
+      element.fetchJSON('/dummy/url', null, () => { return true; }).then(
+          obj => {
+            assert.isUndefined(obj);
+            assert.isTrue(cancelCalled);
+            done();
+          });
     });
 
-    test('parent diff comments are properly grouped', function(done) {
-      sandbox.stub(element, 'fetchJSON', function() {
+    test('parent diff comments are properly grouped', done => {
+      sandbox.stub(element, 'fetchJSON', () => {
         return Promise.resolve({
           '/COMMIT_MSG': [],
           'sieve.go': [
@@ -143,26 +143,26 @@
         });
       });
       element._getDiffComments('42', '', 'PARENT', 1, 'sieve.go').then(
-        function(obj) {
-          assert.equal(obj.baseComments.length, 1);
-          assert.deepEqual(obj.baseComments[0], {
-            side: 'PARENT',
-            message: 'how did this work in the first place?',
-            path: 'sieve.go',
-            updated: '2017-02-03 22:33:28.000000000',
+          obj => {
+            assert.equal(obj.baseComments.length, 1);
+            assert.deepEqual(obj.baseComments[0], {
+              side: 'PARENT',
+              message: 'how did this work in the first place?',
+              path: 'sieve.go',
+              updated: '2017-02-03 22:33:28.000000000',
+            });
+            assert.equal(obj.comments.length, 1);
+            assert.deepEqual(obj.comments[0], {
+              message: 'this isn’t quite right',
+              path: 'sieve.go',
+              updated: '2017-02-03 22:32:28.000000000',
+            });
+            done();
           });
-          assert.equal(obj.comments.length, 1);
-          assert.deepEqual(obj.comments[0], {
-            message: 'this isn’t quite right',
-            path: 'sieve.go',
-            updated: '2017-02-03 22:32:28.000000000',
-          });
-          done();
-        });
     });
 
-    test('_setRange', function() {
-      var comments = [
+    test('_setRange', () => {
+      const comments = [
         {
           id: 1,
           side: 'PARENT',
@@ -182,7 +182,7 @@
           updated: '2017-02-03 22:33:28.000000000',
         },
       ];
-      var expectedResult = {
+      const expectedResult = {
         id: 2,
         in_reply_to: 1,
         message: 'this isn’t quite right',
@@ -194,12 +194,12 @@
           end_character: 1,
         },
       };
-      var comment = comments[1];
+      const comment = comments[1];
       assert.deepEqual(element._setRange(comments, comment), expectedResult);
     });
 
-    test('_setRanges', function() {
-      var comments = [
+    test('_setRanges', () => {
+      const comments = [
         {
           id: 3,
           in_reply_to: 2,
@@ -225,7 +225,7 @@
           },
         },
       ];
-      var expectedResult = [
+      const expectedResult = [
         {
           id: 1,
           side: 'PARENT',
@@ -266,8 +266,8 @@
       assert.deepEqual(element._setRanges(comments), expectedResult);
     });
 
-    test('differing patch diff comments are properly grouped', function(done) {
-      sandbox.stub(element, 'fetchJSON', function(url) {
+    test('differing patch diff comments are properly grouped', done => {
+      sandbox.stub(element, 'fetchJSON', url => {
         if (url == '/changes/42/revisions/1') {
           return Promise.resolve({
             '/COMMIT_MSG': [],
@@ -305,29 +305,29 @@
         }
       });
       element._getDiffComments('42', '', 1, 2, 'sieve.go').then(
-        function(obj) {
-          assert.equal(obj.baseComments.length, 1);
-          assert.deepEqual(obj.baseComments[0], {
-            message: 'this isn’t quite right',
-            path: 'sieve.go',
-            updated: '2017-02-03 22:32:28.000000000',
+          obj => {
+            assert.equal(obj.baseComments.length, 1);
+            assert.deepEqual(obj.baseComments[0], {
+              message: 'this isn’t quite right',
+              path: 'sieve.go',
+              updated: '2017-02-03 22:32:28.000000000',
+            });
+            assert.equal(obj.comments.length, 2);
+            assert.deepEqual(obj.comments[0], {
+              message: 'What on earth are you thinking, here?',
+              path: 'sieve.go',
+              updated: '2017-02-03 22:32:28.000000000',
+            });
+            assert.deepEqual(obj.comments[1], {
+              message: '¯\\_(ツ)_/¯',
+              path: 'sieve.go',
+              updated: '2017-02-04 22:33:28.000000000',
+            });
+            done();
           });
-          assert.equal(obj.comments.length, 2);
-          assert.deepEqual(obj.comments[0], {
-            message: 'What on earth are you thinking, here?',
-            path: 'sieve.go',
-            updated: '2017-02-03 22:32:28.000000000',
-          });
-          assert.deepEqual(obj.comments[1], {
-            message: '¯\\_(ツ)_/¯',
-            path: 'sieve.go',
-            updated: '2017-02-04 22:33:28.000000000',
-          });
-          done();
-        });
     });
 
-    test('special file path sorting', function() {
+    test('special file path sorting', () => {
       assert.deepEqual(
           ['.b', '/COMMIT_MSG', '.a', 'file'].sort(
               element.specialFilePathCompare),
@@ -354,13 +354,13 @@
           ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']);
 
       // Regression test for Issue 4448.
-      assert.deepEqual([
-          'minidump/minidump_memory_writer.cc',
-          'minidump/minidump_memory_writer.h',
-          'minidump/minidump_thread_writer.cc',
-          'minidump/minidump_thread_writer.h',
-          ]
-        .sort(element.specialFilePathCompare),
+      assert.deepEqual(
+          [
+            'minidump/minidump_memory_writer.cc',
+            'minidump/minidump_memory_writer.h',
+            'minidump/minidump_thread_writer.cc',
+            'minidump/minidump_thread_writer.h',
+          ].sort(element.specialFilePathCompare),
           [
             'minidump/minidump_memory_writer.h',
             'minidump/minidump_memory_writer.cc',
@@ -369,102 +369,102 @@
           ]);
 
       // Regression test for Issue 4545.
-      assert.deepEqual([
-          'task_test.go',
-          'task.go',
-          ]
-        .sort(element.specialFilePathCompare),
+      assert.deepEqual(
+          [
+            'task_test.go',
+            'task.go',
+          ].sort(element.specialFilePathCompare),
           [
             'task.go',
             'task_test.go',
           ]);
     });
 
-    suite('rebase action', function() {
-      var resolveFetchJSON;
-      setup(function() {
+    suite('rebase action', () => {
+      let resolveFetchJSON;
+      setup(() => {
         sandbox.stub(element, 'fetchJSON').returns(
-            new Promise(function(resolve) {
+            new Promise(resolve => {
               resolveFetchJSON = resolve;
             }));
       });
 
-      test('no rebase on current', function(done) {
+      test('no rebase on current', done => {
         element.getChangeRevisionActions('42', '1337').then(
-          function(response) {
-            assert.isTrue(response.rebase.enabled);
-            assert.isFalse(response.rebase.rebaseOnCurrent);
-            done();
-          });
+            response => {
+              assert.isTrue(response.rebase.enabled);
+              assert.isFalse(response.rebase.rebaseOnCurrent);
+              done();
+            });
         resolveFetchJSON({rebase: {}});
       });
 
-      test('rebase on current', function(done) {
+      test('rebase on current', done => {
         element.getChangeRevisionActions('42', '1337').then(
-          function(response) {
-            assert.isTrue(response.rebase.enabled);
-            assert.isTrue(response.rebase.rebaseOnCurrent);
-            done();
-          });
+            response => {
+              assert.isTrue(response.rebase.enabled);
+              assert.isTrue(response.rebase.rebaseOnCurrent);
+              done();
+            });
         resolveFetchJSON({rebase: {enabled: true}});
       });
     });
 
 
-    test('server error', function(done) {
-      var getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
+    test('server error', done => {
+      const getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
       window.fetch.returns(Promise.resolve({ok: false}));
-      var serverErrorEventPromise = new Promise(function(resolve) {
-        element.addEventListener('server-error', function() { resolve(); });
+      const serverErrorEventPromise = new Promise(resolve => {
+        element.addEventListener('server-error', () => { resolve(); });
       });
 
       element.fetchJSON().then(
-          function(response) {
+          response => {
             assert.isUndefined(response);
             assert.isTrue(getResponseObjectStub.notCalled);
-            serverErrorEventPromise.then(function() {
+            serverErrorEventPromise.then(() => {
               done();
             });
           });
     });
 
-    test('checkCredentials', function(done) {
-      var responses = [
+    test('checkCredentials', done => {
+      const responses = [
         {
           ok: false,
           status: 403,
-          text: function() { return Promise.resolve(); },
+          text() { return Promise.resolve(); },
         },
         {
           ok: true,
           status: 200,
-          text: function() { return Promise.resolve(')]}\'{}'); },
+          text() { return Promise.resolve(')]}\'{}'); },
         },
       ];
       window.fetch.restore();
-      sandbox.stub(window, 'fetch', function(url) {
+      sandbox.stub(window, 'fetch', url => {
         if (url === '/accounts/self/detail') {
           return Promise.resolve(responses.shift());
         }
       });
 
-      element.getLoggedIn().then(function(account) {
+      element.getLoggedIn().then(account => {
         assert.isNotOk(account);
-        element.checkCredentials().then(function(account) {
+        element.checkCredentials().then(account => {
           assert.isOk(account);
           done();
         });
       });
     });
 
-    test('legacy n,z key in change url is replaced', function() {
-      var stub = sandbox.stub(element, 'fetchJSON');
+    test('legacy n,z key in change url is replaced', () => {
+      const stub = sandbox.stub(element, 'fetchJSON');
       element.getChanges(1, null, 'n,z');
       assert.equal(stub.args[0][3].S, 0);
     });
 
-    test('saveDiffPreferences invalidates cache line', function() {
-      var cacheKey = '/accounts/self/preferences.diff';
+    test('saveDiffPreferences invalidates cache line', () => {
+      const cacheKey = '/accounts/self/preferences.diff';
       sandbox.stub(element, 'send');
       element._cache[cacheKey] = {tab_size: 4};
       element.saveDiffPreferences({tab_size: 8});
@@ -472,108 +472,106 @@
       assert.notOk(element._cache[cacheKey]);
     });
 
-    var preferenceSetup = function(testJSON, loggedIn, smallScreen) {
-      sandbox.stub(element, 'getLoggedIn', function() {
+    const preferenceSetup = function(testJSON, loggedIn, smallScreen) {
+      sandbox.stub(element, 'getLoggedIn', () => {
         return Promise.resolve(loggedIn);
       });
-      sandbox.stub(element, '_isNarrowScreen', function() {
+      sandbox.stub(element, '_isNarrowScreen', () => {
         return smallScreen;
       });
-      sandbox.stub(element, '_fetchSharedCacheURL', function() {
+      sandbox.stub(element, '_fetchSharedCacheURL', () => {
         return Promise.resolve(testJSON);
       });
     };
 
     test('getPreferences returns correctly on small screens logged in',
-        function(done) {
+        done => {
+          const testJSON = {diff_view: 'SIDE_BY_SIDE'};
+          const loggedIn = true;
+          const smallScreen = true;
 
-      var testJSON = {diff_view: 'SIDE_BY_SIDE'};
-      var loggedIn = true;
-      var smallScreen = true;
+          preferenceSetup(testJSON, loggedIn, smallScreen);
 
-      preferenceSetup(testJSON, loggedIn, smallScreen);
-
-      element.getPreferences().then(function(obj) {
-        assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
-        assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
-        done();
-      });
-    });
+          element.getPreferences().then(obj => {
+            assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
+            assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
+            done();
+          });
+        });
 
     test('getPreferences returns correctly on small screens not logged in',
-          function(done) {
+        done => {
+          const testJSON = {diff_view: 'SIDE_BY_SIDE'};
+          const loggedIn = false;
+          const smallScreen = true;
 
-      var testJSON = {diff_view: 'SIDE_BY_SIDE'};
-      var loggedIn = false;
-      var smallScreen = true;
-
-      preferenceSetup(testJSON, loggedIn, smallScreen);
-      element.getPreferences().then(function(obj) {
-        assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
-        assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
-        done();
-      });
-    });
+          preferenceSetup(testJSON, loggedIn, smallScreen);
+          element.getPreferences().then(obj => {
+            assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
+            assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
+            done();
+          });
+        });
 
     test('getPreferences returns correctly on larger screens logged in',
-        function(done) {
-      var testJSON = {diff_view: 'UNIFIED_DIFF'};
-      var loggedIn = true;
-      var smallScreen = false;
+        done => {
+          const testJSON = {diff_view: 'UNIFIED_DIFF'};
+          const loggedIn = true;
+          const smallScreen = false;
 
-      preferenceSetup(testJSON, loggedIn, smallScreen);
+          preferenceSetup(testJSON, loggedIn, smallScreen);
 
-      element.getPreferences().then(function(obj) {
-        assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
-        assert.equal(obj.diff_view, 'UNIFIED_DIFF');
-        done();
-      });
-    });
+          element.getPreferences().then(obj => {
+            assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
+            assert.equal(obj.diff_view, 'UNIFIED_DIFF');
+            done();
+          });
+        });
 
     test('getPreferences returns correctly on larger screens not logged in',
-        function(done) {
-      var testJSON = {diff_view: 'UNIFIED_DIFF'};
-      var loggedIn = false;
-      var smallScreen = false;
+        done => {
+          const testJSON = {diff_view: 'UNIFIED_DIFF'};
+          const loggedIn = false;
+          const smallScreen = false;
 
-      preferenceSetup(testJSON, loggedIn, smallScreen);
+          preferenceSetup(testJSON, loggedIn, smallScreen);
 
-      element.getPreferences().then(function(obj) {
-        assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE');
-        assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
-        done();
-      });
-    });
+          element.getPreferences().then(obj => {
+            assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE');
+            assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
+            done();
+          });
+        });
 
-    test('savPreferences normalizes download scheme', function() {
+    test('savPreferences normalizes download scheme', () => {
       sandbox.stub(element, 'send');
       element.savePreferences({download_scheme: 'HTTP'});
       assert.isTrue(element.send.called);
       assert.equal(element.send.lastCall.args[2].download_scheme, 'http');
     });
 
-    test('confirmEmail', function() {
+    test('confirmEmail', () => {
       sandbox.spy(element, 'send');
       element.confirmEmail('foo');
       assert.isTrue(element.send.calledWith(
           'PUT', '/config/server/email.confirm', {token: 'foo'}));
     });
 
-    test('GrReviewerUpdatesParser.parse is used', function() {
+    test('GrReviewerUpdatesParser.parse is used', () => {
       sandbox.stub(GrReviewerUpdatesParser, 'parse').returns(
           Promise.resolve('foo'));
-      return element.getChangeDetail(42).then(function(result) {
+      return element.getChangeDetail(42).then(result => {
         assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce);
         assert.equal(result, 'foo');
       });
     });
 
-    test('setAccountStatus', function(done) {
+    test('setAccountStatus', done => {
       sandbox.stub(element, 'send').returns(Promise.resolve('OOO'));
       sandbox.stub(element, 'getResponseObject')
           .returns(Promise.resolve('OOO'));
       element._cache['/accounts/self/detail'] = {};
-      element.setAccountStatus('OOO').then(function() {
+      element.setAccountStatus('OOO').then(() => {
         assert.isTrue(element.send.calledWith('PUT', '/accounts/self/status',
             {status: 'OOO'}));
         assert.deepEqual(element._cache['/accounts/self/detail'],
@@ -582,24 +580,24 @@
       });
     });
 
-    test('_sendDiffDraft pending requests tracked', function(done) {
-      sandbox.stub(element, 'send', function() {
+    test('_sendDiffDraft pending requests tracked', done => {
+      sandbox.stub(element, 'send', () => {
         assert.equal(element._pendingRequests.sendDiffDraft, 1);
         return Promise.resolve([]);
       });
-      element.saveDiffDraft('', 1, 1).then(function() {
+      element.saveDiffDraft('', 1, 1).then(() => {
         assert.equal(element._pendingRequests.sendDiffDraft, 0);
-        element.deleteDiffDraft('', 1, 1).then(function() {
+        element.deleteDiffDraft('', 1, 1).then(() => {
           assert.equal(element._pendingRequests.sendDiffDraft, 0);
           done();
         });
       });
     });
 
-    test('saveChangeEdit', function(done) {
-      var change_num = '1';
-      var file_name = 'index.php';
-      var file_contents = '<?php';
+    test('saveChangeEdit', done => {
+      const change_num = '1';
+      const file_name = 'index.php';
+      const file_contents = '<?php';
       sandbox.stub(element, 'send').returns(
           Promise.resolve([change_num, file_name, file_contents])
       );
@@ -607,7 +605,7 @@
           .returns(Promise.resolve([change_num, file_name, file_contents]));
       element._cache['/changes/' + change_num + '/edit/' + file_name] = {};
       element.saveChangeEdit(change_num, file_name, file_contents).then(
-          function() {
+          () => {
             assert.isTrue(element.send.calledWith('PUT',
                 '/changes/' + change_num + '/edit/' + file_name,
                 file_contents));
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
index 1ae04a0..73d0dd2 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
@@ -27,20 +27,20 @@
 <script src="gr-reviewer-updates-parser.js"></script>
 
 <script>
-  suite('gr-reviewer-updates-parser tests', function() {
-    var sandbox;
-    var instance;
+  suite('gr-reviewer-updates-parser tests', () => {
+    let sandbox;
+    let instance;
 
-    setup(function() {
+    setup(() => {
       sandbox = sinon.sandbox.create();
     });
 
-    teardown(function() {
+    teardown(() => {
       sandbox.restore();
     });
 
-    test('ignores changes without messages', function() {
-      var change = {};
+    test('ignores changes without messages', () => {
+      const change = {};
       sandbox.stub(
           GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
       sandbox.stub(
@@ -56,8 +56,8 @@
           GrReviewerUpdatesParser.prototype._formatUpdates.called);
     });
 
-    test('ignores changes without reviewer updates', function() {
-      var change = {
+    test('ignores changes without reviewer updates', () => {
+      const change = {
         messages: [],
       };
       sandbox.stub(
@@ -75,8 +75,8 @@
           GrReviewerUpdatesParser.prototype._formatUpdates.called);
     });
 
-    test('ignores changes with empty reviewer updates', function() {
-      var change = {
+    test('ignores changes with empty reviewer updates', () => {
+      const change = {
         messages: [],
         reviewer_updates: [],
       };
@@ -95,18 +95,18 @@
           GrReviewerUpdatesParser.prototype._formatUpdates.called);
     });
 
-    test('filter removed messages', function() {
-      var change = {
-          messages: [
-            {
-              message: 'msg1',
-              tag: 'autogenerated:gerrit:deleteReviewer',
-            },
-            {
-              message: 'msg2',
-              tag: 'foo',
-            }
-          ],
+    test('filter removed messages', () => {
+      const change = {
+        messages: [
+          {
+            message: 'msg1',
+            tag: 'autogenerated:gerrit:deleteReviewer',
+          },
+          {
+            message: 'msg2',
+            tag: 'foo',
+          },
+        ],
       };
       instance = new GrReviewerUpdatesParser(change);
       instance._filterRemovedMessages();
@@ -118,22 +118,22 @@
       });
     });
 
-    test('group reviewer updates', function() {
-      var reviewer1 = {_account_id: 1};
-      var reviewer2 = {_account_id: 2};
-      var date1 = '2017-01-26 12:11:50.000000000';
-      var date2 = '2017-01-26 12:11:55.000000000'; // Within threshold.
-      var date3 = '2017-01-26 12:33:50.000000000';
-      var date4 = '2017-01-26 12:44:50.000000000';
-      var makeItem = function(state, reviewer, opt_date, opt_author) {
+    test('group reviewer updates', () => {
+      const reviewer1 = {_account_id: 1};
+      const reviewer2 = {_account_id: 2};
+      const date1 = '2017-01-26 12:11:50.000000000';
+      const date2 = '2017-01-26 12:11:55.000000000'; // Within threshold.
+      const date3 = '2017-01-26 12:33:50.000000000';
+      const date4 = '2017-01-26 12:44:50.000000000';
+      const makeItem = function(state, reviewer, opt_date, opt_author) {
         return {
-          reviewer: reviewer,
+          reviewer,
           updated: opt_date || date1,
           updated_by: opt_author || reviewer1,
-          state: state,
+          state,
         };
       };
-      var change = {
+      let change = {
         reviewer_updates: [
           makeItem('REVIEWER', reviewer1), // New group.
           makeItem('CC', reviewer2), // Appended.
@@ -198,36 +198,36 @@
       ]);
     });
 
-    test('format reviewer updates', function() {
-      var reviewer1 = {_account_id: 1};
-      var reviewer2 = {_account_id: 2};
-      var makeItem = function(prev, state, opt_reviewer) {
+    test('format reviewer updates', () => {
+      const reviewer1 = {_account_id: 1};
+      const reviewer2 = {_account_id: 2};
+      const makeItem = function(prev, state, opt_reviewer) {
         return {
           reviewer: opt_reviewer || reviewer1,
           prev_state: prev,
-          state: state,
+          state,
         };
       };
-      var makeUpdate = function(items) {
+      const makeUpdate = function(items) {
         return {
           author: reviewer1,
           updated: '',
           updates: items,
         };
       };
-      var change = {
-          reviewer_updates: [
-            makeUpdate([
-              makeItem(undefined, 'CC'),
-              makeItem(undefined, 'CC', reviewer2)
-            ]),
-            makeUpdate([
-              makeItem('CC', 'REVIEWER'),
-              makeItem('REVIEWER', 'REMOVED'),
-              makeItem('REMOVED', 'REVIEWER'),
-              makeItem(undefined, 'REVIEWER', reviewer2),
-            ]),
-          ],
+      const change = {
+        reviewer_updates: [
+          makeUpdate([
+            makeItem(undefined, 'CC'),
+            makeItem(undefined, 'CC', reviewer2),
+          ]),
+          makeUpdate([
+            makeItem('CC', 'REVIEWER'),
+            makeItem('REVIEWER', 'REMOVED'),
+            makeItem('REMOVED', 'REVIEWER'),
+            makeItem(undefined, 'REVIEWER', reviewer2),
+          ]),
+        ],
       };
 
       instance = new GrReviewerUpdatesParser(change);
@@ -237,7 +237,7 @@
       assert.equal(change.reviewer_updates[0].updates.length, 1);
       assert.equal(change.reviewer_updates[1].updates.length, 3);
 
-      var items = change.reviewer_updates[0].updates;
+      let items = change.reviewer_updates[0].updates;
       assert.equal(items[0].message, 'added to CC: ');
       assert.deepEqual(items[0].reviewers, [reviewer1, reviewer2]);
 
diff --git a/polygerrit-ui/app/elements/test/plugin.html b/polygerrit-ui/app/elements/test/plugin.html
new file mode 100644
index 0000000..0314655
--- /dev/null
+++ b/polygerrit-ui/app/elements/test/plugin.html
@@ -0,0 +1,17 @@
+<dom-module id="my-plugin">
+  <script>
+    Gerrit.install(function(plugin) {
+        plugin.registerStyleModule('app-theme', 'myplugin-app-theme');
+    });
+  </script>
+</dom-module>
+
+<dom-module id="myplugin-app-theme">
+  <style>
+    html {
+      --primary-text-color: #F00BAA;
+      --header-background-color: #F01BAA;
+      --footer-background-color: #F02BAA;
+    }
+  </style>
+</dom-module>
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html
index 773b341..e88b70c 100644
--- a/polygerrit-ui/app/styles/app-theme.html
+++ b/polygerrit-ui/app/styles/app-theme.html
@@ -15,7 +15,12 @@
 -->
 <style is="custom-style">
 :root {
+  /* Following vars have LTS for plugin API. */
   --primary-text-color: #000;
+  --header-background-color: #eee;
+  --footer-background-color: var(--header-background-color);
+
+  /* Following are not part of plugin API. */
   --search-border-color: #ddd;
   --selection-background-color: #ebf5fb;
   --default-text-color: #000;
@@ -23,7 +28,6 @@
   --default-horizontal-margin: 1rem;
   --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
   --monospace-font-family: 'Source Code Pro', Menlo, 'Lucida Console', Monaco, monospace;
-
   --iron-overlay-backdrop: {
     transition: none;
   };